Files
training-tracker/backend/internal/storage/sessions_test.go
2026-01-19 19:50:21 +01:00

436 lines
11 KiB
Go

//go:build integration
package storage
import (
"context"
"database/sql"
"testing"
"time"
"training-tracker/internal/models"
)
func TestSessionRepository_CRUD(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
tdb := setupTestDB(t)
defer tdb.cleanup(t)
sessionRepo := NewSessionRepository(tdb.DB)
planRepo := NewPlanRepository(tdb.DB)
exerciseRepo := NewExerciseRepository(tdb.DB)
ctx := context.Background()
t.Run("List_empty", func(t *testing.T) {
tdb.truncateTables(t)
sessions, err := sessionRepo.List(ctx)
if err != nil {
t.Fatalf("List failed: %v", err)
}
if sessions != nil && len(sessions) != 0 {
t.Errorf("expected empty list, got %d items", len(sessions))
}
})
t.Run("Create_without_plan", func(t *testing.T) {
tdb.truncateTables(t)
now := time.Now().Truncate(time.Second)
req := &models.CreateSessionRequest{
Date: now,
Notes: "Morning workout",
}
session, err := sessionRepo.Create(ctx, req)
if err != nil {
t.Fatalf("Create failed: %v", err)
}
if session.ID == 0 {
t.Error("expected non-zero ID")
}
if session.PlanID != nil {
t.Errorf("expected nil plan_id, got %d", *session.PlanID)
}
if session.Notes != req.Notes {
t.Errorf("notes = %q, want %q", session.Notes, req.Notes)
}
if session.CreatedAt.IsZero() {
t.Error("expected non-zero created_at")
}
})
t.Run("Create_with_plan", func(t *testing.T) {
tdb.truncateTables(t)
plan, _ := planRepo.Create(ctx, &models.CreatePlanRequest{Name: "Test Plan"})
now := time.Now().Truncate(time.Second)
req := &models.CreateSessionRequest{
PlanID: &plan.ID,
Date: now,
Notes: "Following plan",
}
session, err := sessionRepo.Create(ctx, req)
if err != nil {
t.Fatalf("Create failed: %v", err)
}
if session.PlanID == nil {
t.Fatal("expected plan_id, got nil")
}
if *session.PlanID != plan.ID {
t.Errorf("plan_id = %d, want %d", *session.PlanID, plan.ID)
}
})
t.Run("GetByID_found", func(t *testing.T) {
tdb.truncateTables(t)
created, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
Date: time.Now(),
Notes: "Test session",
})
session, err := sessionRepo.GetByID(ctx, created.ID)
if err != nil {
t.Fatalf("GetByID failed: %v", err)
}
if session == nil {
t.Fatal("expected session, got nil")
}
if session.ID != created.ID {
t.Errorf("ID = %d, want %d", session.ID, created.ID)
}
if session.Notes != "Test session" {
t.Errorf("notes = %q, want Test session", session.Notes)
}
})
t.Run("GetByID_with_plan", func(t *testing.T) {
tdb.truncateTables(t)
plan, _ := planRepo.Create(ctx, &models.CreatePlanRequest{Name: "My Plan"})
created, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
PlanID: &plan.ID,
Date: time.Now(),
})
session, err := sessionRepo.GetByID(ctx, created.ID)
if err != nil {
t.Fatalf("GetByID failed: %v", err)
}
if session.Plan == nil {
t.Fatal("expected plan, got nil")
}
if session.Plan.Name != "My Plan" {
t.Errorf("plan name = %q, want My Plan", session.Plan.Name)
}
})
t.Run("GetByID_with_entries", func(t *testing.T) {
tdb.truncateTables(t)
exercise, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{
Name: "Bench Press",
Type: models.ExerciseTypeStrength,
})
session, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
Date: time.Now(),
})
sessionRepo.AddEntry(ctx, session.ID, &models.CreateSessionEntryRequest{
ExerciseID: exercise.ID,
Weight: 100,
Reps: 10,
SetsCompleted: 3,
})
fetched, err := sessionRepo.GetByID(ctx, session.ID)
if err != nil {
t.Fatalf("GetByID failed: %v", err)
}
if len(fetched.Entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(fetched.Entries))
}
if fetched.Entries[0].Weight != 100 {
t.Errorf("weight = %f, want 100", fetched.Entries[0].Weight)
}
if fetched.Entries[0].Exercise == nil {
t.Fatal("expected exercise, got nil")
}
if fetched.Entries[0].Exercise.Name != "Bench Press" {
t.Errorf("exercise name = %q, want Bench Press", fetched.Entries[0].Exercise.Name)
}
})
t.Run("GetByID_not_found", func(t *testing.T) {
tdb.truncateTables(t)
session, err := sessionRepo.GetByID(ctx, 99999)
if err != nil {
t.Fatalf("GetByID failed: %v", err)
}
if session != nil {
t.Errorf("expected nil, got %+v", session)
}
})
t.Run("Update_found", func(t *testing.T) {
tdb.truncateTables(t)
created, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
Date: time.Now(),
Notes: "Old notes",
})
newDate := time.Now().Add(24 * time.Hour).Truncate(time.Second)
updated, err := sessionRepo.Update(ctx, created.ID, &models.CreateSessionRequest{
Date: newDate,
Notes: "New notes",
})
if err != nil {
t.Fatalf("Update failed: %v", err)
}
if updated == nil {
t.Fatal("expected updated session, got nil")
}
if updated.Notes != "New notes" {
t.Errorf("notes = %q, want New notes", updated.Notes)
}
})
t.Run("Update_add_plan", func(t *testing.T) {
tdb.truncateTables(t)
plan, _ := planRepo.Create(ctx, &models.CreatePlanRequest{Name: "New Plan"})
created, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
Date: time.Now(),
})
updated, err := sessionRepo.Update(ctx, created.ID, &models.CreateSessionRequest{
PlanID: &plan.ID,
Date: created.Date,
})
if err != nil {
t.Fatalf("Update failed: %v", err)
}
if updated.PlanID == nil {
t.Fatal("expected plan_id, got nil")
}
if *updated.PlanID != plan.ID {
t.Errorf("plan_id = %d, want %d", *updated.PlanID, plan.ID)
}
})
t.Run("Update_not_found", func(t *testing.T) {
tdb.truncateTables(t)
updated, err := sessionRepo.Update(ctx, 99999, &models.CreateSessionRequest{
Date: time.Now(),
})
if err != nil {
t.Fatalf("Update failed: %v", err)
}
if updated != nil {
t.Errorf("expected nil, got %+v", updated)
}
})
t.Run("Delete_found", func(t *testing.T) {
tdb.truncateTables(t)
created, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
Date: time.Now(),
})
err := sessionRepo.Delete(ctx, created.ID)
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
session, _ := sessionRepo.GetByID(ctx, created.ID)
if session != nil {
t.Error("expected session to be deleted")
}
})
t.Run("Delete_cascades_entries", func(t *testing.T) {
tdb.truncateTables(t)
exercise, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{
Name: "Test Exercise",
Type: models.ExerciseTypeStrength,
})
session, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
Date: time.Now(),
})
sessionRepo.AddEntry(ctx, session.ID, &models.CreateSessionEntryRequest{
ExerciseID: exercise.ID,
Weight: 50,
})
err := sessionRepo.Delete(ctx, session.ID)
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
// Verify entries are deleted
var count int
tdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM session_entries WHERE session_id = $1", session.ID).Scan(&count)
if count != 0 {
t.Errorf("expected 0 session_entries, got %d", count)
}
// But the exercise itself should still exist
existingExercise, _ := exerciseRepo.GetByID(ctx, exercise.ID)
if existingExercise == nil {
t.Error("expected exercise to still exist")
}
})
t.Run("Delete_not_found", func(t *testing.T) {
tdb.truncateTables(t)
err := sessionRepo.Delete(ctx, 99999)
if err != sql.ErrNoRows {
t.Errorf("expected sql.ErrNoRows, got %v", err)
}
})
t.Run("List_multiple_ordered_by_date", func(t *testing.T) {
tdb.truncateTables(t)
now := time.Now().Truncate(time.Second)
sessionRepo.Create(ctx, &models.CreateSessionRequest{Date: now.Add(-48 * time.Hour), Notes: "Two days ago"})
sessionRepo.Create(ctx, &models.CreateSessionRequest{Date: now, Notes: "Today"})
sessionRepo.Create(ctx, &models.CreateSessionRequest{Date: now.Add(-24 * time.Hour), Notes: "Yesterday"})
sessions, err := sessionRepo.List(ctx)
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(sessions) != 3 {
t.Errorf("len = %d, want 3", len(sessions))
}
// Results are ordered by date DESC
if sessions[0].Notes != "Today" {
t.Errorf("first session = %q, want Today", sessions[0].Notes)
}
if sessions[1].Notes != "Yesterday" {
t.Errorf("second session = %q, want Yesterday", sessions[1].Notes)
}
})
t.Run("AddEntry_strength", func(t *testing.T) {
tdb.truncateTables(t)
exercise, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{
Name: "Squat",
Type: models.ExerciseTypeStrength,
})
session, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
Date: time.Now(),
})
entry, err := sessionRepo.AddEntry(ctx, session.ID, &models.CreateSessionEntryRequest{
ExerciseID: exercise.ID,
Weight: 140,
Reps: 5,
SetsCompleted: 5,
Notes: "Felt heavy",
})
if err != nil {
t.Fatalf("AddEntry failed: %v", err)
}
if entry.ID == 0 {
t.Error("expected non-zero ID")
}
if entry.SessionID != session.ID {
t.Errorf("session_id = %d, want %d", entry.SessionID, session.ID)
}
if entry.Weight != 140 {
t.Errorf("weight = %f, want 140", entry.Weight)
}
if entry.Reps != 5 {
t.Errorf("reps = %d, want 5", entry.Reps)
}
if entry.SetsCompleted != 5 {
t.Errorf("sets_completed = %d, want 5", entry.SetsCompleted)
}
if entry.Notes != "Felt heavy" {
t.Errorf("notes = %q, want Felt heavy", entry.Notes)
}
})
t.Run("AddEntry_cardio", func(t *testing.T) {
tdb.truncateTables(t)
exercise, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{
Name: "Running",
Type: models.ExerciseTypeCardio,
})
session, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
Date: time.Now(),
})
entry, err := sessionRepo.AddEntry(ctx, session.ID, &models.CreateSessionEntryRequest{
ExerciseID: exercise.ID,
Duration: 3600,
Distance: 10.5,
HeartRate: 155,
})
if err != nil {
t.Fatalf("AddEntry failed: %v", err)
}
if entry.Duration != 3600 {
t.Errorf("duration = %d, want 3600", entry.Duration)
}
if entry.Distance != 10.5 {
t.Errorf("distance = %f, want 10.5", entry.Distance)
}
if entry.HeartRate != 155 {
t.Errorf("heart_rate = %d, want 155", entry.HeartRate)
}
})
t.Run("Plan_delete_sets_null_on_sessions", func(t *testing.T) {
tdb.truncateTables(t)
plan, _ := planRepo.Create(ctx, &models.CreatePlanRequest{Name: "Will be deleted"})
session, _ := sessionRepo.Create(ctx, &models.CreateSessionRequest{
PlanID: &plan.ID,
Date: time.Now(),
})
err := planRepo.Delete(ctx, plan.ID)
if err != nil {
t.Fatalf("Delete plan failed: %v", err)
}
// Session should still exist but with null plan_id
fetched, _ := sessionRepo.GetByID(ctx, session.ID)
if fetched == nil {
t.Fatal("expected session to still exist")
}
if fetched.PlanID != nil {
t.Errorf("expected nil plan_id after plan deletion, got %d", *fetched.PlanID)
}
})
}