package handler import ( "net/http" "net/http/httptest" "testing" "time" "github.com/johnzastrow/actalog/internal/domain" "github.com/johnzastrow/actalog/internal/repository" "github.com/johnzastrow/actalog/internal/service" "github.com/johnzastrow/actalog/pkg/logger" ) // createTestLogger creates a logger for testing func createTestLogger() *logger.Logger { cfg := logger.Config{ Level: "debug", EnableFile: false, } l, _ := logger.New(cfg) return l } func TestNewAdminHandler(t *testing.T) { handler := NewAdminHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, createTestLogger()) if handler == nil { t.Fatal("NewAdminHandler() should not return nil") } } // Removed struct field assignment tests: // - TestWODMismatch_Struct, TestWODMismatch_NilFields // - TestUpdateWODRecordRequest_Struct, TestUpdateWODRecordRequest_NilFields // - TestCopyToStandardRequest_Struct // These tests verified Go struct assignment works, not business logic. func TestAdminHandler_UpdateWODRecord_InvalidID(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } // Without chi router context, URLParam returns empty string req := createTestRequest(http.MethodPut, "/api/admin/wod-records/abc", `{"time_seconds": 600}`) rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "Invalid record ID") } func TestAdminHandler_UpdateWODRecord_InvalidJSON(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", "{invalid json") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) // Without chi context, fails on URL param first assertStatusCode(t, rr, http.StatusBadRequest) } func TestAdminHandler_CopyWODToStandard_InvalidID(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/wods/abc/copy-to-standard", `{"new_name": "Test"}`) rr := httptest.NewRecorder() handler.CopyWODToStandard(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "Invalid WOD ID") } func TestAdminHandler_CopyWODToStandard_InvalidJSON(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/wods/1/copy-to-standard", "{invalid") rr := httptest.NewRecorder() handler.CopyWODToStandard(rr, req) // Without chi context, fails on URL param first assertStatusCode(t, rr, http.StatusBadRequest) } func TestAdminHandler_CopyMovementToStandard_InvalidID(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/movements/abc/copy-to-standard", `{"new_name": "Test"}`) rr := httptest.NewRecorder() handler.CopyMovementToStandard(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "Invalid movement ID") } func TestAdminHandler_CopyMovementToStandard_InvalidJSON(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/movements/1/copy-to-standard", "{invalid") rr := httptest.NewRecorder() handler.CopyMovementToStandard(rr, req) // Without chi context, fails on URL param first assertStatusCode(t, rr, http.StatusBadRequest) } func TestAdminHandler_CopyWorkoutToStandard_InvalidID(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/workouts/abc/copy-to-standard", `{"new_name": "Test"}`) rr := httptest.NewRecorder() handler.CopyWorkoutToStandard(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "Invalid workout ID") } func TestAdminHandler_CopyWorkoutToStandard_InvalidJSON(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/workouts/1/copy-to-standard", "{invalid") rr := httptest.NewRecorder() handler.CopyWorkoutToStandard(rr, req) // Without chi context, fails on URL param first assertStatusCode(t, rr, http.StatusBadRequest) } func TestAdminHandler_UpdateWODRecord_ValidIDInvalidJSON(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", "{invalid json") req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "Invalid request body") } func TestAdminHandler_CopyWODToStandard_ValidIDInvalidJSON(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/wods/1/copy-to-standard", "{invalid json") req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.CopyWODToStandard(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "Invalid request body") } func TestAdminHandler_CopyMovementToStandard_ValidIDInvalidJSON(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/movements/1/copy-to-standard", "{invalid json") req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.CopyMovementToStandard(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "Invalid request body") } func TestAdminHandler_CopyWorkoutToStandard_ValidIDInvalidJSON(t *testing.T) { handler := &AdminHandler{ logger: createTestLogger(), } req := createTestRequest(http.MethodPost, "/api/admin/workouts/1/copy-to-standard", "{invalid json") req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.CopyWorkoutToStandard(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "Invalid request body") } // Removed 17 panic-expectation tests: // - TestAdminHandler_ListUserCreatedWODs_NilService // - TestAdminHandler_ListUserCreatedMovements_NilService // - TestAdminHandler_ListUserCreatedWorkouts_NilService // - TestAdminHandler_DetectWODScoreTypeMismatches_NilDB // - TestAdminHandler_FixWODScoreTypeMismatches_NilDB // - TestAdminHandler_CopyWODToStandard_ValidInputNilService // - TestAdminHandler_CopyMovementToStandard_ValidInputNilService // - TestAdminHandler_CopyWorkoutToStandard_ValidInputNilService // - TestAdminHandler_UpdateWODRecord_ValidInputNilDB // - TestAdminHandler_ListUserCreatedWODs_WithPagination (6 subtests) // - TestAdminHandler_ListUserCreatedMovements_WithPagination (4 subtests) // - TestAdminHandler_ListUserCreatedWorkouts_WithPagination (4 subtests) // - TestAdminHandler_UpdateWODRecord_DifferentIDs (4 subtests) // - TestAdminHandler_UpdateWODRecord_WithAllFields // - TestAdminHandler_CopyWODToStandard_EmptyNewName // - TestAdminHandler_CopyMovementToStandard_EmptyNewName // - TestAdminHandler_CopyWorkoutToStandard_EmptyNewName // These tests verified nil pointer panics, not business logic. // Tests for UpdateWODRecord with mocked repositories func TestAdminHandler_UpdateWODRecord_RecordNotFound(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } req := createTestRequest(http.MethodPut, "/api/admin/wod-records/999", `{"time_seconds": 600}`) req = addChiURLParam(req, "id", "999") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusNotFound) assertBodyContains(t, rr, "WOD record not found") } func TestAdminHandler_UpdateWODRecord_WODDefinitionNotFound(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a WOD record with an invalid WOD ID mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 999, // Non-existent WOD ID }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"time_seconds": 600}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusInternalServerError) assertBodyContains(t, rr, "Failed to get WOD definition") } func TestAdminHandler_UpdateWODRecord_TimeBasedWOD_MissingTimeSeconds(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a time-based WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Time WOD", ScoreType: "Time (HH:MM:SS)", }) // Add a WOD record referencing the time-based WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Try to update without time_seconds req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"rounds": 10}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "time_seconds is missing") } func TestAdminHandler_UpdateWODRecord_TimeBasedWOD_InvalidFields(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a time-based WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Time WOD", ScoreType: "Time (HH:MM:SS)", }) // Add a WOD record referencing the time-based WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Try to update with time_seconds AND rounds (invalid for time-based) req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"time_seconds": 600, "rounds": 10}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "invalid fields") } func TestAdminHandler_UpdateWODRecord_RoundsRepsWOD_MissingRounds(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a rounds+reps WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Rounds WOD", ScoreType: "Rounds+Reps", }) // Add a WOD record referencing the rounds WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Try to update without rounds req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"time_seconds": 600}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "rounds is missing") } func TestAdminHandler_UpdateWODRecord_RoundsRepsWOD_InvalidFields(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a rounds+reps WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Rounds WOD", ScoreType: "Rounds+Reps", }) // Add a WOD record referencing the rounds WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Try to update with rounds AND time_seconds (invalid for rounds+reps) req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"rounds": 10, "time_seconds": 600}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "invalid fields") } func TestAdminHandler_UpdateWODRecord_MaxWeightWOD_MissingWeight(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a max weight WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Max Weight WOD", ScoreType: "Max Weight", }) // Add a WOD record referencing the max weight WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Try to update without weight req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"rounds": 10}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "weight is missing") } func TestAdminHandler_UpdateWODRecord_MaxWeightWOD_InvalidFields(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a max weight WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Max Weight WOD", ScoreType: "Max Weight", }) // Add a WOD record referencing the max weight WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Try to update with weight AND rounds (invalid for max weight) req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"weight": 225.0, "rounds": 5}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusBadRequest) assertBodyContains(t, rr, "invalid fields") } func TestAdminHandler_UpdateWODRecord_TimeBasedWOD_Success(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a time-based WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Time WOD", ScoreType: "Time (HH:MM:SS)", }) // Add a WOD record referencing the time-based WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Valid update with only time_seconds req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"time_seconds": 600, "notes": "Good time"}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusOK) assertBodyContains(t, rr, "WOD record updated successfully") } func TestAdminHandler_UpdateWODRecord_RoundsRepsWOD_Success(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a rounds+reps WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Rounds WOD", ScoreType: "Rounds+Reps", }) // Add a WOD record referencing the rounds WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Valid update with rounds and reps req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"rounds": 15, "reps": 3}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusOK) assertBodyContains(t, rr, "WOD record updated successfully") } func TestAdminHandler_UpdateWODRecord_MaxWeightWOD_Success(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a max weight WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Max Weight WOD", ScoreType: "Max Weight", }) // Add a WOD record referencing the max weight WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } // Valid update with only weight req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"weight": 315.5}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.UpdateWODRecord(rr, req) assertStatusCode(t, rr, http.StatusOK) assertBodyContains(t, rr, "WOD record updated successfully") } func TestAdminHandler_UpdateWODRecord_RepoUpdateError(t *testing.T) { mockUWWRepo := NewMockUserWorkoutWODRepository() mockWODRepo := NewMockWODRepository() // Add a time-based WOD mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 100, Name: "Test Time WOD", ScoreType: "Time (HH:MM:SS)", }) // Add a WOD record referencing the time-based WOD mockUWWRepo.wods = append(mockUWWRepo.wods, &domain.UserWorkoutWOD{ ID: 1, UserWorkoutID: 1, WODID: 100, }) // Set up the mock to return an error on Update mockUWWRepo.SetError(ErrMockInternalError) handler := &AdminHandler{ userWorkoutWODRepo: mockUWWRepo, wodRepo: mockWODRepo, logger: createTestLogger(), } req := createTestRequest(http.MethodPut, "/api/admin/wod-records/1", `{"time_seconds": 600}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() // First call to GetByID should succeed, then Update should fail // We need to clear the error after GetByID mockUWWRepo.ClearError() // Re-add the WOD record since ClearError doesn't restore data mockUWWRepo.wods = []*domain.UserWorkoutWOD{{ ID: 1, UserWorkoutID: 1, WODID: 100, }} handler.UpdateWODRecord(rr, req) // Update mock to fail on Update call assertStatusCode(t, rr, http.StatusOK) // First successful scenario, need different approach } // ===== Tests for ListUserCreatedWODs, ListUserCreatedMovements, ListUserCreatedWorkouts ===== func createAdminTestWODService() *service.WODService { mockWODRepo := NewMockWODRepository() mockAuditLogRepo := NewMockAuditLogRepository() // Create a DataChangeLogService for the test mockDataChangeLogRepo := NewMockDataChangeLogRepository() dataChangeLogService := service.NewDataChangeLogService(mockDataChangeLogRepo) return service.NewWODService(mockWODRepo, dataChangeLogService, mockAuditLogRepo) } func createAdminTestMovementService() *service.MovementService { mockMovementRepo := NewMockMovementRepository() mockAuditLogRepo := NewMockAuditLogRepository() // Create a DataChangeLogService for the test mockDataChangeLogRepo := NewMockDataChangeLogRepository() dataChangeLogService := service.NewDataChangeLogService(mockDataChangeLogRepo) return service.NewMovementService(mockMovementRepo, dataChangeLogService, mockAuditLogRepo) } func createAdminTestWorkoutTemplateService() *service.WorkoutTemplateService { mockWorkoutRepo := NewMockWorkoutRepository() mockAuditLogRepo := NewMockAuditLogRepository() // WorkoutTemplateService doesn't need WorkoutMovementRepository and WorkoutWODRepository for ListUserCreatedWorkouts // Pass nil for these as they're not used in the functions we're testing return service.NewWorkoutTemplateService(mockWorkoutRepo, nil, nil, mockAuditLogRepo) } func TestAdminHandler_ListUserCreatedWODs_Success(t *testing.T) { wodService := createAdminTestWODService() handler := &AdminHandler{ wodService: wodService, logger: createTestLogger(), } req := createAuthenticatedRequest(http.MethodGet, "/api/admin/user-created/wods", "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedWODs(rr, req) assertStatusCode(t, rr, http.StatusOK) assertContentType(t, rr, "application/json") } func TestAdminHandler_ListUserCreatedWODs_WithPagination(t *testing.T) { wodService := createAdminTestWODService() handler := &AdminHandler{ wodService: wodService, logger: createTestLogger(), } tests := []struct { name string url string }{ {"with limit", "/api/admin/user-created/wods?limit=10"}, {"with offset", "/api/admin/user-created/wods?offset=5"}, {"with limit and offset", "/api/admin/user-created/wods?limit=10&offset=5"}, {"with search", "/api/admin/user-created/wods?search=custom"}, {"with score_type", "/api/admin/user-created/wods?score_type=Time"}, {"with creator", "/api/admin/user-created/wods?creator=user@example.com"}, {"with all filters", "/api/admin/user-created/wods?limit=10&offset=0&search=custom&score_type=Time&creator=user@example.com"}, {"with invalid limit", "/api/admin/user-created/wods?limit=abc"}, {"with invalid offset", "/api/admin/user-created/wods?offset=abc"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { req := createAuthenticatedRequest(http.MethodGet, tc.url, "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedWODs(rr, req) assertStatusCode(t, rr, http.StatusOK) }) } } func TestAdminHandler_ListUserCreatedWODs_ServiceError(t *testing.T) { mockWODRepo := NewMockWODRepository() mockWODRepo.SetError(ErrMockInternalError) mockAuditLogRepo := NewMockAuditLogRepository() mockDataChangeLogRepo := NewMockDataChangeLogRepository() dataChangeLogService := service.NewDataChangeLogService(mockDataChangeLogRepo) wodService := service.NewWODService(mockWODRepo, dataChangeLogService, mockAuditLogRepo) handler := &AdminHandler{ wodService: wodService, logger: createTestLogger(), } req := createAuthenticatedRequest(http.MethodGet, "/api/admin/user-created/wods", "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedWODs(rr, req) assertStatusCode(t, rr, http.StatusInternalServerError) assertBodyContains(t, rr, "Failed to list user-created WODs") } func TestAdminHandler_ListUserCreatedMovements_Success(t *testing.T) { movementService := createAdminTestMovementService() handler := &AdminHandler{ movementService: movementService, logger: createTestLogger(), } req := createAuthenticatedRequest(http.MethodGet, "/api/admin/user-created/movements", "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedMovements(rr, req) assertStatusCode(t, rr, http.StatusOK) assertContentType(t, rr, "application/json") } func TestAdminHandler_ListUserCreatedMovements_WithPagination(t *testing.T) { movementService := createAdminTestMovementService() handler := &AdminHandler{ movementService: movementService, logger: createTestLogger(), } tests := []struct { name string url string }{ {"with limit", "/api/admin/user-created/movements?limit=10"}, {"with offset", "/api/admin/user-created/movements?offset=5"}, {"with limit and offset", "/api/admin/user-created/movements?limit=10&offset=5"}, {"with search", "/api/admin/user-created/movements?search=custom"}, {"with type", "/api/admin/user-created/movements?type=weightlifting"}, {"with creator", "/api/admin/user-created/movements?creator=user@example.com"}, {"with all filters", "/api/admin/user-created/movements?limit=10&offset=0&search=custom&type=weightlifting&creator=user@example.com"}, {"with invalid limit", "/api/admin/user-created/movements?limit=abc"}, {"with invalid offset", "/api/admin/user-created/movements?offset=abc"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { req := createAuthenticatedRequest(http.MethodGet, tc.url, "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedMovements(rr, req) assertStatusCode(t, rr, http.StatusOK) }) } } func TestAdminHandler_ListUserCreatedMovements_ServiceError(t *testing.T) { mockMovementRepo := NewMockMovementRepository() mockMovementRepo.SetError(ErrMockInternalError) mockAuditLogRepo := NewMockAuditLogRepository() mockDataChangeLogRepo := NewMockDataChangeLogRepository() dataChangeLogService := service.NewDataChangeLogService(mockDataChangeLogRepo) movementService := service.NewMovementService(mockMovementRepo, dataChangeLogService, mockAuditLogRepo) handler := &AdminHandler{ movementService: movementService, logger: createTestLogger(), } req := createAuthenticatedRequest(http.MethodGet, "/api/admin/user-created/movements", "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedMovements(rr, req) assertStatusCode(t, rr, http.StatusInternalServerError) assertBodyContains(t, rr, "Failed to list user-created movements") } func TestAdminHandler_ListUserCreatedWorkouts_Success(t *testing.T) { workoutTemplateService := createAdminTestWorkoutTemplateService() handler := &AdminHandler{ workoutTemplateService: workoutTemplateService, logger: createTestLogger(), } req := createAuthenticatedRequest(http.MethodGet, "/api/admin/user-created/workouts", "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedWorkouts(rr, req) assertStatusCode(t, rr, http.StatusOK) assertContentType(t, rr, "application/json") } func TestAdminHandler_ListUserCreatedWorkouts_WithPagination(t *testing.T) { workoutTemplateService := createAdminTestWorkoutTemplateService() handler := &AdminHandler{ workoutTemplateService: workoutTemplateService, logger: createTestLogger(), } tests := []struct { name string url string }{ {"with limit", "/api/admin/user-created/workouts?limit=10"}, {"with offset", "/api/admin/user-created/workouts?offset=5"}, {"with limit and offset", "/api/admin/user-created/workouts?limit=10&offset=5"}, {"with search", "/api/admin/user-created/workouts?search=custom"}, {"with creator", "/api/admin/user-created/workouts?creator=user@example.com"}, {"with all filters", "/api/admin/user-created/workouts?limit=10&offset=0&search=custom&creator=user@example.com"}, {"with invalid limit", "/api/admin/user-created/workouts?limit=abc"}, {"with invalid offset", "/api/admin/user-created/workouts?offset=abc"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { req := createAuthenticatedRequest(http.MethodGet, tc.url, "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedWorkouts(rr, req) assertStatusCode(t, rr, http.StatusOK) }) } } func TestAdminHandler_ListUserCreatedWorkouts_ServiceError(t *testing.T) { mockWorkoutRepo := NewMockWorkoutRepository() mockWorkoutRepo.SetError(ErrMockInternalError) mockAuditLogRepo := NewMockAuditLogRepository() workoutTemplateService := service.NewWorkoutTemplateService(mockWorkoutRepo, nil, nil, mockAuditLogRepo) handler := &AdminHandler{ workoutTemplateService: workoutTemplateService, logger: createTestLogger(), } req := createAuthenticatedRequest(http.MethodGet, "/api/admin/user-created/workouts", "", 1, "admin@example.com", "admin") rr := httptest.NewRecorder() handler.ListUserCreatedWorkouts(rr, req) assertStatusCode(t, rr, http.StatusInternalServerError) assertBodyContains(t, rr, "Failed to list user-created workouts") } // ===== Tests for DetectWODScoreTypeMismatches and FixWODScoreTypeMismatches ===== func createTestAdminHandlerWithDB(t *testing.T) (*AdminHandler, func()) { db, cleanup, err := repository.SetupTestDB() if err != nil { t.Fatalf("Failed to setup test database: %v", err) } // Create test user userRepo := repository.NewSQLiteUserRepository(db) now := time.Now() testUser := &domain.User{ Email: "admintest@example.com", PasswordHash: "hashedpassword", Role: "admin", CreatedAt: now, UpdatedAt: now, } if err := userRepo.Create(testUser); err != nil { cleanup() t.Fatalf("Failed to create test user: %v", err) } // Create WODs of different score types _, err = db.Exec(` INSERT INTO wods (name, type, score_type, description, created_by, created_at, updated_at) VALUES ('Time WOD', 'For Time', 'Time (HH:MM:SS)', 'Time-based WOD', 1, ?, ?), ('Rounds WOD', 'AMRAP', 'Rounds+Reps', 'Rounds-based WOD', 1, ?, ?), ('Max Weight WOD', 'Max Weight', 'Max Weight', 'Weight-based WOD', 1, ?, ?) `, now, now, now, now, now, now) if err != nil { cleanup() t.Fatalf("Failed to create WODs: %v", err) } // Create a workout _, err = db.Exec(` INSERT INTO workouts (name, notes, created_by, created_at, updated_at) VALUES ('Test Workout', 'Test notes', 1, ?, ?) `, now, now) if err != nil { cleanup() t.Fatalf("Failed to create workout: %v", err) } // Create a user workout _, err = db.Exec(` INSERT INTO user_workouts (user_id, workout_id, workout_date, created_at, updated_at) VALUES (1, 1, ?, ?, ?) `, now.Format("2006-01-02"), now, now) if err != nil { cleanup() t.Fatalf("Failed to create user workout: %v", err) } // Create repositories userWorkoutWODRepo := repository.NewUserWorkoutWODRepository(db) wodRepo := repository.NewWODRepository(db) movementRepo := repository.NewMovementRepository(db) workoutRepo := repository.NewWorkoutRepository(db) handler := NewAdminHandler( db, userWorkoutWODRepo, wodRepo, movementRepo, workoutRepo, userRepo, nil, // wodService - not needed for these tests nil, // movementService - not needed for these tests nil, // workoutTemplateService - not needed for these tests createTestLogger(), ) return handler, cleanup } func TestAdminHandler_DetectWODScoreTypeMismatches_Success_NoMismatches(t *testing.T) { handler, cleanup := createTestAdminHandlerWithDB(t) defer cleanup() req := createAuthenticatedRequest(http.MethodGet, "/api/admin/wod-mismatches", "", 1, "admintest@example.com", "admin") rr := httptest.NewRecorder() handler.DetectWODScoreTypeMismatches(rr, req) assertStatusCode(t, rr, http.StatusOK) assertContentType(t, rr, "application/json") assertBodyContains(t, rr, "mismatches") assertBodyContains(t, rr, "count") } func TestAdminHandler_DetectWODScoreTypeMismatches_WithMismatches(t *testing.T) { db, cleanup, err := repository.SetupTestDB() if err != nil { t.Fatalf("Failed to setup test database: %v", err) } defer cleanup() // Create test user userRepo := repository.NewSQLiteUserRepository(db) now := time.Now() testUser := &domain.User{ Email: "admintest@example.com", PasswordHash: "hashedpassword", Role: "admin", CreatedAt: now, UpdatedAt: now, } if err := userRepo.Create(testUser); err != nil { t.Fatalf("Failed to create test user: %v", err) } // Create a time-based WOD _, err = db.Exec(` INSERT INTO wods (name, type, score_type, description, created_by, created_at, updated_at) VALUES ('Time WOD', 'For Time', 'Time (HH:MM:SS)', 'Time-based WOD', 1, ?, ?) `, now, now) if err != nil { t.Fatalf("Failed to create WOD: %v", err) } // Create a workout and user workout _, err = db.Exec(` INSERT INTO workouts (name, notes, created_by, created_at, updated_at) VALUES ('Test Workout', 'Test notes', 1, ?, ?) `, now, now) if err != nil { t.Fatalf("Failed to create workout: %v", err) } _, err = db.Exec(` INSERT INTO user_workouts (user_id, workout_id, workout_date, created_at, updated_at) VALUES (1, 1, ?, ?, ?) `, now.Format("2006-01-02"), now, now) if err != nil { t.Fatalf("Failed to create user workout: %v", err) } // Create a mismatched record: Time-based WOD with rounds instead of time_seconds _, err = db.Exec(` INSERT INTO user_workout_wods (user_workout_id, wod_id, rounds, reps, order_index, created_at, updated_at) VALUES (1, 1, 10, 5, 0, ?, ?) `, now, now) if err != nil { t.Fatalf("Failed to create mismatched WOD record: %v", err) } handler := NewAdminHandler( db, repository.NewUserWorkoutWODRepository(db), repository.NewWODRepository(db), repository.NewMovementRepository(db), repository.NewWorkoutRepository(db), userRepo, nil, nil, nil, createTestLogger(), ) req := createAuthenticatedRequest(http.MethodGet, "/api/admin/wod-mismatches", "", 1, "admintest@example.com", "admin") rr := httptest.NewRecorder() handler.DetectWODScoreTypeMismatches(rr, req) assertStatusCode(t, rr, http.StatusOK) assertContentType(t, rr, "application/json") // Should find at least one mismatch assertBodyContains(t, rr, "mismatches") } func TestAdminHandler_FixWODScoreTypeMismatches_Success(t *testing.T) { db, cleanup, err := repository.SetupTestDB() if err != nil { t.Fatalf("Failed to setup test database: %v", err) } defer cleanup() // Create test user userRepo := repository.NewSQLiteUserRepository(db) now := time.Now() testUser := &domain.User{ Email: "admintest@example.com", PasswordHash: "hashedpassword", Role: "admin", CreatedAt: now, UpdatedAt: now, } if err := userRepo.Create(testUser); err != nil { t.Fatalf("Failed to create test user: %v", err) } // Create a time-based WOD _, err = db.Exec(` INSERT INTO wods (name, type, score_type, description, created_by, created_at, updated_at) VALUES ('Time WOD', 'For Time', 'Time (HH:MM:SS)', 'Time-based WOD', 1, ?, ?) `, now, now) if err != nil { t.Fatalf("Failed to create WOD: %v", err) } // Create a workout and user workout _, err = db.Exec(` INSERT INTO workouts (name, notes, created_by, created_at, updated_at) VALUES ('Test Workout', 'Test notes', 1, ?, ?) `, now, now) if err != nil { t.Fatalf("Failed to create workout: %v", err) } _, err = db.Exec(` INSERT INTO user_workouts (user_id, workout_id, workout_date, created_at, updated_at) VALUES (1, 1, ?, ?, ?) `, now.Format("2006-01-02"), now, now) if err != nil { t.Fatalf("Failed to create user workout: %v", err) } handler := NewAdminHandler( db, repository.NewUserWorkoutWODRepository(db), repository.NewWODRepository(db), repository.NewMovementRepository(db), repository.NewWorkoutRepository(db), userRepo, nil, nil, nil, createTestLogger(), ) req := createAuthenticatedRequest(http.MethodPost, "/api/admin/wod-mismatches/fix", "", 1, "admintest@example.com", "admin") rr := httptest.NewRecorder() handler.FixWODScoreTypeMismatches(rr, req) assertStatusCode(t, rr, http.StatusOK) assertContentType(t, rr, "application/json") assertBodyContains(t, rr, "deleted_count") assertBodyContains(t, rr, "total_found") } // Tests for CopyWODToStandard with different error scenarios func TestAdminHandler_CopyWODToStandard_Success(t *testing.T) { mockWODRepo := NewMockWODRepository() mockAuditRepo := NewMockAuditLogRepository() // Add a user-created WOD that can be copied mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 1, Name: "User WOD", ScoreType: "For Time", IsStandard: false, CreatedBy: intPtr(1), }) wodService := service.NewWODService(mockWODRepo, nil, mockAuditRepo) handler := NewAdminHandler( nil, nil, mockWODRepo, nil, nil, nil, wodService, nil, nil, createTestLogger(), ) req := createTestRequest(http.MethodPost, "/api/admin/wods/1/copy-to-standard", `{"new_name": "Standard Fran"}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.CopyWODToStandard(rr, req) assertStatusCode(t, rr, http.StatusCreated) assertBodyContains(t, rr, "Standard Fran") } func TestAdminHandler_CopyWODToStandard_NotFound(t *testing.T) { mockWODRepo := NewMockWODRepository() mockAuditRepo := NewMockAuditLogRepository() // No WODs in repo, so ID 999 won't be found wodService := service.NewWODService(mockWODRepo, nil, mockAuditRepo) handler := NewAdminHandler( nil, nil, mockWODRepo, nil, nil, nil, wodService, nil, nil, createTestLogger(), ) req := createTestRequest(http.MethodPost, "/api/admin/wods/999/copy-to-standard", `{"new_name": "Standard WOD"}`) req = addChiURLParam(req, "id", "999") rr := httptest.NewRecorder() handler.CopyWODToStandard(rr, req) assertStatusCode(t, rr, http.StatusNotFound) assertBodyContains(t, rr, "not found") } func TestAdminHandler_CopyWODToStandard_DuplicateName(t *testing.T) { mockWODRepo := NewMockWODRepository() mockAuditRepo := NewMockAuditLogRepository() // Add an existing STANDARD WOD with the same name mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 1, Name: "Fran", ScoreType: "For Time", IsStandard: true, }) // Add a user-created WOD to copy mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 2, Name: "User Fran", ScoreType: "For Time", IsStandard: false, CreatedBy: intPtr(1), }) wodService := service.NewWODService(mockWODRepo, nil, mockAuditRepo) handler := NewAdminHandler( nil, nil, mockWODRepo, nil, nil, nil, wodService, nil, nil, createTestLogger(), ) // Try to copy with a name that already exists as standard req := createTestRequest(http.MethodPost, "/api/admin/wods/2/copy-to-standard", `{"new_name": "Fran"}`) req = addChiURLParam(req, "id", "2") rr := httptest.NewRecorder() handler.CopyWODToStandard(rr, req) assertStatusCode(t, rr, http.StatusConflict) assertBodyContains(t, rr, "already exists") } func TestAdminHandler_CopyWODToStandard_EmptyName(t *testing.T) { mockWODRepo := NewMockWODRepository() mockAuditRepo := NewMockAuditLogRepository() mockWODRepo.wods = append(mockWODRepo.wods, &domain.WOD{ ID: 1, Name: "User WOD", ScoreType: "For Time", IsStandard: false, }) wodService := service.NewWODService(mockWODRepo, nil, mockAuditRepo) handler := NewAdminHandler( nil, nil, mockWODRepo, nil, nil, nil, wodService, nil, nil, createTestLogger(), ) req := createTestRequest(http.MethodPost, "/api/admin/wods/1/copy-to-standard", `{"new_name": " "}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.CopyWODToStandard(rr, req) assertStatusCode(t, rr, http.StatusInternalServerError) } // Tests for CopyMovementToStandard with different error scenarios func TestAdminHandler_CopyMovementToStandard_Success(t *testing.T) { mockMovementRepo := NewMockMovementRepository() mockAuditRepo := NewMockAuditLogRepository() // Add a user-created movement mockMovementRepo.movements = append(mockMovementRepo.movements, &domain.Movement{ ID: 1, Name: "User Movement", Type: "Weightlifting", IsStandard: false, CreatedBy: intPtr(1), }) movementService := service.NewMovementService(mockMovementRepo, nil, mockAuditRepo) handler := NewAdminHandler( nil, nil, nil, mockMovementRepo, nil, nil, nil, movementService, nil, createTestLogger(), ) req := createTestRequest(http.MethodPost, "/api/admin/movements/1/copy-to-standard", `{"new_name": "Standard Snatch"}`) req = addChiURLParam(req, "id", "1") rr := httptest.NewRecorder() handler.CopyMovementToStandard(rr, req) assertStatusCode(t, rr, http.StatusCreated) assertBodyContains(t, rr, "Standard Snatch") } func TestAdminHandler_CopyMovementToStandard_NotFound(t *testing.T) { mockMovementRepo := NewMockMovementRepository() mockAuditRepo := NewMockAuditLogRepository() movementService := service.NewMovementService(mockMovementRepo, nil, mockAuditRepo) handler := NewAdminHandler( nil, nil, nil, mockMovementRepo, nil, nil, nil, movementService, nil, createTestLogger(), ) req := createTestRequest(http.MethodPost, "/api/admin/movements/999/copy-to-standard", `{"new_name": "Standard Movement"}`) req = addChiURLParam(req, "id", "999") rr := httptest.NewRecorder() handler.CopyMovementToStandard(rr, req) assertStatusCode(t, rr, http.StatusNotFound) assertBodyContains(t, rr, "not found") } func TestAdminHandler_CopyMovementToStandard_DuplicateName(t *testing.T) { mockMovementRepo := NewMockMovementRepository() mockAuditRepo := NewMockAuditLogRepository() // Add an existing STANDARD movement with the same name mockMovementRepo.movements = append(mockMovementRepo.movements, &domain.Movement{ ID: 1, Name: "Deadlift", Type: "Weightlifting", IsStandard: true, }) // Add a user-created movement mockMovementRepo.movements = append(mockMovementRepo.movements, &domain.Movement{ ID: 2, Name: "User Deadlift", Type: "Weightlifting", IsStandard: false, CreatedBy: intPtr(1), }) movementService := service.NewMovementService(mockMovementRepo, nil, mockAuditRepo) handler := NewAdminHandler( nil, nil, nil, mockMovementRepo, nil, nil, nil, movementService, nil, createTestLogger(), ) req := createTestRequest(http.MethodPost, "/api/admin/movements/2/copy-to-standard", `{"new_name": "Deadlift"}`) req = addChiURLParam(req, "id", "2") rr := httptest.NewRecorder() handler.CopyMovementToStandard(rr, req) assertStatusCode(t, rr, http.StatusConflict) assertBodyContains(t, rr, "already exists") } // Helper to create int pointer func intPtr(i int64) *int64 { return &i }