internal/handler/wod_handler_test.go
Rama main · réplica de lectura del repositorio original.
package handler
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/johnzastrow/actalog/internal/service"
)
func TestNewWODHandler(t *testing.T) {
// Test constructor with nil service
handler := NewWODHandler(nil)
if handler == nil {
t.Error("NewWODHandler() should not return nil")
}
if handler.wodService != nil {
t.Error("wodService should be nil when passed nil")
}
}
func TestCreateWODRequest_Struct(t *testing.T) {
url := "https://example.com/wod"
notes := "Test notes"
req := CreateWODRequest{
Name: "Fran",
Source: "CrossFit",
Type: "Benchmark",
Regime: "For Time",
ScoreType: "Time",
Description: "21-15-9 Thrusters and Pull-ups",
URL: &url,
Notes: ¬es,
}
if req.Name != "Fran" {
t.Errorf("Name = %q, want %q", req.Name, "Fran")
}
if req.Source != "CrossFit" {
t.Errorf("Source = %q, want %q", req.Source, "CrossFit")
}
if req.Type != "Benchmark" {
t.Errorf("Type = %q, want %q", req.Type, "Benchmark")
}
if req.Regime != "For Time" {
t.Errorf("Regime = %q, want %q", req.Regime, "For Time")
}
if req.ScoreType != "Time" {
t.Errorf("ScoreType = %q, want %q", req.ScoreType, "Time")
}
if req.URL == nil || *req.URL != url {
t.Errorf("URL = %v, want %q", req.URL, url)
}
if req.Notes == nil || *req.Notes != notes {
t.Errorf("Notes = %v, want %q", req.Notes, notes)
}
}
func TestCreateWODRequest_NilOptionalFields(t *testing.T) {
req := CreateWODRequest{
Name: "Simple WOD",
}
if req.URL != nil {
t.Error("URL should be nil")
}
if req.Notes != nil {
t.Error("Notes should be nil")
}
if req.Source != "" {
t.Errorf("Source = %q, want empty", req.Source)
}
}
func TestUpdateWODRequest_Struct(t *testing.T) {
url := "https://updated.com/wod"
notes := "Updated notes"
req := UpdateWODRequest{
Name: "Updated Fran",
Source: "Custom",
Type: "AMRAP",
Regime: "20 min",
ScoreType: "Rounds+Reps",
Description: "Updated description",
URL: &url,
Notes: ¬es,
}
if req.Name != "Updated Fran" {
t.Errorf("Name = %q, want %q", req.Name, "Updated Fran")
}
if req.Type != "AMRAP" {
t.Errorf("Type = %q, want %q", req.Type, "AMRAP")
}
if req.ScoreType != "Rounds+Reps" {
t.Errorf("ScoreType = %q, want %q", req.ScoreType, "Rounds+Reps")
}
}
func TestWODResponse_Struct(t *testing.T) {
url := "https://example.com"
notes := "Some notes"
createdBy := int64(1)
resp := WODResponse{
ID: 123,
Name: "Test WOD",
Source: "CrossFit",
Type: "Benchmark",
Regime: "For Time",
ScoreType: "Time",
Description: "Test description",
URL: &url,
Notes: ¬es,
IsStandard: true,
CreatedBy: &createdBy,
CreatedAt: "2024-01-15T10:00:00Z",
UpdatedAt: "2024-01-15T10:00:00Z",
}
if resp.ID != 123 {
t.Errorf("ID = %d, want 123", resp.ID)
}
if resp.Name != "Test WOD" {
t.Errorf("Name = %q, want %q", resp.Name, "Test WOD")
}
if !resp.IsStandard {
t.Error("IsStandard should be true")
}
if resp.CreatedBy == nil || *resp.CreatedBy != 1 {
t.Errorf("CreatedBy = %v, want 1", resp.CreatedBy)
}
}
func TestWODResponse_NilFields(t *testing.T) {
resp := WODResponse{
ID: 1,
Name: "Custom WOD",
IsStandard: false,
}
if resp.URL != nil {
t.Error("URL should be nil")
}
if resp.Notes != nil {
t.Error("Notes should be nil")
}
if resp.CreatedBy != nil {
t.Error("CreatedBy should be nil")
}
}
func TestWODHandler_CreateWOD_Unauthorized(t *testing.T) {
handler := &WODHandler{}
// Request without user context
req := createTestRequest(http.MethodPost, "/api/wods", `{"name": "Test WOD"}`)
rr := httptest.NewRecorder()
handler.CreateWOD(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestWODHandler_CreateWOD_InvalidJSON(t *testing.T) {
handler := &WODHandler{}
req := createAuthenticatedRequest(http.MethodPost, "/api/wods", "{bad json", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.CreateWOD(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid request body")
}
func TestWODHandler_CreateWOD_MissingName(t *testing.T) {
handler := &WODHandler{}
tests := []struct {
name string
body string
wantError string
}{
{
name: "empty name",
body: `{"name": ""}`,
wantError: "WOD name is required",
},
{
name: "missing name field",
body: `{"source": "CrossFit"}`,
wantError: "WOD name is required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodPost, "/api/wods", tt.body, 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.CreateWOD(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, tt.wantError)
})
}
}
func TestWODHandler_GetWOD_InvalidID(t *testing.T) {
handler := &WODHandler{}
// Without chi router context, URLParam returns empty string
req := createTestRequest(http.MethodGet, "/api/wods/abc", "")
rr := httptest.NewRecorder()
handler.GetWOD(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid WOD ID")
}
// Tests for ListMyWODs
func TestWODHandler_ListMyWODs_Unauthorized(t *testing.T) {
handler := &WODHandler{}
// Request without user context
req := createTestRequest(http.MethodGet, "/api/wods/mine", "")
rr := httptest.NewRecorder()
handler.ListMyWODs(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Authentication required")
}
// Tests for SearchWODs
func TestWODHandler_SearchWODs_MissingQuery(t *testing.T) {
handler := &WODHandler{}
req := createTestRequest(http.MethodGet, "/api/wods/search", "")
rr := httptest.NewRecorder()
handler.SearchWODs(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Search query is required")
}
func TestWODHandler_SearchWODs_EmptyQuery(t *testing.T) {
handler := &WODHandler{}
req := createTestRequest(http.MethodGet, "/api/wods/search?q=", "")
rr := httptest.NewRecorder()
handler.SearchWODs(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Search query is required")
}
// Tests for UpdateWOD
func TestWODHandler_UpdateWOD_Unauthorized(t *testing.T) {
handler := &WODHandler{}
req := createTestRequest(http.MethodPut, "/api/wods/1", `{"name": "Updated WOD"}`)
rr := httptest.NewRecorder()
handler.UpdateWOD(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestWODHandler_UpdateWOD_InvalidID(t *testing.T) {
handler := &WODHandler{}
// Without chi context, URLParam returns empty string -> invalid ID
req := createAuthenticatedRequest(http.MethodPut, "/api/wods/abc", `{"name": "Updated"}`, 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.UpdateWOD(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid WOD ID")
}
func TestWODHandler_UpdateWOD_InvalidJSON(t *testing.T) {
handler := &WODHandler{}
req := createAuthenticatedRequest(http.MethodPut, "/api/wods/1", "{bad json", 1, "test@example.com", "user")
// Add chi URL param context
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.UpdateWOD(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid request body")
}
// Tests for DeleteWOD
func TestWODHandler_DeleteWOD_Unauthorized(t *testing.T) {
handler := &WODHandler{}
req := createTestRequest(http.MethodDelete, "/api/wods/1", "")
rr := httptest.NewRecorder()
handler.DeleteWOD(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestWODHandler_DeleteWOD_InvalidID(t *testing.T) {
handler := &WODHandler{}
// Without chi context, URLParam returns empty string -> invalid ID
req := createAuthenticatedRequest(http.MethodDelete, "/api/wods/abc", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.DeleteWOD(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid WOD ID")
}
// Removed 25 panic-expectation tests:
// - TestWODHandler_GetWOD_ValidIDNilService, TestWODHandler_GetWOD_DifferentIDs
// - TestWODHandler_CreateWOD_ValidInputNilService, TestWODHandler_CreateWOD_MinimalValidInput
// - TestWODHandler_CreateWOD_WithAllFields, TestWODHandler_CreateWOD_WithSource
// - TestWODHandler_UpdateWOD_ValidInputNilService, TestWODHandler_UpdateWOD_AsAdmin
// - TestWODHandler_UpdateWOD_MissingName, TestWODHandler_UpdateWOD_DifferentIDs
// - TestWODHandler_DeleteWOD_ValidIDNilService, TestWODHandler_DeleteWOD_AsAdmin
// - TestWODHandler_DeleteWOD_DifferentIDs
// - TestWODHandler_ListWODs_NilService, TestWODHandler_ListWODs_WithStandardOnlyParam
// - TestWODHandler_ListWODs_WithPaginationParams, TestWODHandler_ListWODs_WithUserAuth
// - TestWODHandler_ListStandardWODs_WithPagination, TestWODHandler_ListStandardWODs_DifferentLimits
// - TestWODHandler_ListMyWODs_WithPagination, TestWODHandler_ListMyWODs_DifferentUserIDs
// - TestWODHandler_SearchWODs_ValidQuery, TestWODHandler_SearchWODs_WithLimitParam
// - TestWODHandler_SearchWODs_VariousQueries, TestWODHandler_SearchWODs_WithAuth
// These tests verified nil pointer panics, not business logic.
// Helper function to create a test WOD service with mock repositories
func createTestWODService() *service.WODService {
mockWODRepo := NewMockWODRepository()
mockDataChangeLogRepo := NewMockDataChangeLogRepository()
mockAuditLogRepo := NewMockAuditLogRepository()
dataChangeLogService := service.NewDataChangeLogService(mockDataChangeLogRepo)
return service.NewWODService(mockWODRepo, dataChangeLogService, mockAuditLogRepo)
}
// =============================================
// Mock-based tests with real service for WOD handler
// =============================================
func TestWODHandler_ListWODs_Success(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
req := createTestRequest(http.MethodGet, "/api/wods", "")
rr := httptest.NewRecorder()
handler.ListWODs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "wods")
}
func TestWODHandler_ListWODs_WithPagination(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
req := createTestRequest(http.MethodGet, "/api/wods?limit=10&offset=0", "")
rr := httptest.NewRecorder()
handler.ListWODs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "wods")
}
func TestWODHandler_ListStandardWODs_Success(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
req := createTestRequest(http.MethodGet, "/api/wods/standard", "")
rr := httptest.NewRecorder()
handler.ListStandardWODs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "wods")
}
func TestWODHandler_ListMyWODs_Success(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
req := createAuthenticatedRequest(http.MethodGet, "/api/wods/mine", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.ListMyWODs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "wods")
}
func TestWODHandler_SearchWODs_Success(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
req := createTestRequest(http.MethodGet, "/api/wods/search?q=Fran", "")
rr := httptest.NewRecorder()
handler.SearchWODs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "wods")
}
func TestWODHandler_SearchWODs_EmptyQueryWithService(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
req := createTestRequest(http.MethodGet, "/api/wods/search", "")
rr := httptest.NewRecorder()
handler.SearchWODs(rr, req)
// Search query is required
assertStatusCode(t, rr, http.StatusBadRequest)
}
func TestWODHandler_GetWOD_Success(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
req := createTestRequest(http.MethodGet, "/api/wods/1", "")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.GetWOD(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "Fran")
}
func TestWODHandler_GetWOD_NotFound(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
req := createTestRequest(http.MethodGet, "/api/wods/999", "")
req = addChiURLParam(req, "id", "999")
rr := httptest.NewRecorder()
handler.GetWOD(rr, req)
// Service returns 500 for not found errors (wrapped error)
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "not found")
}
func TestWODHandler_CreateWOD_Success(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
body := `{"name": "New WOD", "description": "A new workout", "score_type": "Time (HH:MM:SS)", "source": "Self-recorded", "type": "Self-created"}`
req := createAuthenticatedRequest(http.MethodPost, "/api/wods", body, 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.CreateWOD(rr, req)
assertStatusCode(t, rr, http.StatusCreated)
assertBodyContains(t, rr, "New WOD")
}
func TestWODHandler_UpdateWOD_Success(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
// Use ID 4 which is the user-created WOD owned by user 1
body := `{"name": "Updated Custom WOD", "description": "Updated description", "score_type": "Time (HH:MM:SS)", "source": "Self-recorded", "type": "Self-created"}`
req := createAuthenticatedRequest(http.MethodPut, "/api/wods/4", body, 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "4")
rr := httptest.NewRecorder()
handler.UpdateWOD(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestWODHandler_DeleteWOD_Success(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
// Use ID 4 which is the user-created WOD owned by user 1
req := createAuthenticatedRequest(http.MethodDelete, "/api/wods/4", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "4")
rr := httptest.NewRecorder()
handler.DeleteWOD(rr, req)
// Should succeed since user owns the WOD
if rr.Code != http.StatusOK && rr.Code != http.StatusNoContent {
t.Errorf("Expected success, got %d: %s", rr.Code, rr.Body.String())
}
}
func TestWODHandler_DeleteWOD_NotOwner(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
// Try to delete WOD 4 as user 999 (not the owner)
req := createAuthenticatedRequest(http.MethodDelete, "/api/wods/4", "", 999, "other@example.com", "user")
req = addChiURLParam(req, "id", "4")
rr := httptest.NewRecorder()
handler.DeleteWOD(rr, req)
// Service returns error which handler converts to 500
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "unauthorized")
}
func TestWODHandler_DeleteWOD_StandardWOD(t *testing.T) {
wodService := createTestWODService()
handler := NewWODHandler(wodService)
// Try to delete a standard WOD (ID 1 is "Fran" - a standard WOD)
req := createAuthenticatedRequest(http.MethodDelete, "/api/wods/1", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.DeleteWOD(rr, req)
// Service returns error which handler converts to 500
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "unauthorized")
}