gohorsejobs/backend/internal/handlers/job_handler_test.go
Tiago Yamamoto bb970f4a74 fix(backend): resolve 500 errors on jobs, notifications and secure routes
- Fix CreateJob 500 error by extracting user ID correctly
- Secure Create/Update/Delete Job routes with AuthGuard
- Fix Notifications/Tickets/Profile 500 error (UUID vs Int mismatch)
- Add E2E test for CreateJob
2025-12-24 17:48:06 -03:00

403 lines
10 KiB
Go

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)
}