//go:build e2e // +build e2e package e2e import ( "fmt" "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" ) // setupTestCompanyAndUser creates a test company and user in the database and returns their IDs func setupTestCompanyAndUser(t *testing.T) (companyID, userID int) { 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 int) { 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 int, title string) int { 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 int 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) // ===================== // 1. GET JOB BY ID // ===================== t.Run("GetJobByID", func(t *testing.T) { resp, err := client.get(fmt.Sprintf("/jobs/%d", 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 %d, got %d", 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("/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) // 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("/jobs/%d", 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("/jobs/%d", 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) // 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("/jobs/%d", 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("/jobs/%d", 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("/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("/jobs?companyId=%d", 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) } }) } // 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("/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("/jobs/invalid") 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) } }) t.Run("CreateJobInvalidJSON", func(t *testing.T) { req, _ := http.NewRequest("POST", testServer.URL+"/jobs", nil) req.Header.Set("Content-Type", "application/json") 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) } }) }