internal/handler/movement_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 TestMovementHandler_Search_MissingQuery(t *testing.T) {
handler := &MovementHandler{}
req := createTestRequest(http.MethodGet, "/api/movements/search", "")
rr := httptest.NewRecorder()
handler.Search(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Query parameter 'q' is required")
}
func TestMovementHandler_Create_Unauthorized(t *testing.T) {
handler := &MovementHandler{}
// Request without user context
req := createTestRequest(http.MethodPost, "/api/movements", `{"name": "Test", "type": "strength"}`)
rr := httptest.NewRecorder()
handler.Create(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestMovementHandler_Create_InvalidJSON(t *testing.T) {
handler := &MovementHandler{}
req := createAuthenticatedRequest(http.MethodPost, "/api/movements", "{bad json", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.Create(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid request body")
}
func TestMovementHandler_Create_MissingFields(t *testing.T) {
handler := &MovementHandler{}
tests := []struct {
name string
body string
wantError string
}{
{
name: "missing name",
body: `{"type": "strength"}`,
wantError: "Name and type are required",
},
{
name: "missing type",
body: `{"name": "Test Movement"}`,
wantError: "Name and type are required",
},
{
name: "empty name",
body: `{"name": "", "type": "strength"}`,
wantError: "Name and type are required",
},
{
name: "empty type",
body: `{"name": "Test", "type": ""}`,
wantError: "Name and type are required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodPost, "/api/movements", tt.body, 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.Create(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, tt.wantError)
})
}
}
func TestMovementHandler_Update_Unauthorized(t *testing.T) {
handler := &MovementHandler{}
// Request without user context
req := createTestRequest(http.MethodPut, "/api/movements/1", `{"name": "Test", "type": "strength"}`)
rr := httptest.NewRecorder()
handler.Update(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
// Note: Update tests that require chi.URLParam need router context
// Testing ID parsing requires chi router setup which is complex for unit tests
// These validation tests verify the input is checked before service calls
func TestMovementHandler_Update_InvalidID(t *testing.T) {
handler := &MovementHandler{}
// chi.URLParam returns empty string without router context, which fails int parsing
req := createAuthenticatedRequest(http.MethodPut, "/api/movements/abc", `{"name": "Test", "type": "strength"}`, 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.Update(rr, req)
// Without chi router context, URLParam returns empty string -> invalid ID
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid movement ID")
}
func TestMovementHandler_Delete_Unauthorized(t *testing.T) {
handler := &MovementHandler{}
// Request without user context
req := createTestRequest(http.MethodDelete, "/api/movements/1", "")
rr := httptest.NewRecorder()
handler.Delete(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestMovementHandler_Delete_InvalidID(t *testing.T) {
handler := &MovementHandler{}
// Without chi router context, URLParam returns empty string -> invalid ID
req := createAuthenticatedRequest(http.MethodDelete, "/api/movements/abc", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.Delete(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid movement ID")
}
// Tests for GetByID
func TestMovementHandler_GetByID_InvalidID(t *testing.T) {
handler := &MovementHandler{}
// Without chi router context, URLParam returns empty string -> invalid ID
req := createTestRequest(http.MethodGet, "/api/movements/abc", "")
rr := httptest.NewRecorder()
handler.GetByID(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid movement ID")
}
// Tests for Update with valid ID
func TestMovementHandler_Update_InvalidJSON(t *testing.T) {
handler := &MovementHandler{}
req := createAuthenticatedRequest(http.MethodPut, "/api/movements/1", "{bad json", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.Update(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid request body")
}
func TestMovementHandler_Update_MissingFields(t *testing.T) {
handler := &MovementHandler{}
tests := []struct {
name string
body string
wantError string
}{
{
name: "missing name",
body: `{"type": "strength"}`,
wantError: "Name and type are required",
},
{
name: "missing type",
body: `{"name": "Test Movement"}`,
wantError: "Name and type are required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodPut, "/api/movements/1", tt.body, 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.Update(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, tt.wantError)
})
}
}
// Test NewMovementHandler
func TestNewMovementHandler(t *testing.T) {
handler := NewMovementHandler(nil, nil, nil)
if handler == nil {
t.Error("NewMovementHandler should return a non-nil handler")
}
}
// Removed 17 panic-expectation tests:
// - TestMovementHandler_ListAll_NilRepo
// - TestMovementHandler_ListStandard_NilRepo
// - TestMovementHandler_GetByID_ValidIDNilRepo
// - TestMovementHandler_Search_WithLimit (4 subtests)
// - TestMovementHandler_Create_ValidInputNilService
// - TestMovementHandler_Update_ValidInputNilService
// - TestMovementHandler_Update_AsAdminNilService
// - TestMovementHandler_Delete_ValidIDNilService
// - TestMovementHandler_Search_ValidQueryWithLogger (4 subtests)
// - TestMovementHandler_ListAll_WithLogger
// - TestMovementHandler_ListStandard_WithLogger
// - TestMovementHandler_GetByID_WithLogger
// - TestMovementHandler_GetByID_DifferentIDs (4 subtests)
// - TestMovementHandler_Delete_DifferentIDs (3 subtests)
// - TestMovementHandler_Update_DifferentIDs (3 subtests)
// - TestMovementHandler_Search_DifferentQueries (5 subtests)
// - TestMovementHandler_Create_AllFields
// - TestMovementHandler_Create_DifferentTypes (5 subtests)
// - TestMovementHandler_Update_AllFields
// These tests verified nil pointer panics, not business logic.
// Tests using MockMovementRepository for full code path coverage
func TestMovementHandler_ListAll_Success(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements", "")
rr := httptest.NewRecorder()
handler.ListAll(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "movements")
assertBodyContains(t, rr, "Back Squat")
}
func TestMovementHandler_ListAll_Error(t *testing.T) {
mockRepo := NewMockMovementRepository()
mockRepo.SetError(ErrMockInternalError)
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements", "")
rr := httptest.NewRecorder()
handler.ListAll(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "Failed to retrieve movements")
}
func TestMovementHandler_ListStandard_Success(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements/standard", "")
rr := httptest.NewRecorder()
handler.ListStandard(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "movements")
}
func TestMovementHandler_ListStandard_Error(t *testing.T) {
mockRepo := NewMockMovementRepository()
mockRepo.SetError(ErrMockInternalError)
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements/standard", "")
rr := httptest.NewRecorder()
handler.ListStandard(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "Failed to retrieve movements")
}
func TestMovementHandler_Search_Success(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements/search?q=squat", "")
rr := httptest.NewRecorder()
handler.Search(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "movements")
}
func TestMovementHandler_Search_Error(t *testing.T) {
mockRepo := NewMockMovementRepository()
mockRepo.SetError(ErrMockInternalError)
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements/search?q=squat", "")
rr := httptest.NewRecorder()
handler.Search(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "Failed to search movements")
}
func TestMovementHandler_Search_WithValidLimit(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements/search?q=test&limit=5", "")
rr := httptest.NewRecorder()
handler.Search(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "movements")
}
func TestMovementHandler_GetByID_Success(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements/1", "")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.GetByID(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "Back Squat")
}
func TestMovementHandler_GetByID_NotFound(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements/999", "")
req = addChiURLParam(req, "id", "999")
rr := httptest.NewRecorder()
handler.GetByID(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
}
func TestMovementHandler_GetByID_Error(t *testing.T) {
mockRepo := NewMockMovementRepository()
mockRepo.SetError(ErrMockInternalError)
handler := &MovementHandler{
movementRepo: mockRepo,
logger: createTestLogger(),
}
req := createTestRequest(http.MethodGet, "/api/movements/1", "")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.GetByID(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "Failed to retrieve movement")
}
func TestMovementHandler_ListAll_NoLogger(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
}
req := createTestRequest(http.MethodGet, "/api/movements", "")
rr := httptest.NewRecorder()
handler.ListAll(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestMovementHandler_ListAll_ErrorNoLogger(t *testing.T) {
mockRepo := NewMockMovementRepository()
mockRepo.SetError(ErrMockInternalError)
handler := &MovementHandler{
movementRepo: mockRepo,
}
req := createTestRequest(http.MethodGet, "/api/movements", "")
rr := httptest.NewRecorder()
handler.ListAll(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
}
func TestMovementHandler_ListStandard_NoLogger(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
}
req := createTestRequest(http.MethodGet, "/api/movements/standard", "")
rr := httptest.NewRecorder()
handler.ListStandard(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestMovementHandler_Search_NoLogger(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
}
req := createTestRequest(http.MethodGet, "/api/movements/search?q=test", "")
rr := httptest.NewRecorder()
handler.Search(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestMovementHandler_GetByID_NoLogger(t *testing.T) {
mockRepo := NewMockMovementRepository()
handler := &MovementHandler{
movementRepo: mockRepo,
}
req := createTestRequest(http.MethodGet, "/api/movements/1", "")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.GetByID(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
// =============================================
// Service-based tests for Create, Update, Delete
// =============================================
// createTestMovementService creates a MovementService with mock repositories
func createTestMovementService() *service.MovementService {
mockMovementRepo := NewMockMovementRepository()
mockDataChangeLogRepo := NewMockDataChangeLogRepository()
mockAuditLogRepo := NewMockAuditLogRepository()
dataChangeLogService := service.NewDataChangeLogService(mockDataChangeLogRepo)
return service.NewMovementService(mockMovementRepo, dataChangeLogService, mockAuditLogRepo)
}
func TestMovementHandler_Create_Success(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
body := `{"name": "New Movement", "type": "weightlifting", "description": "A new test movement"}`
req := createAuthenticatedRequest(http.MethodPost, "/api/movements", body, 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.Create(rr, req)
assertStatusCode(t, rr, http.StatusCreated)
assertBodyContains(t, rr, "New Movement")
}
func TestMovementHandler_Create_WithAllFields(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
body := `{
"name": "Complex Movement",
"type": "gymnastics",
"description": "A complex gymnastics movement",
"equipment": "Rings",
"video_url": "https://example.com/video"
}`
req := createAuthenticatedRequest(http.MethodPost, "/api/movements", body, 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.Create(rr, req)
assertStatusCode(t, rr, http.StatusCreated)
}
func TestMovementHandler_Update_Success(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
// Update a user-created movement (ID 4 is created by user 1)
body := `{"name": "Updated Movement", "type": "strength", "description": "Updated description"}`
req := createAuthenticatedRequest(http.MethodPut, "/api/movements/4", body, 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "4")
rr := httptest.NewRecorder()
handler.Update(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestMovementHandler_Update_AsAdmin(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
// Admin can update standard movements
body := `{"name": "Admin Updated", "type": "weightlifting", "description": "Admin update"}`
req := createAuthenticatedRequest(http.MethodPut, "/api/movements/1", body, 1, "admin@example.com", "admin")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.Update(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestMovementHandler_Update_StandardMovementAsUser(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
// Non-admin trying to update standard movement (ID 1)
body := `{"name": "Unauthorized Update", "type": "strength"}`
req := createAuthenticatedRequest(http.MethodPut, "/api/movements/1", body, 1, "user@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.Update(rr, req)
// Should fail - can't update standard movements as non-admin
assertStatusCode(t, rr, http.StatusForbidden)
assertBodyContains(t, rr, "Cannot modify standard movement")
}
func TestMovementHandler_Delete_Success(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
// Delete user-created movement (ID 4)
req := createAuthenticatedRequest(http.MethodDelete, "/api/movements/4", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "4")
rr := httptest.NewRecorder()
handler.Delete(rr, req)
// Should succeed
if rr.Code != http.StatusOK && rr.Code != http.StatusNoContent {
t.Errorf("Expected success, got %d: %s", rr.Code, rr.Body.String())
}
}
func TestMovementHandler_Delete_AsAdmin(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
// Admin deleting a user-created movement (ID 4)
req := createAuthenticatedRequest(http.MethodDelete, "/api/movements/4", "", 1, "admin@example.com", "admin")
req = addChiURLParam(req, "id", "4")
rr := httptest.NewRecorder()
handler.Delete(rr, req)
// Should succeed
if rr.Code != http.StatusOK && rr.Code != http.StatusNoContent {
t.Errorf("Expected success, got %d: %s", rr.Code, rr.Body.String())
}
}
func TestMovementHandler_Delete_NotFound(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
// Try to delete non-existent movement (ID 999)
req := createAuthenticatedRequest(http.MethodDelete, "/api/movements/999", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "999")
rr := httptest.NewRecorder()
handler.Delete(rr, req)
// Should fail - movement not found
assertStatusCode(t, rr, http.StatusInternalServerError)
}
func TestMovementHandler_Delete_StandardMovement(t *testing.T) {
mockRepo := NewMockMovementRepository()
movementService := createTestMovementService()
handler := &MovementHandler{
movementRepo: mockRepo,
movementService: movementService,
logger: createTestLogger(),
}
// Non-admin trying to delete standard movement (ID 1)
req := createAuthenticatedRequest(http.MethodDelete, "/api/movements/1", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.Delete(rr, req)
// Should fail - can't delete standard movements as non-admin
assertStatusCode(t, rr, http.StatusForbidden)
assertBodyContains(t, rr, "Cannot delete standard movement")
}