450 lines
12 KiB
Go
450 lines
12 KiB
Go
//go:build e2e
|
|
// +build e2e
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rede5/gohorsejobs/backend/internal/database"
|
|
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
|
"github.com/rede5/gohorsejobs/backend/internal/models"
|
|
|
|
"os"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// createAuthToken generates a JWT token for testing
|
|
func createAuthToken(t *testing.T, userID, tenantID string) string {
|
|
t.Helper()
|
|
secret := os.Getenv("JWT_SECRET")
|
|
if secret == "" {
|
|
secret = "gohorse-super-secret-key-2024-production"
|
|
}
|
|
claims := jwt.MapClaims{
|
|
"sub": userID,
|
|
"tenant": tenantID,
|
|
"roles": []string{"superadmin"},
|
|
"iss": "gohorse-jobs",
|
|
"exp": time.Now().Add(time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
tokenStr, err := token.SignedString([]byte(secret))
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate token: %v", err)
|
|
}
|
|
return tokenStr
|
|
}
|
|
|
|
// setupTestCompanyAndUser creates a test company and user in the database and returns their IDs
|
|
func setupTestCompanyAndUser(t *testing.T) (companyID, userID string) {
|
|
t.Helper()
|
|
|
|
// Create user first (required for created_by in jobs)
|
|
userQuery := `
|
|
INSERT INTO users (identifier, password_hash, role, full_name, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (identifier) DO UPDATE SET password_hash = $2
|
|
RETURNING id
|
|
`
|
|
err := database.DB.QueryRow(
|
|
userQuery,
|
|
"e2e-test-jobs-user",
|
|
"hashedpassword",
|
|
"superadmin",
|
|
"E2E Test Jobs User",
|
|
time.Now(),
|
|
time.Now(),
|
|
).Scan(&userID)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test user: %v", err)
|
|
}
|
|
|
|
// Create company
|
|
companyQuery := `
|
|
INSERT INTO companies (name, slug, type, active, verified, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
ON CONFLICT (slug) DO UPDATE SET name = $1
|
|
RETURNING id
|
|
`
|
|
err = database.DB.QueryRow(
|
|
companyQuery,
|
|
"E2E Test Company",
|
|
"e2e-test-company",
|
|
"employer",
|
|
true,
|
|
false,
|
|
time.Now(),
|
|
time.Now(),
|
|
).Scan(&companyID)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test company: %v", err)
|
|
}
|
|
|
|
return companyID, userID
|
|
}
|
|
|
|
// cleanupTestCompanyAndUser removes the test company and user
|
|
func cleanupTestCompanyAndUser(t *testing.T, companyID, userID string) {
|
|
t.Helper()
|
|
database.DB.Exec("DELETE FROM applications WHERE job_id IN (SELECT id FROM jobs WHERE company_id = $1)", companyID)
|
|
database.DB.Exec("DELETE FROM jobs WHERE company_id = $1", companyID)
|
|
database.DB.Exec("DELETE FROM companies WHERE id = $1", companyID)
|
|
// Don't delete user as it might be used elsewhere
|
|
}
|
|
|
|
// createTestJob creates a job directly in the database (bypasses API auth requirement)
|
|
func createTestJob(t *testing.T, companyID, userID string, title string) string {
|
|
t.Helper()
|
|
|
|
query := `
|
|
INSERT INTO jobs (company_id, created_by, title, description, status, visa_support, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING id
|
|
`
|
|
var jobID string
|
|
err := database.DB.QueryRow(
|
|
query,
|
|
companyID,
|
|
userID,
|
|
title,
|
|
"Test job created by E2E tests",
|
|
"open",
|
|
false,
|
|
time.Now(),
|
|
time.Now(),
|
|
).Scan(&jobID)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test job: %v", err)
|
|
}
|
|
|
|
return jobID
|
|
}
|
|
|
|
// TestE2E_Jobs_Read tests job reading operations
|
|
func TestE2E_Jobs_Read(t *testing.T) {
|
|
client := newTestClient()
|
|
companyID, userID := setupTestCompanyAndUser(t)
|
|
defer cleanupTestCompanyAndUser(t, companyID, userID)
|
|
|
|
// Create a test job directly in DB
|
|
jobID := createTestJob(t, companyID, userID, "E2E Test Software Engineer")
|
|
defer database.DB.Exec("DELETE FROM jobs WHERE id = $1", jobID)
|
|
|
|
// =====================
|
|
// 0. CREATE JOB (POST)
|
|
// =====================
|
|
t.Run("CreateJob", func(t *testing.T) {
|
|
// Generate token for auth
|
|
token := createAuthToken(t, userID, companyID)
|
|
client.setAuthToken(token)
|
|
|
|
title := "New Created Job E2E"
|
|
desc := "This is a new job created via API E2E test."
|
|
salaryMin := 5000.0
|
|
salaryMax := 8000.0
|
|
salaryType := "monthly"
|
|
employmentType := "full-time"
|
|
// workMode := "remote"
|
|
location := "Sao Paulo, SP"
|
|
|
|
createReq := dto.CreateJobRequest{
|
|
CompanyID: companyID,
|
|
Title: title,
|
|
Description: desc,
|
|
SalaryMin: &salaryMin,
|
|
SalaryMax: &salaryMax,
|
|
SalaryType: &salaryType,
|
|
EmploymentType: &employmentType,
|
|
WorkingHours: nil,
|
|
Location: &location,
|
|
// WorkMode: &workMode, // Temporarily removed as DTO doesn't support it yet
|
|
Status: "open",
|
|
}
|
|
|
|
resp, err := client.post("/api/v1/jobs", createReq)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create job: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
t.Errorf("Expected status 201, got %d. Body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var job models.Job
|
|
if err := parseJSON(resp, &job); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
if job.Title != title {
|
|
t.Errorf("Expected title '%s', got '%s'", title, job.Title)
|
|
}
|
|
|
|
// Cleanup created job
|
|
database.DB.Exec("DELETE FROM jobs WHERE id = $1", job.ID)
|
|
})
|
|
|
|
// =====================
|
|
// 1. GET JOB BY ID
|
|
// =====================
|
|
t.Run("GetJobByID", func(t *testing.T) {
|
|
resp, err := client.get(fmt.Sprintf("/api/v1/jobs/%s", jobID))
|
|
if err != nil {
|
|
t.Fatalf("Failed to get job: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var job models.Job
|
|
if err := parseJSON(resp, &job); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
if job.ID != jobID {
|
|
t.Errorf("Expected job ID %s, got %s", jobID, job.ID)
|
|
}
|
|
|
|
if job.Title != "E2E Test Software Engineer" {
|
|
t.Errorf("Expected title 'E2E Test Software Engineer', got '%s'", job.Title)
|
|
}
|
|
})
|
|
|
|
// =====================
|
|
// 2. LIST JOBS
|
|
// =====================
|
|
t.Run("ListJobs", func(t *testing.T) {
|
|
resp, err := client.get("/api/v1/jobs")
|
|
if err != nil {
|
|
t.Fatalf("Failed to list jobs: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var response dto.PaginatedResponse
|
|
if err := parseJSON(resp, &response); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
// Should have at least our created job
|
|
if response.Pagination.Total < 1 {
|
|
t.Error("Expected at least 1 job in list")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestE2E_Jobs_Update tests job update operations
|
|
func TestE2E_Jobs_Update(t *testing.T) {
|
|
client := newTestClient()
|
|
companyID, userID := setupTestCompanyAndUser(t)
|
|
defer cleanupTestCompanyAndUser(t, companyID, userID)
|
|
|
|
// Generate and set auth token
|
|
token := createAuthToken(t, userID, companyID)
|
|
client.setAuthToken(token)
|
|
|
|
// Create a test job
|
|
jobID := createTestJob(t, companyID, userID, "E2E Test Job for Update")
|
|
defer database.DB.Exec("DELETE FROM jobs WHERE id = $1", jobID)
|
|
|
|
t.Run("UpdateJobTitle", func(t *testing.T) {
|
|
newTitle := "E2E Test Updated Title"
|
|
updateReq := dto.UpdateJobRequest{
|
|
Title: &newTitle,
|
|
}
|
|
|
|
resp, err := client.put(fmt.Sprintf("/api/v1/jobs/%s", jobID), updateReq)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update job: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var job models.Job
|
|
if err := parseJSON(resp, &job); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
if job.Title != newTitle {
|
|
t.Errorf("Expected title '%s', got '%s'", newTitle, job.Title)
|
|
}
|
|
})
|
|
|
|
t.Run("UpdateJobStatus", func(t *testing.T) {
|
|
newStatus := "closed"
|
|
updateReq := dto.UpdateJobRequest{
|
|
Status: &newStatus,
|
|
}
|
|
|
|
resp, err := client.put(fmt.Sprintf("/api/v1/jobs/%s", jobID), updateReq)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update job: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestE2E_Jobs_Delete tests job deletion
|
|
func TestE2E_Jobs_Delete(t *testing.T) {
|
|
client := newTestClient()
|
|
companyID, userID := setupTestCompanyAndUser(t)
|
|
defer cleanupTestCompanyAndUser(t, companyID, userID)
|
|
|
|
// Generate and set auth token
|
|
token := createAuthToken(t, userID, companyID)
|
|
client.setAuthToken(token)
|
|
|
|
// Create a test job to delete
|
|
jobID := createTestJob(t, companyID, userID, "E2E Test Job to Delete")
|
|
|
|
t.Run("DeleteJob", func(t *testing.T) {
|
|
resp, err := client.delete(fmt.Sprintf("/api/v1/jobs/%s", jobID))
|
|
if err != nil {
|
|
t.Fatalf("Failed to delete job: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
t.Errorf("Expected status 204, got %d", resp.StatusCode)
|
|
}
|
|
|
|
// Verify job is deleted
|
|
verifyResp, _ := client.get(fmt.Sprintf("/api/v1/jobs/%s", jobID))
|
|
if verifyResp.StatusCode != http.StatusNotFound {
|
|
t.Error("Job should be deleted but still exists")
|
|
}
|
|
verifyResp.Body.Close()
|
|
})
|
|
}
|
|
|
|
// TestE2E_Jobs_Filters tests job listing with filters
|
|
func TestE2E_Jobs_Filters(t *testing.T) {
|
|
client := newTestClient()
|
|
companyID, userID := setupTestCompanyAndUser(t)
|
|
defer cleanupTestCompanyAndUser(t, companyID, userID)
|
|
|
|
// Create multiple jobs directly in DB
|
|
for i := 1; i <= 3; i++ {
|
|
createTestJob(t, companyID, userID, fmt.Sprintf("E2E Test Job %d", i))
|
|
}
|
|
|
|
t.Run("Pagination", func(t *testing.T) {
|
|
resp, err := client.get("/api/v1/jobs?page=1&limit=2")
|
|
if err != nil {
|
|
t.Fatalf("Failed to list jobs: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("FilterByCompany", func(t *testing.T) {
|
|
resp, err := client.get(fmt.Sprintf("/api/v1/jobs?companyId=%s", companyID))
|
|
if err != nil {
|
|
t.Fatalf("Failed to list jobs: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var response dto.PaginatedResponse
|
|
if err := parseJSON(resp, &response); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
// Should have our 3 test jobs
|
|
if response.Pagination.Total < 3 {
|
|
t.Errorf("Expected at least 3 jobs for company, got %d", response.Pagination.Total)
|
|
}
|
|
})
|
|
|
|
t.Run("FilterByFeatured", func(t *testing.T) {
|
|
resp, err := client.get("/api/v1/jobs?featured=true&limit=10")
|
|
if err != nil {
|
|
t.Fatalf("Failed to list jobs: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestE2E_Jobs_InvalidInput tests error handling for invalid input
|
|
func TestE2E_Jobs_InvalidInput(t *testing.T) {
|
|
client := newTestClient()
|
|
|
|
t.Run("GetNonExistentJob", func(t *testing.T) {
|
|
resp, err := client.get("/api/v1/jobs/999999")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("Expected status 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("GetInvalidJobID", func(t *testing.T) {
|
|
resp, err := client.get("/api/v1/jobs/invalid")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("Expected status 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("CreateJobInvalidJSON", func(t *testing.T) {
|
|
// Need auth for POST
|
|
companyID, userID := setupTestCompanyAndUser(t)
|
|
defer cleanupTestCompanyAndUser(t, companyID, userID)
|
|
token := createAuthToken(t, userID, companyID)
|
|
|
|
req, _ := http.NewRequest("POST", testServer.URL+"/api/v1/jobs", nil)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Body = http.NoBody
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Errorf("Expected status 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|