//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) } }) }