gohorsejobs/backend/tests/e2e/jobs_e2e_test.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)
}
})
}