internal/handler/admin_handler_test.go
Rama main · réplica de lectura del repositorio original.
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
}