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
This commit is contained in:
parent
7b5752f71f
commit
bb970f4a74
11 changed files with 195 additions and 141 deletions
|
|
@ -417,40 +417,14 @@ func extractClientIP(r *http.Request) *string {
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/notifications [get]
|
// @Router /api/v1/notifications [get]
|
||||||
func (h *CoreHandlers) ListNotifications(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) ListNotifications(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
|
||||||
// Assuming ContextUserID is "user_id" string or int?
|
|
||||||
// Looking at CreateUser handler, it gets tenantID which is string.
|
|
||||||
// But auditService.RecordLogin uses resp.User.ID (int).
|
|
||||||
// Typically auth middleware sets user_id. Let's assume middleware.ContextUserID is the key.
|
|
||||||
// I need to check middleware keys ideally.
|
|
||||||
// But commonly it's set.
|
|
||||||
// Let's check how AuditService uses user ID. It gets it from Login response.
|
|
||||||
// Wait, ListUsers doesn't use user ID, only TenantID.
|
|
||||||
// I need to know how to get current User ID.
|
|
||||||
// Using middleware.ContextUserID.
|
|
||||||
userIDVal := r.Context().Value(middleware.ContextUserID)
|
userIDVal := r.Context().Value(middleware.ContextUserID)
|
||||||
if userIDVal == nil {
|
userID, ok := userIDVal.(string)
|
||||||
http.Error(w, "User ID not found", http.StatusUnauthorized)
|
if !ok || userID == "" {
|
||||||
|
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to int
|
notifications, err := h.notificationService.ListNotifications(r.Context(), userID)
|
||||||
var userID int
|
|
||||||
switch v := userIDVal.(type) {
|
|
||||||
case int:
|
|
||||||
userID = v
|
|
||||||
case string:
|
|
||||||
var err error
|
|
||||||
userID, err = strconv.Atoi(v)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid User ID format", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case float64:
|
|
||||||
userID = int(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
notifications, err := h.notificationService.ListNotifications(ctx, userID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -472,18 +446,12 @@ func (h *CoreHandlers) ListNotifications(w http.ResponseWriter, r *http.Request)
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/notifications/{id}/read [patch]
|
// @Router /api/v1/notifications/{id}/read [patch]
|
||||||
func (h *CoreHandlers) MarkNotificationAsRead(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) MarkNotificationAsRead(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
userIDVal := r.Context().Value(middleware.ContextUserID)
|
||||||
userIDVal := ctx.Value(middleware.ContextUserID)
|
userID, ok := userIDVal.(string)
|
||||||
if userIDVal == nil {
|
if !ok || userID == "" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var userID int
|
|
||||||
if v, ok := userIDVal.(string); ok {
|
|
||||||
userID, _ = strconv.Atoi(v)
|
|
||||||
} else if v, ok := userIDVal.(int); ok {
|
|
||||||
userID = v
|
|
||||||
}
|
|
||||||
|
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
|
|
@ -494,7 +462,7 @@ func (h *CoreHandlers) MarkNotificationAsRead(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.notificationService.MarkAsRead(ctx, id, userID); err != nil {
|
if err := h.notificationService.MarkAsRead(r.Context(), id, userID); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -512,20 +480,14 @@ func (h *CoreHandlers) MarkNotificationAsRead(w http.ResponseWriter, r *http.Req
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/notifications/read-all [patch]
|
// @Router /api/v1/notifications/read-all [patch]
|
||||||
func (h *CoreHandlers) MarkAllNotificationsAsRead(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) MarkAllNotificationsAsRead(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
userIDVal := r.Context().Value(middleware.ContextUserID)
|
||||||
userIDVal := ctx.Value(middleware.ContextUserID)
|
userID, ok := userIDVal.(string)
|
||||||
if userIDVal == nil {
|
if !ok || userID == "" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var userID int
|
|
||||||
if v, ok := userIDVal.(string); ok {
|
|
||||||
userID, _ = strconv.Atoi(v)
|
|
||||||
} else if v, ok := userIDVal.(int); ok {
|
|
||||||
userID = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.notificationService.MarkAllAsRead(ctx, userID); err != nil {
|
if err := h.notificationService.MarkAllAsRead(r.Context(), userID); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -545,18 +507,12 @@ func (h *CoreHandlers) MarkAllNotificationsAsRead(w http.ResponseWriter, r *http
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/support/tickets [post]
|
// @Router /api/v1/support/tickets [post]
|
||||||
func (h *CoreHandlers) CreateTicket(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) CreateTicket(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
userIDVal := r.Context().Value(middleware.ContextUserID)
|
||||||
userIDVal := ctx.Value(middleware.ContextUserID)
|
userID, ok := userIDVal.(string)
|
||||||
if userIDVal == nil {
|
if !ok || userID == "" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var userID int
|
|
||||||
if v, ok := userIDVal.(string); ok {
|
|
||||||
userID, _ = strconv.Atoi(v)
|
|
||||||
} else if v, ok := userIDVal.(int); ok {
|
|
||||||
userID = v
|
|
||||||
}
|
|
||||||
|
|
||||||
var req dto.CreateTicketRequest
|
var req dto.CreateTicketRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
|
@ -564,7 +520,7 @@ func (h *CoreHandlers) CreateTicket(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ticket, err := h.ticketService.CreateTicket(ctx, userID, req.Subject, req.Priority)
|
ticket, err := h.ticketService.CreateTicket(r.Context(), userID, req.Subject, req.Priority)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -572,7 +528,7 @@ func (h *CoreHandlers) CreateTicket(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Create initial message if provided
|
// Create initial message if provided
|
||||||
if req.Message != "" {
|
if req.Message != "" {
|
||||||
_, _ = h.ticketService.AddMessage(ctx, ticket.ID, userID, req.Message)
|
_, _ = h.ticketService.AddMessage(r.Context(), ticket.ID, userID, req.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|
@ -591,20 +547,14 @@ func (h *CoreHandlers) CreateTicket(w http.ResponseWriter, r *http.Request) {
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/support/tickets [get]
|
// @Router /api/v1/support/tickets [get]
|
||||||
func (h *CoreHandlers) ListTickets(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) ListTickets(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
userIDVal := r.Context().Value(middleware.ContextUserID)
|
||||||
userIDVal := ctx.Value(middleware.ContextUserID)
|
userID, ok := userIDVal.(string)
|
||||||
if userIDVal == nil {
|
if !ok || userID == "" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var userID int
|
|
||||||
if v, ok := userIDVal.(string); ok {
|
|
||||||
userID, _ = strconv.Atoi(v)
|
|
||||||
} else if v, ok := userIDVal.(int); ok {
|
|
||||||
userID = v
|
|
||||||
}
|
|
||||||
|
|
||||||
tickets, err := h.ticketService.ListTickets(ctx, userID)
|
tickets, err := h.ticketService.ListTickets(r.Context(), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -627,18 +577,12 @@ func (h *CoreHandlers) ListTickets(w http.ResponseWriter, r *http.Request) {
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/support/tickets/{id} [get]
|
// @Router /api/v1/support/tickets/{id} [get]
|
||||||
func (h *CoreHandlers) GetTicket(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) GetTicket(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
userIDVal := r.Context().Value(middleware.ContextUserID)
|
||||||
userIDVal := ctx.Value(middleware.ContextUserID)
|
userID, ok := userIDVal.(string)
|
||||||
if userIDVal == nil {
|
if !ok || userID == "" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var userID int
|
|
||||||
if v, ok := userIDVal.(string); ok {
|
|
||||||
userID, _ = strconv.Atoi(v)
|
|
||||||
} else if v, ok := userIDVal.(int); ok {
|
|
||||||
userID = v
|
|
||||||
}
|
|
||||||
|
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
|
|
@ -648,7 +592,7 @@ func (h *CoreHandlers) GetTicket(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ticket, messages, err := h.ticketService.GetTicket(ctx, id, userID)
|
ticket, messages, err := h.ticketService.GetTicket(r.Context(), id, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
|
@ -677,18 +621,12 @@ func (h *CoreHandlers) GetTicket(w http.ResponseWriter, r *http.Request) {
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/support/tickets/{id}/messages [post]
|
// @Router /api/v1/support/tickets/{id}/messages [post]
|
||||||
func (h *CoreHandlers) AddMessage(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) AddMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
userIDVal := r.Context().Value(middleware.ContextUserID)
|
||||||
userIDVal := ctx.Value(middleware.ContextUserID)
|
userID, ok := userIDVal.(string)
|
||||||
if userIDVal == nil {
|
if !ok || userID == "" {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var userID int
|
|
||||||
if v, ok := userIDVal.(string); ok {
|
|
||||||
userID, _ = strconv.Atoi(v)
|
|
||||||
} else if v, ok := userIDVal.(int); ok {
|
|
||||||
userID = v
|
|
||||||
}
|
|
||||||
|
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
|
|
@ -705,7 +643,7 @@ func (h *CoreHandlers) AddMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := h.ticketService.AddMessage(ctx, id, userID, req.Message)
|
msg, err := h.ticketService.AddMessage(r.Context(), id, userID, req.Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -731,19 +669,12 @@ func (h *CoreHandlers) AddMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
func (h *CoreHandlers) UpdateMyProfile(w http.ResponseWriter, r *http.Request) {
|
func (h *CoreHandlers) UpdateMyProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
userIDVal := ctx.Value(middleware.ContextUserID)
|
userIDVal := ctx.Value(middleware.ContextUserID)
|
||||||
if userIDVal == nil {
|
userID, ok := userIDVal.(string)
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
if !ok || userID == "" {
|
||||||
|
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var userID int
|
|
||||||
if v, ok := userIDVal.(string); ok {
|
|
||||||
userID, _ = strconv.Atoi(v)
|
|
||||||
} else if v, ok := userIDVal.(int); ok {
|
|
||||||
userID = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert userID to string if updateUC needs string (it does for ID)
|
|
||||||
idStr := strconv.Itoa(userID)
|
|
||||||
// TenantID needed? updateUC takes tenantID.
|
// TenantID needed? updateUC takes tenantID.
|
||||||
tenantID, _ := ctx.Value(middleware.ContextTenantID).(string)
|
tenantID, _ := ctx.Value(middleware.ContextTenantID).(string)
|
||||||
|
|
||||||
|
|
@ -753,12 +684,8 @@ func (h *CoreHandlers) UpdateMyProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse existing UpdateUserUseCase but ensuring ID matches Me.
|
// userID (string) passed directly as first arg
|
||||||
// Assuming UpdateUserUseCase handles generic updates.
|
resp, err := h.updateUserUC.Execute(ctx, userID, tenantID, req)
|
||||||
// Wait, UpdateUserUseCase might require Admin role if it checks permissions.
|
|
||||||
// If UpdateUserUseCase is simple DB update, it's fine.
|
|
||||||
// Let's assume for now. If it fails due to RBAC, we need a separate usecase.
|
|
||||||
resp, err := h.updateUserUC.Execute(ctx, idStr, tenantID, req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/rede5/gohorsejobs/backend/internal/api/middleware"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/models"
|
"github.com/rede5/gohorsejobs/backend/internal/models"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/services"
|
"github.com/rede5/gohorsejobs/backend/internal/services"
|
||||||
|
|
@ -103,6 +104,7 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) {
|
||||||
// @Param job body dto.CreateJobRequest true "Job data"
|
// @Param job body dto.CreateJobRequest true "Job data"
|
||||||
// @Success 201 {object} models.Job
|
// @Success 201 {object} models.Job
|
||||||
// @Failure 400 {string} string "Bad Request"
|
// @Failure 400 {string} string "Bad Request"
|
||||||
|
// @Failure 401 {string} string "Unauthorized"
|
||||||
// @Failure 500 {string} string "Internal Server Error"
|
// @Failure 500 {string} string "Internal Server Error"
|
||||||
// @Router /api/v1/jobs [post]
|
// @Router /api/v1/jobs [post]
|
||||||
func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -114,7 +116,15 @@ func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Validate request (omitted for brevity, assume validation middleware or service validation)
|
// Validate request (omitted for brevity, assume validation middleware or service validation)
|
||||||
|
|
||||||
job, err := h.Service.CreateJob(req)
|
// 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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rede5/gohorsejobs/backend/internal/api/middleware"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/models"
|
"github.com/rede5/gohorsejobs/backend/internal/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -16,7 +18,7 @@ import (
|
||||||
// mockJobService is a mock implementation of the job service for testing
|
// mockJobService is a mock implementation of the job service for testing
|
||||||
type mockJobService struct {
|
type mockJobService struct {
|
||||||
getJobsFunc func(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error)
|
getJobsFunc func(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error)
|
||||||
createJobFunc func(req dto.CreateJobRequest) (*models.Job, error)
|
createJobFunc func(req dto.CreateJobRequest, createdBy string) (*models.Job, error)
|
||||||
getJobByIDFunc func(id string) (*models.Job, error)
|
getJobByIDFunc func(id string) (*models.Job, error)
|
||||||
updateJobFunc func(id string, req dto.UpdateJobRequest) (*models.Job, error)
|
updateJobFunc func(id string, req dto.UpdateJobRequest) (*models.Job, error)
|
||||||
deleteJobFunc func(id string) error
|
deleteJobFunc func(id string) error
|
||||||
|
|
@ -29,9 +31,9 @@ func (m *mockJobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCom
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockJobService) CreateJob(req dto.CreateJobRequest) (*models.Job, error) {
|
func (m *mockJobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) {
|
||||||
if m.createJobFunc != nil {
|
if m.createJobFunc != nil {
|
||||||
return m.createJobFunc(req)
|
return m.createJobFunc(req, createdBy)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +62,7 @@ func (m *mockJobService) DeleteJob(id string) error {
|
||||||
// JobServiceInterface defines the interface for job service operations
|
// JobServiceInterface defines the interface for job service operations
|
||||||
type JobServiceInterface interface {
|
type JobServiceInterface interface {
|
||||||
GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error)
|
GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error)
|
||||||
CreateJob(req dto.CreateJobRequest) (*models.Job, error)
|
CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error)
|
||||||
GetJobByID(id string) (*models.Job, error)
|
GetJobByID(id string) (*models.Job, error)
|
||||||
UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error)
|
UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error)
|
||||||
DeleteJob(id string) error
|
DeleteJob(id string) error
|
||||||
|
|
@ -100,7 +102,15 @@ func (h *testableJobHandler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job, err := h.service.CreateJob(req)
|
// 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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -219,7 +229,8 @@ func TestGetJobs_Error(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateJob_Success(t *testing.T) {
|
func TestCreateJob_Success(t *testing.T) {
|
||||||
mockService := &mockJobService{
|
mockService := &mockJobService{
|
||||||
createJobFunc: func(req dto.CreateJobRequest) (*models.Job, error) {
|
createJobFunc: func(req dto.CreateJobRequest, createdBy string) (*models.Job, error) {
|
||||||
|
assert.Equal(t, "user-123", createdBy)
|
||||||
return &models.Job{
|
return &models.Job{
|
||||||
ID: "1",
|
ID: "1",
|
||||||
CompanyID: req.CompanyID,
|
CompanyID: req.CompanyID,
|
||||||
|
|
@ -244,6 +255,11 @@ func TestCreateJob_Success(t *testing.T) {
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/jobs", bytes.NewReader(body))
|
req := httptest.NewRequest("POST", "/jobs", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
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()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
handler.CreateJob(rr, req)
|
handler.CreateJob(rr, req)
|
||||||
|
|
@ -272,7 +288,7 @@ func TestCreateJob_InvalidJSON(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateJob_ServiceError(t *testing.T) {
|
func TestCreateJob_ServiceError(t *testing.T) {
|
||||||
mockService := &mockJobService{
|
mockService := &mockJobService{
|
||||||
createJobFunc: func(req dto.CreateJobRequest) (*models.Job, error) {
|
createJobFunc: func(req dto.CreateJobRequest, createdBy string) (*models.Job, error) {
|
||||||
return nil, assert.AnError
|
return nil, assert.AnError
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -289,6 +305,11 @@ func TestCreateJob_ServiceError(t *testing.T) {
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/jobs", bytes.NewReader(body))
|
req := httptest.NewRequest("POST", "/jobs", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
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()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
handler.CreateJob(rr, req)
|
handler.CreateJob(rr, req)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID int `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
Type string `json:"type"` // info, success, warning, error
|
Type string `json:"type"` // info, success, warning, error
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
type Ticket struct {
|
type Ticket struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID int `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Status string `json:"status"` // open, in_progress, closed
|
Status string `json:"status"` // open, in_progress, closed
|
||||||
Priority string `json:"priority"` // low, medium, high
|
Priority string `json:"priority"` // low, medium, high
|
||||||
|
|
@ -17,7 +17,7 @@ type Ticket struct {
|
||||||
type TicketMessage struct {
|
type TicketMessage struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
TicketID string `json:"ticketId"`
|
TicketID string `json:"ticketId"`
|
||||||
UserID int `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,10 +143,10 @@ func NewRouter() http.Handler {
|
||||||
|
|
||||||
// Job Routes
|
// Job Routes
|
||||||
mux.HandleFunc("GET /api/v1/jobs", jobHandler.GetJobs)
|
mux.HandleFunc("GET /api/v1/jobs", jobHandler.GetJobs)
|
||||||
mux.HandleFunc("POST /api/v1/jobs", jobHandler.CreateJob)
|
mux.Handle("POST /api/v1/jobs", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.CreateJob)))
|
||||||
mux.HandleFunc("GET /api/v1/jobs/{id}", jobHandler.GetJobByID)
|
mux.HandleFunc("GET /api/v1/jobs/{id}", jobHandler.GetJobByID)
|
||||||
mux.HandleFunc("PUT /api/v1/jobs/{id}", jobHandler.UpdateJob)
|
mux.Handle("PUT /api/v1/jobs/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.UpdateJob)))
|
||||||
mux.HandleFunc("DELETE /api/v1/jobs/{id}", jobHandler.DeleteJob)
|
mux.Handle("DELETE /api/v1/jobs/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.DeleteJob)))
|
||||||
|
|
||||||
// --- ADMIN ROUTES (Consolidated to Standard Paths with RBAC) ---
|
// --- ADMIN ROUTES (Consolidated to Standard Paths with RBAC) ---
|
||||||
// /api/v1/admin/access/roles -> /api/v1/users/roles
|
// /api/v1/admin/access/roles -> /api/v1/users/roles
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,19 @@ func NewJobService(db *sql.DB) *JobService {
|
||||||
return &JobService{DB: db}
|
return &JobService{DB: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *JobService) CreateJob(req dto.CreateJobRequest) (*models.Job, error) {
|
func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) {
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO jobs (
|
INSERT INTO jobs (
|
||||||
company_id, title, description, salary_min, salary_max, salary_type,
|
company_id, created_by, title, description, salary_min, salary_max, salary_type,
|
||||||
employment_type, working_hours, location, region_id, city_id,
|
employment_type, working_hours, location, region_id, city_id,
|
||||||
requirements, benefits, visa_support, language_level, status, created_at, updated_at
|
requirements, benefits, visa_support, language_level, status, created_at, updated_at
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||||
RETURNING id, created_at, updated_at
|
RETURNING id, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
job := &models.Job{
|
job := &models.Job{
|
||||||
CompanyID: req.CompanyID,
|
CompanyID: req.CompanyID,
|
||||||
|
CreatedBy: createdBy,
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
SalaryMin: req.SalaryMin,
|
SalaryMin: req.SalaryMin,
|
||||||
|
|
@ -51,7 +52,7 @@ func (s *JobService) CreateJob(req dto.CreateJobRequest) (*models.Job, error) {
|
||||||
|
|
||||||
err := s.DB.QueryRow(
|
err := s.DB.QueryRow(
|
||||||
query,
|
query,
|
||||||
job.CompanyID, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType,
|
job.CompanyID, job.CreatedBy, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType,
|
||||||
job.EmploymentType, job.WorkingHours, job.Location, job.RegionID, job.CityID,
|
job.EmploymentType, job.WorkingHours, job.Location, job.RegionID, job.CityID,
|
||||||
job.Requirements, job.Benefits, job.VisaSupport, job.LanguageLevel, job.Status, job.CreatedAt, job.UpdatedAt,
|
job.Requirements, job.Benefits, job.VisaSupport, job.LanguageLevel, job.Status, job.CreatedAt, job.UpdatedAt,
|
||||||
).Scan(&job.ID, &job.CreatedAt, &job.UpdatedAt)
|
).Scan(&job.ID, &job.CreatedAt, &job.UpdatedAt)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ func TestCreateJob(t *testing.T) {
|
||||||
},
|
},
|
||||||
mockRun: func() {
|
mockRun: func() {
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO jobs`)).
|
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO jobs`)).
|
||||||
WithArgs("1", "Go Developer", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "published", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
WithArgs("1", "user-123", "Go Developer", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "published", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow("100", time.Now(), time.Now()))
|
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow("100", time.Now(), time.Now()))
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
|
@ -57,7 +57,7 @@ func TestCreateJob(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tt.mockRun()
|
tt.mockRun()
|
||||||
got, err := service.CreateJob(tt.req)
|
got, err := service.CreateJob(tt.req, "user-123")
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("JobService.CreateJob() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("JobService.CreateJob() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ func NewNotificationService(db *sql.DB) *NotificationService {
|
||||||
return &NotificationService{DB: db}
|
return &NotificationService{DB: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NotificationService) CreateNotification(ctx context.Context, userID int, nType, title, message string, link *string) error {
|
func (s *NotificationService) CreateNotification(ctx context.Context, userID string, nType, title, message string, link *string) error {
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO notifications (user_id, type, title, message, link, created_at, updated_at)
|
INSERT INTO notifications (user_id, type, title, message, link, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
||||||
|
|
@ -24,7 +24,7 @@ func (s *NotificationService) CreateNotification(ctx context.Context, userID int
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NotificationService) ListNotifications(ctx context.Context, userID int) ([]models.Notification, error) {
|
func (s *NotificationService) ListNotifications(ctx context.Context, userID string) ([]models.Notification, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, user_id, type, title, message, link, read_at, created_at, updated_at
|
SELECT id, user_id, type, title, message, link, read_at, created_at, updated_at
|
||||||
FROM notifications
|
FROM notifications
|
||||||
|
|
@ -59,7 +59,7 @@ func (s *NotificationService) ListNotifications(ctx context.Context, userID int)
|
||||||
return notifications, nil
|
return notifications, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NotificationService) MarkAsRead(ctx context.Context, id string, userID int) error {
|
func (s *NotificationService) MarkAsRead(ctx context.Context, id string, userID string) error {
|
||||||
query := `
|
query := `
|
||||||
UPDATE notifications
|
UPDATE notifications
|
||||||
SET read_at = NOW(), updated_at = NOW()
|
SET read_at = NOW(), updated_at = NOW()
|
||||||
|
|
@ -69,7 +69,7 @@ func (s *NotificationService) MarkAsRead(ctx context.Context, id string, userID
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NotificationService) MarkAllAsRead(ctx context.Context, userID int) error {
|
func (s *NotificationService) MarkAllAsRead(ctx context.Context, userID string) error {
|
||||||
query := `
|
query := `
|
||||||
UPDATE notifications
|
UPDATE notifications
|
||||||
SET read_at = NOW(), updated_at = NOW()
|
SET read_at = NOW(), updated_at = NOW()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ func NewTicketService(db *sql.DB) *TicketService {
|
||||||
return &TicketService{DB: db}
|
return &TicketService{DB: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TicketService) CreateTicket(ctx context.Context, userID int, subject, priority string) (*models.Ticket, error) {
|
func (s *TicketService) CreateTicket(ctx context.Context, userID string, subject, priority string) (*models.Ticket, error) {
|
||||||
if priority == "" {
|
if priority == "" {
|
||||||
priority = "medium"
|
priority = "medium"
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +35,7 @@ func (s *TicketService) CreateTicket(ctx context.Context, userID int, subject, p
|
||||||
return &t, nil
|
return &t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TicketService) ListTickets(ctx context.Context, userID int) ([]models.Ticket, error) {
|
func (s *TicketService) ListTickets(ctx context.Context, userID string) ([]models.Ticket, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, user_id, subject, status, priority, created_at, updated_at
|
SELECT id, user_id, subject, status, priority, created_at, updated_at
|
||||||
FROM tickets
|
FROM tickets
|
||||||
|
|
@ -61,7 +61,7 @@ func (s *TicketService) ListTickets(ctx context.Context, userID int) ([]models.T
|
||||||
return tickets, nil
|
return tickets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TicketService) GetTicket(ctx context.Context, ticketID string, userID int) (*models.Ticket, []models.TicketMessage, error) {
|
func (s *TicketService) GetTicket(ctx context.Context, ticketID string, userID string) (*models.Ticket, []models.TicketMessage, error) {
|
||||||
// 1. Get Ticket
|
// 1. Get Ticket
|
||||||
queryTicket := `
|
queryTicket := `
|
||||||
SELECT id, user_id, subject, status, priority, created_at, updated_at
|
SELECT id, user_id, subject, status, priority, created_at, updated_at
|
||||||
|
|
@ -106,7 +106,7 @@ func (s *TicketService) GetTicket(ctx context.Context, ticketID string, userID i
|
||||||
return &t, messages, nil
|
return &t, messages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TicketService) AddMessage(ctx context.Context, ticketID string, userID int, message string) (*models.TicketMessage, error) {
|
func (s *TicketService) AddMessage(ctx context.Context, ticketID string, userID string, message string) (*models.TicketMessage, error) {
|
||||||
// Verify ticket ownership first (or admin access, but keeping simple for now)
|
// Verify ticket ownership first (or admin access, but keeping simple for now)
|
||||||
var count int
|
var count int
|
||||||
err := s.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM tickets WHERE id = $1 AND user_id = $2", ticketID, userID).Scan(&count)
|
err := s.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM tickets WHERE id = $1 AND user_id = $2", ticketID, userID).Scan(&count)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -12,8 +13,35 @@ import (
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/database"
|
"github.com/rede5/gohorsejobs/backend/internal/database"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/models"
|
"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
|
// setupTestCompanyAndUser creates a test company and user in the database and returns their IDs
|
||||||
func setupTestCompanyAndUser(t *testing.T) (companyID, userID string) {
|
func setupTestCompanyAndUser(t *testing.T) (companyID, userID string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
@ -112,6 +140,61 @@ func TestE2E_Jobs_Read(t *testing.T) {
|
||||||
jobID := createTestJob(t, companyID, userID, "E2E Test Software Engineer")
|
jobID := createTestJob(t, companyID, userID, "E2E Test Software Engineer")
|
||||||
defer database.DB.Exec("DELETE FROM jobs WHERE id = $1", jobID)
|
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
|
// 1. GET JOB BY ID
|
||||||
// =====================
|
// =====================
|
||||||
|
|
@ -293,6 +376,18 @@ func TestE2E_Jobs_Filters(t *testing.T) {
|
||||||
t.Errorf("Expected at least 3 jobs for company, got %d", response.Pagination.Total)
|
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
|
// TestE2E_Jobs_InvalidInput tests error handling for invalid input
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue