518 lines
12 KiB
Go
518 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"training-tracker/internal/models"
|
|
)
|
|
|
|
func TestSessionHandler_List(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
mock *mockSessionRepo
|
|
wantStatus int
|
|
wantLen int
|
|
}{
|
|
{
|
|
name: "empty list",
|
|
mock: &mockSessionRepo{sessions: nil},
|
|
wantStatus: http.StatusOK,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "multiple sessions",
|
|
mock: &mockSessionRepo{
|
|
sessions: []models.Session{
|
|
{ID: 1, Date: now, Notes: "Morning workout"},
|
|
{ID: 2, Date: now.AddDate(0, 0, -1), Notes: "Evening cardio"},
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
wantLen: 2,
|
|
},
|
|
{
|
|
name: "session with plan",
|
|
mock: &mockSessionRepo{
|
|
sessions: []models.Session{
|
|
{
|
|
ID: 1,
|
|
Date: now,
|
|
PlanID: ptr(int64(1)),
|
|
Plan: &models.TrainingPlan{ID: 1, Name: "Push Day"},
|
|
},
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
wantLen: 1,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
mock: &mockSessionRepo{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 := NewSessionHandler(tt.mock)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/sessions", 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 sessions []models.Session
|
|
if err := json.NewDecoder(rec.Body).Decode(&sessions); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if len(sessions) != tt.wantLen {
|
|
t.Errorf("len = %d, want %d", len(sessions), tt.wantLen)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionHandler_GetByID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
mock *mockSessionRepo
|
|
wantStatus int
|
|
wantNotes string
|
|
}{
|
|
{
|
|
name: "found",
|
|
url: "/api/sessions/1",
|
|
mock: &mockSessionRepo{
|
|
session: &models.Session{
|
|
ID: 1,
|
|
Date: now,
|
|
Notes: "Great workout",
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
wantNotes: "Great workout",
|
|
},
|
|
{
|
|
name: "found with entries",
|
|
url: "/api/sessions/1",
|
|
mock: &mockSessionRepo{
|
|
session: &models.Session{
|
|
ID: 1,
|
|
Date: now,
|
|
Notes: "Complete session",
|
|
Entries: []models.SessionEntry{
|
|
{ID: 1, SessionID: 1, ExerciseID: 1, Weight: 100, Reps: 10},
|
|
{ID: 2, SessionID: 1, ExerciseID: 2, Duration: 1800},
|
|
},
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
wantNotes: "Complete session",
|
|
},
|
|
{
|
|
name: "not found",
|
|
url: "/api/sessions/999",
|
|
mock: &mockSessionRepo{session: nil},
|
|
wantStatus: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
url: "/api/sessions/1",
|
|
mock: &mockSessionRepo{err: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewSessionHandler(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 session models.Session
|
|
if err := json.NewDecoder(rec.Body).Decode(&session); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if session.Notes != tt.wantNotes {
|
|
t.Errorf("notes = %q, want %q", session.Notes, tt.wantNotes)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionHandler_Create(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Now()
|
|
dateStr := now.Format(time.RFC3339)
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
mock *mockSessionRepo
|
|
wantStatus int
|
|
}{
|
|
{
|
|
name: "valid session without plan",
|
|
body: `{"date":"` + dateStr + `","notes":"Morning run"}`,
|
|
mock: &mockSessionRepo{
|
|
session: &models.Session{
|
|
ID: 1,
|
|
Date: now,
|
|
Notes: "Morning run",
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusCreated,
|
|
},
|
|
{
|
|
name: "valid session with plan",
|
|
body: `{"date":"` + dateStr + `","plan_id":1,"notes":"Following plan"}`,
|
|
mock: &mockSessionRepo{
|
|
session: &models.Session{
|
|
ID: 2,
|
|
Date: now,
|
|
PlanID: ptr(int64(1)),
|
|
Notes: "Following plan",
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusCreated,
|
|
},
|
|
{
|
|
name: "missing date",
|
|
body: `{"notes":"No date"}`,
|
|
mock: &mockSessionRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "invalid JSON",
|
|
body: `{invalid}`,
|
|
mock: &mockSessionRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
body: `{"date":"` + dateStr + `"}`,
|
|
mock: &mockSessionRepo{err: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewSessionHandler(tt.mock)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/sessions", 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 TestSessionHandler_Update(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Now()
|
|
dateStr := now.Format(time.RFC3339)
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
body string
|
|
mock *mockSessionRepo
|
|
wantStatus int
|
|
}{
|
|
{
|
|
name: "valid update",
|
|
url: "/api/sessions/1",
|
|
body: `{"date":"` + dateStr + `","notes":"Updated notes"}`,
|
|
mock: &mockSessionRepo{
|
|
session: &models.Session{
|
|
ID: 1,
|
|
Date: now,
|
|
Notes: "Updated notes",
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "update with plan",
|
|
url: "/api/sessions/1",
|
|
body: `{"date":"` + dateStr + `","plan_id":2}`,
|
|
mock: &mockSessionRepo{
|
|
session: &models.Session{
|
|
ID: 1,
|
|
Date: now,
|
|
PlanID: ptr(int64(2)),
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "not found",
|
|
url: "/api/sessions/999",
|
|
body: `{"date":"` + dateStr + `"}`,
|
|
mock: &mockSessionRepo{session: nil},
|
|
wantStatus: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "missing date",
|
|
url: "/api/sessions/1",
|
|
body: `{"notes":"No date"}`,
|
|
mock: &mockSessionRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "no ID in URL",
|
|
url: "/api/sessions/",
|
|
body: `{"date":"` + dateStr + `"}`,
|
|
mock: &mockSessionRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
url: "/api/sessions/1",
|
|
body: `{"date":"` + dateStr + `"}`,
|
|
mock: &mockSessionRepo{err: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewSessionHandler(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 TestSessionHandler_Delete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
mock *mockSessionRepo
|
|
wantStatus int
|
|
}{
|
|
{
|
|
name: "successful delete",
|
|
url: "/api/sessions/1",
|
|
mock: &mockSessionRepo{deleteErr: nil},
|
|
wantStatus: http.StatusNoContent,
|
|
},
|
|
{
|
|
name: "not found",
|
|
url: "/api/sessions/999",
|
|
mock: &mockSessionRepo{deleteErr: sql.ErrNoRows},
|
|
wantStatus: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "no ID in URL",
|
|
url: "/api/sessions/",
|
|
mock: &mockSessionRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
url: "/api/sessions/1",
|
|
mock: &mockSessionRepo{deleteErr: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewSessionHandler(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 TestSessionHandler_AddEntry(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Now()
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
body string
|
|
mock *mockSessionRepo
|
|
wantStatus int
|
|
}{
|
|
{
|
|
name: "valid strength entry",
|
|
url: "/api/sessions/1/entries",
|
|
body: `{"exercise_id":1,"weight":100,"reps":10,"sets_completed":3}`,
|
|
mock: &mockSessionRepo{
|
|
entry: &models.SessionEntry{
|
|
ID: 1,
|
|
SessionID: 1,
|
|
ExerciseID: 1,
|
|
Weight: 100,
|
|
Reps: 10,
|
|
SetsCompleted: 3,
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusCreated,
|
|
},
|
|
{
|
|
name: "valid cardio entry",
|
|
url: "/api/sessions/1/entries",
|
|
body: `{"exercise_id":2,"duration":1800,"distance":5.5,"heart_rate":145}`,
|
|
mock: &mockSessionRepo{
|
|
entry: &models.SessionEntry{
|
|
ID: 2,
|
|
SessionID: 1,
|
|
ExerciseID: 2,
|
|
Duration: 1800,
|
|
Distance: 5.5,
|
|
HeartRate: 145,
|
|
CreatedAt: now,
|
|
},
|
|
},
|
|
wantStatus: http.StatusCreated,
|
|
},
|
|
{
|
|
name: "missing exercise_id",
|
|
url: "/api/sessions/1/entries",
|
|
body: `{"weight":100,"reps":10}`,
|
|
mock: &mockSessionRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "invalid JSON",
|
|
url: "/api/sessions/1/entries",
|
|
body: `{invalid}`,
|
|
mock: &mockSessionRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "no session ID",
|
|
url: "/api/sessions//entries",
|
|
body: `{"exercise_id":1}`,
|
|
mock: &mockSessionRepo{},
|
|
wantStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "repository error",
|
|
url: "/api/sessions/1/entries",
|
|
body: `{"exercise_id":1}`,
|
|
mock: &mockSessionRepo{entryErr: errors.New("db error")},
|
|
wantStatus: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewSessionHandler(tt.mock)
|
|
req := httptest.NewRequest(http.MethodPost, 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 TestSessionHandler_MethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewSessionHandler(&mockSessionRepo{})
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/sessions/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func TestSessionHandler_EntriesMethodNotAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler := NewSessionHandler(&mockSessionRepo{})
|
|
req := httptest.NewRequest(http.MethodGet, "/api/sessions/1/entries", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// ptr is a helper to create a pointer to a value
|
|
func ptr[T any](v T) *T {
|
|
return &v
|
|
}
|