397 lines
9.0 KiB
Go
397 lines
9.0 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"training-tracker/internal/models"
|
|
)
|
|
|
|
func TestPlanHandler_List(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
mock *mockPlanRepo
|
|
wantStatus int
|
|
wantLen int
|
|
}{
|
|
{
|
|
name: "empty list",
|
|
mock: &mockPlanRepo{plans: nil},
|
|
wantStatus: http.StatusOK,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "multiple plans",
|
|
mock: &mockPlanRepo{
|
|
plans: []models.TrainingPlan{
|
|
{ID: 1, Name: "Push Day"},
|
|
{ID: 2, Name: "Pull Day"},
|
|
{ID: 3, Name: "Leg Day"},
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
wantLen: 3,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
mock: &mockPlanRepo{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 := NewPlanHandler(tt.mock)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/plans", 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 plans []models.TrainingPlan
|
|
if err := json.NewDecoder(rec.Body).Decode(&plans); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if len(plans) != tt.wantLen {
|
|
t.Errorf("len = %d, want %d", len(plans), tt.wantLen)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlanHandler_GetByID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
mock *mockPlanRepo
|
|
wantStatus int
|
|
wantName string
|
|
}{
|
|
{
|
|
name: "found",
|
|
url: "/api/plans/1",
|
|
mock: &mockPlanRepo{
|
|
plan: &models.TrainingPlan{
|
|
ID: 1,
|
|
Name: "Full Body",
|
|
Description: "Complete workout",
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
wantName: "Full Body",
|
|
},
|
|
{
|
|
name: "found with exercises",
|
|
url: "/api/plans/2",
|
|
mock: &mockPlanRepo{
|
|
plan: &models.TrainingPlan{
|
|
ID: 2,
|
|
Name: "Upper Body",
|
|
Exercises: []models.PlanExercise{
|
|
{ID: 1, PlanID: 2, ExerciseID: 1, Sets: 3, Reps: 10},
|
|
{ID: 2, PlanID: 2, ExerciseID: 2, Sets: 4, Reps: 8},
|
|
},
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
wantName: "Upper Body",
|
|
},
|
|
{
|
|
name: "not found",
|
|
url: "/api/plans/999",
|
|
mock: &mockPlanRepo{plan: nil},
|
|
wantStatus: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
url: "/api/plans/1",
|
|
mock: &mockPlanRepo{err: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewPlanHandler(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 plan models.TrainingPlan
|
|
if err := json.NewDecoder(rec.Body).Decode(&plan); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if plan.Name != tt.wantName {
|
|
t.Errorf("name = %q, want %q", plan.Name, tt.wantName)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlanHandler_Create(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
mock *mockPlanRepo
|
|
wantStatus int
|
|
}{
|
|
{
|
|
name: "valid plan without exercises",
|
|
body: `{"name":"Beginner Plan","description":"A simple plan"}`,
|
|
mock: &mockPlanRepo{
|
|
plan: &models.TrainingPlan{
|
|
ID: 1,
|
|
Name: "Beginner Plan",
|
|
Description: "A simple plan",
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusCreated,
|
|
},
|
|
{
|
|
name: "valid plan with exercises",
|
|
body: `{"name":"Advanced Plan","exercises":[{"exercise_id":1,"sets":3,"reps":10,"order":1}]}`,
|
|
mock: &mockPlanRepo{
|
|
plan: &models.TrainingPlan{
|
|
ID: 2,
|
|
Name: "Advanced Plan",
|
|
Exercises: []models.PlanExercise{
|
|
{ID: 1, PlanID: 2, ExerciseID: 1, Sets: 3, Reps: 10, Order: 1},
|
|
},
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusCreated,
|
|
},
|
|
{
|
|
name: "missing name",
|
|
body: `{"description":"No name"}`,
|
|
mock: &mockPlanRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "empty name",
|
|
body: `{"name":""}`,
|
|
mock: &mockPlanRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "invalid JSON",
|
|
body: `{invalid}`,
|
|
mock: &mockPlanRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
body: `{"name":"Test Plan"}`,
|
|
mock: &mockPlanRepo{err: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewPlanHandler(tt.mock)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/plans", 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 TestPlanHandler_Update(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
body string
|
|
mock *mockPlanRepo
|
|
wantStatus int
|
|
}{
|
|
{
|
|
name: "valid update",
|
|
url: "/api/plans/1",
|
|
body: `{"name":"Updated Plan","description":"New description"}`,
|
|
mock: &mockPlanRepo{
|
|
plan: &models.TrainingPlan{
|
|
ID: 1,
|
|
Name: "Updated Plan",
|
|
Description: "New description",
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "update with exercises",
|
|
url: "/api/plans/1",
|
|
body: `{"name":"Plan with Exercises","exercises":[{"exercise_id":1,"sets":4,"reps":12,"order":1}]}`,
|
|
mock: &mockPlanRepo{
|
|
plan: &models.TrainingPlan{
|
|
ID: 1,
|
|
Name: "Plan with Exercises",
|
|
Exercises: []models.PlanExercise{
|
|
{ID: 1, PlanID: 1, ExerciseID: 1, Sets: 4, Reps: 12, Order: 1},
|
|
},
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "not found",
|
|
url: "/api/plans/999",
|
|
body: `{"name":"Test"}`,
|
|
mock: &mockPlanRepo{plan: nil},
|
|
wantStatus: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "missing name",
|
|
url: "/api/plans/1",
|
|
body: `{"description":"No name"}`,
|
|
mock: &mockPlanRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "no ID in URL",
|
|
url: "/api/plans/",
|
|
body: `{"name":"Test"}`,
|
|
mock: &mockPlanRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
url: "/api/plans/1",
|
|
body: `{"name":"Test"}`,
|
|
mock: &mockPlanRepo{err: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewPlanHandler(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 TestPlanHandler_Delete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
mock *mockPlanRepo
|
|
wantStatus int
|
|
}{
|
|
{
|
|
name: "successful delete",
|
|
url: "/api/plans/1",
|
|
mock: &mockPlanRepo{deleteErr: nil},
|
|
wantStatus: http.StatusNoContent,
|
|
},
|
|
{
|
|
name: "not found",
|
|
url: "/api/plans/999",
|
|
mock: &mockPlanRepo{deleteErr: sql.ErrNoRows},
|
|
wantStatus: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "no ID in URL",
|
|
url: "/api/plans/",
|
|
mock: &mockPlanRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
url: "/api/plans/1",
|
|
mock: &mockPlanRepo{deleteErr: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewPlanHandler(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 TestPlanHandler_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewPlanHandler(&mockPlanRepo{})
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/plans/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|