//go:build test // +build test package scheduling import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "crussell/db" "crussell/mw" "crussell/testutils/jwt" "crussell/testutils/testdb" "github.com/jackc/pgx/v5/pgxpool" ) func setupTestDB(t *testing.T) func() { t.Helper() pool := testdb.Pool(t) testdb.Migrate(t, pool) originalDB := db.DB db.DB = pool jwt.Init() // Seed default working hours seedDefaultWorkingHours(t, pool) return func() { db.DB = originalDB pool.Close() } } func seedDefaultWorkingHours(t *testing.T, pool *pgxpool.Pool) { t.Helper() // Seed 7 days of working hours (Monday=0 to Sunday=6) hours := []struct { weekday int startTime string endTime string isOpen bool }{ {0, "09:00", "17:00", true}, // Monday {1, "09:00", "17:00", true}, // Tuesday {2, "09:00", "17:00", true}, // Wednesday {3, "09:00", "17:00", true}, // Thursday {4, "09:00", "17:00", true}, // Friday {5, "10:00", "16:00", true}, // Saturday {6, "00:00", "00:00", false}, // Sunday } for _, h := range hours { _, err := pool.Exec(context.Background(), ` INSERT INTO working_hours (weekday, start_time, end_time, is_open) VALUES ($1, $2, $3, $4) ON CONFLICT (weekday) DO UPDATE SET start_time = $2, end_time = $3, is_open = $4 `, h.weekday, h.startTime, h.endTime, h.isOpen) if err != nil { t.Fatalf("failed to seed working hours: %v", err) } } } func makeRequest(handler http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder { var req *http.Request if body != nil { bodyBytes, _ := json.Marshal(body) req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, path, nil) } w := httptest.NewRecorder() handler.ServeHTTP(w, req) return w } func makeAuthRequest(handler http.Handler, method, path, token string, body interface{}) *httptest.ResponseRecorder { var req *http.Request if body != nil { bodyBytes, _ := json.Marshal(body) req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, path, nil) } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() handler.ServeHTTP(w, req) return w } // --- Tests for GetDefaultHours --- func TestScheduling_GetDefaultHours(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() handler := http.HandlerFunc(GetDefaultHours) w := makeRequest(handler, "GET", "/api/scheduling/default-hours", nil) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String()) } var response []DefaultHours if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if len(response) != 7 { t.Errorf("expected 7 days of hours, got %d", len(response)) } // Verify Monday (weekday 0) has our seeded hours var monday *DefaultHours for i := range response { if response[i].Weekday == 0 { monday = &response[i] break } } if monday == nil { t.Fatal("expected Monday hours in response") } // Database returns HH:MM:SS format if monday.StartTime != "09:00:00" { t.Errorf("expected Monday start time 09:00:00, got %s", monday.StartTime) } if monday.EndTime != "17:00:00" { t.Errorf("expected Monday end time 17:00:00, got %s", monday.EndTime) } if !monday.IsOpen { t.Error("expected Monday to be open") } } // --- Tests for UpdateDefaultHours --- func TestScheduling_UpdateDefaultHours_Admin(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() adminToken := jwt.GenerateAdminToken() handler := http.HandlerFunc(UpdateDefaultHours) newHours := []DefaultHours{ {Weekday: 0, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 1, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 2, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 3, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 4, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 5, StartTime: "09:00", EndTime: "17:00", IsOpen: true}, {Weekday: 6, StartTime: "00:00", EndTime: "00:00", IsOpen: false}, } w := makeAuthRequest(handler, "PUT", "/api/scheduling/default-hours", adminToken, newHours) if w.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String()) } // Verify the update persisted var hours []DefaultHours rows, err := db.DB.Query(context.Background(), `SELECT weekday, start_time::text, end_time::text, is_open FROM working_hours ORDER BY weekday`) if err != nil { t.Fatalf("failed to query hours: %v", err) } defer rows.Close() for rows.Next() { var h DefaultHours if err := rows.Scan(&h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err != nil { t.Fatalf("failed to scan hours: %v", err) } hours = append(hours, h) } if hours[0].StartTime != "08:00:00" { t.Errorf("expected Monday start time 08:00:00, got %s", hours[0].StartTime) } } func TestScheduling_UpdateDefaultHours_NonAdmin(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() userToken := jwt.GenerateUserToken("user-123") newHours := []DefaultHours{ {Weekday: 0, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 1, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 2, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 3, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 4, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 5, StartTime: "09:00", EndTime: "17:00", IsOpen: true}, {Weekday: 6, StartTime: "00:00", EndTime: "00:00", IsOpen: false}, } // Wrap handler with RequireAdmin middleware w := makeAuthRequest(mw.RequireAdmin(http.HandlerFunc(UpdateDefaultHours)), "PUT", "/api/scheduling/default-hours", userToken, newHours) if w.Code != http.StatusForbidden { t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String()) } } // --- Tests for ListExceptionalGroups --- func TestScheduling_ListExceptionalGroups(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() // Create an exceptional group _, err := db.DB.Exec(context.Background(), ` INSERT INTO exceptional_working_hours_groups (name, description) VALUES ('Holiday Hours', 'Christmas holiday schedule') `) if err != nil { t.Fatalf("failed to create group: %v", err) } handler := http.HandlerFunc(ListExceptionalGroups) w := makeRequest(handler, "GET", "/api/scheduling/exceptional-groups", nil) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String()) } var response []ExceptionalGroup if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if len(response) == 0 { t.Error("expected at least one group in response") } if response[0].Name != "Holiday Hours" { t.Errorf("expected group name 'Holiday Hours', got %s", response[0].Name) } } // --- Tests for CreateExceptionalGroup --- func TestScheduling_CreateExceptionalGroup_Admin(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() adminToken := jwt.GenerateAdminToken() handler := http.HandlerFunc(CreateExceptionalGroup) newGroup := ExceptionalGroup{ Name: "Summer Hours", Description: "Extended summer schedule", Hours: []ExceptionalHours{ {Weekday: 0, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 1, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 2, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 3, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 4, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 5, StartTime: "09:00", EndTime: "17:00", IsOpen: true}, {Weekday: 6, StartTime: "00:00", EndTime: "00:00", IsOpen: false}, }, WeekStarts: []string{"2026-06-01"}, } w := makeAuthRequest(handler, "POST", "/api/scheduling/exceptional-groups", adminToken, newGroup) if w.Code != http.StatusCreated { t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String()) } var response ExceptionalGroup if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if response.Name != "Summer Hours" { t.Errorf("expected group name 'Summer Hours', got %s", response.Name) } if len(response.Hours) != 7 { t.Errorf("expected 7 hours, got %d", len(response.Hours)) } } func TestScheduling_CreateExceptionalGroup_NonAdmin(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() userToken := jwt.GenerateUserToken("user-123") newGroup := ExceptionalGroup{ Name: "Summer Hours", Description: "Extended summer schedule", Hours: []ExceptionalHours{ {Weekday: 0, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 1, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 2, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 3, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 4, StartTime: "08:00", EndTime: "18:00", IsOpen: true}, {Weekday: 5, StartTime: "09:00", EndTime: "17:00", IsOpen: true}, {Weekday: 6, StartTime: "00:00", EndTime: "00:00", IsOpen: false}, }, WeekStarts: []string{"2026-06-01"}, } w := makeAuthRequest(mw.RequireAdmin(http.HandlerFunc(CreateExceptionalGroup)), "POST", "/api/scheduling/exceptional-groups", userToken, newGroup) if w.Code != http.StatusForbidden { t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String()) } } // --- Tests for DeleteExceptionalGroup --- func TestScheduling_DeleteExceptionalGroup_Admin(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() adminToken := jwt.GenerateAdminToken() // Create a group to delete var groupID int err := db.DB.QueryRow(context.Background(), ` INSERT INTO exceptional_working_hours_groups (name, description) VALUES ('To Delete', 'Will be deleted') RETURNING id `).Scan(&groupID) if err != nil { t.Fatalf("failed to create group: %v", err) } handler := http.HandlerFunc(DeleteExceptionalGroup) req := httptest.NewRequest("DELETE", "/api/scheduling/exceptional-groups?id="+string(rune(groupID+'0')), nil) req.Header.Set("Authorization", "Bearer "+adminToken) w := httptest.NewRecorder() handler.ServeHTTP(w, req) // The handler expects id as query param but as a proper int // Let's use proper URL query req = httptest.NewRequest("DELETE", "/api/scheduling/exceptional-groups?id="+strconv.Itoa(groupID), nil) req.Header.Set("Authorization", "Bearer "+adminToken) w = httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String()) } // Verify group was deleted var count int err = db.DB.QueryRow(context.Background(), `SELECT COUNT(*) FROM exceptional_working_hours_groups WHERE id = $1`, groupID).Scan(&count) if err != nil { t.Fatalf("failed to check group: %v", err) } if count != 0 { t.Error("expected group to be deleted") } } func TestScheduling_DeleteExceptionalGroup_NonAdmin(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() userToken := jwt.GenerateUserToken("user-123") handler := mw.RequireAdmin(http.HandlerFunc(DeleteExceptionalGroup)) req := httptest.NewRequest("DELETE", "/api/scheduling/exceptional-groups?id=1", nil) req.Header.Set("Authorization", "Bearer "+userToken) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String()) } } // --- Tests for GetWorkingHours --- func TestScheduling_GetWorkingHours(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() handler := http.HandlerFunc(GetWorkingHours) req := httptest.NewRequest("GET", "/api/scheduling/working-hours?start=2026-02-16&end=2026-02-22", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String()) } var response []DayWorkingHours if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if len(response) == 0 { t.Error("expected working hours in response") } // Verify source is "default" for seeded hours for _, day := range response { if day.Source != "default" { t.Errorf("expected source 'default', got %s", day.Source) } break } } // --- Tests for GetAvailableHours --- func TestScheduling_GetAvailableHours(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() handler := http.HandlerFunc(GetAvailableHours) req := httptest.NewRequest("GET", "/api/scheduling/available-hours?start=2026-02-16&end=2026-02-22", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String()) } var response []DayAvailableHours if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if len(response) == 0 { t.Error("expected available hours in response") } // Verify we have slots for open days for _, day := range response { if day.IsOpen { if len(day.Slots) == 0 { t.Error("expected slots for open days") } break } } } // --- Tests for UpdateExceptionalApplications --- func TestScheduling_UpdateExceptionalApplications_Admin(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() adminToken := jwt.GenerateAdminToken() // Create a group var groupID int err := db.DB.QueryRow(context.Background(), ` INSERT INTO exceptional_working_hours_groups (name, description) VALUES ('Test Group', 'Test') RETURNING id `).Scan(&groupID) if err != nil { t.Fatalf("failed to create group: %v", err) } handler := http.HandlerFunc(UpdateExceptionalApplications) reqBody := map[string]interface{}{ "groupId": groupID, "weekStarts": []string{"2026-03-02", "2026-03-09"}, } w := makeAuthRequest(handler, "PUT", "/api/scheduling/exceptional-applications", adminToken, reqBody) if w.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String()) } // Verify applications were created var count int err = db.DB.QueryRow(context.Background(), ` SELECT COUNT(*) FROM exceptional_group_applications WHERE group_id = $1 `, groupID).Scan(&count) if err != nil { t.Fatalf("failed to check applications: %v", err) } if count != 2 { t.Errorf("expected 2 applications, got %d", count) } } func TestScheduling_UpdateExceptionalApplications_NonAdmin(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() userToken := jwt.GenerateUserToken("user-123") handler := mw.RequireAdmin(http.HandlerFunc(UpdateExceptionalApplications)) reqBody := map[string]interface{}{ "groupId": 1, "weekStarts": []string{"2026-03-02"}, } w := makeAuthRequest(handler, "PUT", "/api/scheduling/exceptional-applications", userToken, reqBody) if w.Code != http.StatusForbidden { t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String()) } }