package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/rede5/gohorsejobs/backend/internal/api/middleware" "github.com/rede5/gohorsejobs/backend/internal/dto" "github.com/rede5/gohorsejobs/backend/internal/models" "github.com/stretchr/testify/assert" ) // mockJobService is a mock implementation of the job service for testing type mockJobService struct { getJobsFunc func(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) createJobFunc func(req dto.CreateJobRequest, createdBy string) (*models.Job, error) getJobByIDFunc func(id string) (*models.Job, error) updateJobFunc func(id string, req dto.UpdateJobRequest) (*models.Job, error) deleteJobFunc func(id string) error } func (m *mockJobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) { if m.getJobsFunc != nil { return m.getJobsFunc(filter) } return nil, 0, nil } func (m *mockJobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) { if m.createJobFunc != nil { return m.createJobFunc(req, createdBy) } return nil, nil } func (m *mockJobService) GetJobByID(id string) (*models.Job, error) { if m.getJobByIDFunc != nil { return m.getJobByIDFunc(id) } return nil, nil } func (m *mockJobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) { if m.updateJobFunc != nil { return m.updateJobFunc(id, req) } return nil, nil } func (m *mockJobService) DeleteJob(id string) error { if m.deleteJobFunc != nil { return m.deleteJobFunc(id) } return nil } // JobServiceInterface defines the interface for job service operations type JobServiceInterface interface { GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) GetJobByID(id string) (*models.Job, error) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) DeleteJob(id string) error } // testableJobHandler wraps an interface for testing type testableJobHandler struct { service JobServiceInterface } func newTestableJobHandler(service JobServiceInterface) *testableJobHandler { return &testableJobHandler{service: service} } func (h *testableJobHandler) GetJobs(w http.ResponseWriter, r *http.Request) { jobs, total, err := h.service.GetJobs(dto.JobFilterQuery{}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } response := dto.PaginatedResponse{ Data: jobs, Pagination: dto.Pagination{ Total: total, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func (h *testableJobHandler) CreateJob(w http.ResponseWriter, r *http.Request) { var req dto.CreateJobRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Extract UserID from context val := r.Context().Value(middleware.ContextUserID) userID, ok := val.(string) if !ok || userID == "" { http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized) return } job, err := h.service.CreateJob(req, userID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(job) } func (h *testableJobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") job, err := h.service.GetJobByID(id) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(job) } func (h *testableJobHandler) DeleteJob(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := h.service.DeleteJob(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // ============================================================================= // Test Cases // ============================================================================= func TestGetJobs_Success(t *testing.T) { mockService := &mockJobService{ getJobsFunc: func(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) { return []models.JobWithCompany{ { Job: models.Job{ ID: "1", CompanyID: "1", Title: "Software Engineer", Status: "open", CreatedAt: time.Now(), UpdatedAt: time.Now(), }, CompanyName: "TestCorp", }, { Job: models.Job{ ID: "2", CompanyID: "1", Title: "DevOps Engineer", Status: "open", CreatedAt: time.Now(), UpdatedAt: time.Now(), }, CompanyName: "TestCorp", }, }, 2, nil }, } handler := newTestableJobHandler(mockService) req := httptest.NewRequest("GET", "/jobs", nil) rr := httptest.NewRecorder() handler.GetJobs(rr, req) assert.Equal(t, http.StatusOK, rr.Code) var response dto.PaginatedResponse err := json.Unmarshal(rr.Body.Bytes(), &response) assert.NoError(t, err) assert.Equal(t, 2, response.Pagination.Total) } func TestGetJobs_Empty(t *testing.T) { mockService := &mockJobService{ getJobsFunc: func(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) { return []models.JobWithCompany{}, 0, nil }, } handler := newTestableJobHandler(mockService) req := httptest.NewRequest("GET", "/jobs", nil) rr := httptest.NewRecorder() handler.GetJobs(rr, req) assert.Equal(t, http.StatusOK, rr.Code) var response dto.PaginatedResponse err := json.Unmarshal(rr.Body.Bytes(), &response) assert.NoError(t, err) assert.Equal(t, 0, response.Pagination.Total) } func TestGetJobs_Error(t *testing.T) { mockService := &mockJobService{ getJobsFunc: func(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) { return nil, 0, assert.AnError }, } handler := newTestableJobHandler(mockService) req := httptest.NewRequest("GET", "/jobs", nil) rr := httptest.NewRecorder() handler.GetJobs(rr, req) assert.Equal(t, http.StatusInternalServerError, rr.Code) } func TestCreateJob_Success(t *testing.T) { mockService := &mockJobService{ createJobFunc: func(req dto.CreateJobRequest, createdBy string) (*models.Job, error) { assert.Equal(t, "user-123", createdBy) return &models.Job{ ID: "1", CompanyID: req.CompanyID, Title: req.Title, Description: req.Description, Status: req.Status, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, nil }, } handler := newTestableJobHandler(mockService) jobReq := dto.CreateJobRequest{ CompanyID: "1", Title: "Backend Developer", Description: "Build awesome APIs", Status: "open", } body, _ := json.Marshal(jobReq) req := httptest.NewRequest("POST", "/jobs", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") // Inject Context ctx := context.WithValue(req.Context(), middleware.ContextUserID, "user-123") req = req.WithContext(ctx) rr := httptest.NewRecorder() handler.CreateJob(rr, req) assert.Equal(t, http.StatusCreated, rr.Code) var job models.Job err := json.Unmarshal(rr.Body.Bytes(), &job) assert.NoError(t, err) assert.Equal(t, "Backend Developer", job.Title) } func TestCreateJob_InvalidJSON(t *testing.T) { mockService := &mockJobService{} handler := newTestableJobHandler(mockService) req := httptest.NewRequest("POST", "/jobs", bytes.NewReader([]byte("invalid json"))) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() handler.CreateJob(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) } func TestCreateJob_ServiceError(t *testing.T) { mockService := &mockJobService{ createJobFunc: func(req dto.CreateJobRequest, createdBy string) (*models.Job, error) { return nil, assert.AnError }, } handler := newTestableJobHandler(mockService) jobReq := dto.CreateJobRequest{ CompanyID: "1", Title: "Backend Developer", Description: "Build awesome APIs", Status: "open", } body, _ := json.Marshal(jobReq) req := httptest.NewRequest("POST", "/jobs", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") // Inject Context ctx := context.WithValue(req.Context(), middleware.ContextUserID, "user-123") req = req.WithContext(ctx) rr := httptest.NewRecorder() handler.CreateJob(rr, req) assert.Equal(t, http.StatusInternalServerError, rr.Code) } func TestGetJobByID_Success(t *testing.T) { mockService := &mockJobService{ getJobByIDFunc: func(id string) (*models.Job, error) { return &models.Job{ ID: id, CompanyID: "1", Title: "Software Engineer", Description: "Great job opportunity", Status: "open", CreatedAt: time.Now(), UpdatedAt: time.Now(), }, nil }, } handler := newTestableJobHandler(mockService) req := httptest.NewRequest("GET", "/jobs/1", nil) req.SetPathValue("id", "1") rr := httptest.NewRecorder() handler.GetJobByID(rr, req) assert.Equal(t, http.StatusOK, rr.Code) var job models.Job err := json.Unmarshal(rr.Body.Bytes(), &job) assert.NoError(t, err) assert.Equal(t, "Software Engineer", job.Title) } func TestGetJobByID_NotFound(t *testing.T) { mockService := &mockJobService{ getJobByIDFunc: func(id string) (*models.Job, error) { return nil, assert.AnError }, } handler := newTestableJobHandler(mockService) req := httptest.NewRequest("GET", "/jobs/999", nil) req.SetPathValue("id", "999") rr := httptest.NewRecorder() handler.GetJobByID(rr, req) assert.Equal(t, http.StatusNotFound, rr.Code) } func TestDeleteJob_Success(t *testing.T) { mockService := &mockJobService{ deleteJobFunc: func(id string) error { return nil }, } handler := newTestableJobHandler(mockService) req := httptest.NewRequest("DELETE", "/jobs/1", nil) req.SetPathValue("id", "1") rr := httptest.NewRecorder() handler.DeleteJob(rr, req) assert.Equal(t, http.StatusNoContent, rr.Code) } func TestDeleteJob_Error(t *testing.T) { mockService := &mockJobService{ deleteJobFunc: func(id string) error { return assert.AnError }, } handler := newTestableJobHandler(mockService) req := httptest.NewRequest("DELETE", "/jobs/1", nil) req.SetPathValue("id", "1") rr := httptest.NewRecorder() handler.DeleteJob(rr, req) assert.Equal(t, http.StatusInternalServerError, rr.Code) }