package api import ( "bytes" "database/sql" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "training-tracker/internal/models" ) func TestExerciseHandler_List(t *testing.T) { t.Parallel() tests := []struct { name string mock *mockExerciseRepo wantStatus int wantLen int }{ { name: "empty list", mock: &mockExerciseRepo{exercises: nil}, wantStatus: http.StatusOK, wantLen: 0, }, { name: "multiple exercises", mock: &mockExerciseRepo{ exercises: []models.Exercise{ {ID: 1, Name: "Bench Press", Type: models.ExerciseTypeStrength}, {ID: 2, Name: "Running", Type: models.ExerciseTypeCardio}, }, }, wantStatus: http.StatusOK, wantLen: 2, }, { name: "repository error", mock: &mockExerciseRepo{err: errors.New("db error")}, wantStatus: http.StatusInternalServerError, wantLen: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() handler := NewExerciseHandler(tt.mock) req := httptest.NewRequest(http.MethodGet, "/api/exercises", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } if tt.wantStatus == http.StatusOK { var exercises []models.Exercise if err := json.NewDecoder(rec.Body).Decode(&exercises); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(exercises) != tt.wantLen { t.Errorf("len = %d, want %d", len(exercises), tt.wantLen) } } }) } } func TestExerciseHandler_GetByID(t *testing.T) { t.Parallel() now := time.Now() tests := []struct { name string url string mock *mockExerciseRepo wantStatus int wantName string }{ { name: "found", url: "/api/exercises/1", mock: &mockExerciseRepo{ exercise: &models.Exercise{ ID: 1, Name: "Squat", Type: models.ExerciseTypeStrength, CreatedAt: now, }, }, wantStatus: http.StatusOK, wantName: "Squat", }, { name: "not found", url: "/api/exercises/999", mock: &mockExerciseRepo{exercise: nil}, wantStatus: http.StatusNotFound, }, { name: "repository error", url: "/api/exercises/1", mock: &mockExerciseRepo{err: errors.New("db error")}, wantStatus: http.StatusInternalServerError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() handler := NewExerciseHandler(tt.mock) req := httptest.NewRequest(http.MethodGet, tt.url, nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } if tt.wantStatus == http.StatusOK { var exercise models.Exercise if err := json.NewDecoder(rec.Body).Decode(&exercise); err != nil { t.Fatalf("failed to decode response: %v", err) } if exercise.Name != tt.wantName { t.Errorf("name = %q, want %q", exercise.Name, tt.wantName) } } }) } } func TestExerciseHandler_Create(t *testing.T) { t.Parallel() now := time.Now() tests := []struct { name string body string mock *mockExerciseRepo wantStatus int }{ { name: "valid strength exercise", body: `{"name":"Deadlift","type":"strength","muscle_group":"Back"}`, mock: &mockExerciseRepo{ exercise: &models.Exercise{ ID: 1, Name: "Deadlift", Type: models.ExerciseTypeStrength, MuscleGroup: "Back", CreatedAt: now, }, }, wantStatus: http.StatusCreated, }, { name: "valid cardio exercise", body: `{"name":"Cycling","type":"cardio"}`, mock: &mockExerciseRepo{ exercise: &models.Exercise{ ID: 2, Name: "Cycling", Type: models.ExerciseTypeCardio, CreatedAt: now, }, }, wantStatus: http.StatusCreated, }, { name: "missing name", body: `{"type":"strength"}`, mock: &mockExerciseRepo{}, wantStatus: http.StatusBadRequest, }, { name: "missing type", body: `{"name":"Test"}`, mock: &mockExerciseRepo{}, wantStatus: http.StatusBadRequest, }, { name: "invalid type", body: `{"name":"Test","type":"invalid"}`, mock: &mockExerciseRepo{}, wantStatus: http.StatusBadRequest, }, { name: "invalid JSON", body: `{invalid}`, mock: &mockExerciseRepo{}, wantStatus: http.StatusBadRequest, }, { name: "repository error", body: `{"name":"Test","type":"strength"}`, mock: &mockExerciseRepo{err: errors.New("db error")}, wantStatus: http.StatusInternalServerError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() handler := NewExerciseHandler(tt.mock) req := httptest.NewRequest(http.MethodPost, "/api/exercises", bytes.NewBufferString(tt.body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } }) } } func TestExerciseHandler_Update(t *testing.T) { t.Parallel() now := time.Now() tests := []struct { name string url string body string mock *mockExerciseRepo wantStatus int }{ { name: "valid update", url: "/api/exercises/1", body: `{"name":"Updated Squat","type":"strength"}`, mock: &mockExerciseRepo{ exercise: &models.Exercise{ ID: 1, Name: "Updated Squat", Type: models.ExerciseTypeStrength, CreatedAt: now, }, }, wantStatus: http.StatusOK, }, { name: "not found", url: "/api/exercises/999", body: `{"name":"Test","type":"strength"}`, mock: &mockExerciseRepo{exercise: nil}, wantStatus: http.StatusNotFound, }, { name: "missing name", url: "/api/exercises/1", body: `{"type":"strength"}`, mock: &mockExerciseRepo{}, wantStatus: http.StatusBadRequest, }, { name: "invalid type", url: "/api/exercises/1", body: `{"name":"Test","type":"invalid"}`, mock: &mockExerciseRepo{}, wantStatus: http.StatusBadRequest, }, { name: "no ID in URL", url: "/api/exercises/", body: `{"name":"Test","type":"strength"}`, mock: &mockExerciseRepo{}, wantStatus: http.StatusBadRequest, }, { name: "repository error", url: "/api/exercises/1", body: `{"name":"Test","type":"strength"}`, mock: &mockExerciseRepo{err: errors.New("db error")}, wantStatus: http.StatusInternalServerError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() handler := NewExerciseHandler(tt.mock) req := httptest.NewRequest(http.MethodPut, tt.url, bytes.NewBufferString(tt.body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } }) } } func TestExerciseHandler_Delete(t *testing.T) { t.Parallel() tests := []struct { name string url string mock *mockExerciseRepo wantStatus int }{ { name: "successful delete", url: "/api/exercises/1", mock: &mockExerciseRepo{deleteErr: nil}, wantStatus: http.StatusNoContent, }, { name: "not found", url: "/api/exercises/999", mock: &mockExerciseRepo{deleteErr: sql.ErrNoRows}, wantStatus: http.StatusNotFound, }, { name: "no ID in URL", url: "/api/exercises/", mock: &mockExerciseRepo{}, wantStatus: http.StatusBadRequest, }, { name: "repository error", url: "/api/exercises/1", mock: &mockExerciseRepo{deleteErr: errors.New("db error")}, wantStatus: http.StatusInternalServerError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() handler := NewExerciseHandler(tt.mock) req := httptest.NewRequest(http.MethodDelete, tt.url, nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } }) } } func TestExerciseHandler_MethodNotAllowed(t *testing.T) { t.Parallel() handler := NewExerciseHandler(&mockExerciseRepo{}) req := httptest.NewRequest(http.MethodPatch, "/api/exercises/1", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusMethodNotAllowed { t.Errorf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed) } }