//go:build integration package storage import ( "context" "database/sql" "testing" "training-tracker/internal/models" ) func TestPlanRepository_CRUD(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } tdb := setupTestDB(t) defer tdb.cleanup(t) planRepo := NewPlanRepository(tdb.DB) exerciseRepo := NewExerciseRepository(tdb.DB) ctx := context.Background() t.Run("List_empty", func(t *testing.T) { tdb.truncateTables(t) plans, err := planRepo.List(ctx) if err != nil { t.Fatalf("List failed: %v", err) } if plans != nil && len(plans) != 0 { t.Errorf("expected empty list, got %d items", len(plans)) } }) t.Run("Create_without_exercises", func(t *testing.T) { tdb.truncateTables(t) req := &models.CreatePlanRequest{ Name: "Push Day", Description: "Chest, shoulders, triceps", } plan, err := planRepo.Create(ctx, req) if err != nil { t.Fatalf("Create failed: %v", err) } if plan.ID == 0 { t.Error("expected non-zero ID") } if plan.Name != req.Name { t.Errorf("name = %q, want %q", plan.Name, req.Name) } if plan.Description != req.Description { t.Errorf("description = %q, want %q", plan.Description, req.Description) } if plan.CreatedAt.IsZero() { t.Error("expected non-zero created_at") } }) t.Run("Create_with_exercises", func(t *testing.T) { tdb.truncateTables(t) exercise1, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{ Name: "Bench Press", Type: models.ExerciseTypeStrength, }) exercise2, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{ Name: "Shoulder Press", Type: models.ExerciseTypeStrength, }) req := &models.CreatePlanRequest{ Name: "Push Day", Exercises: []models.PlanExerciseInput{ {ExerciseID: exercise1.ID, Sets: 4, Reps: 8, Order: 1}, {ExerciseID: exercise2.ID, Sets: 3, Reps: 12, Order: 2}, }, } plan, err := planRepo.Create(ctx, req) if err != nil { t.Fatalf("Create failed: %v", err) } if len(plan.Exercises) != 2 { t.Fatalf("expected 2 exercises, got %d", len(plan.Exercises)) } if plan.Exercises[0].Sets != 4 { t.Errorf("sets = %d, want 4", plan.Exercises[0].Sets) } if plan.Exercises[0].Reps != 8 { t.Errorf("reps = %d, want 8", plan.Exercises[0].Reps) } if plan.Exercises[0].Exercise.Name != "Bench Press" { t.Errorf("exercise name = %q, want %q", plan.Exercises[0].Exercise.Name, "Bench Press") } }) t.Run("Create_with_duration_exercise", func(t *testing.T) { tdb.truncateTables(t) exercise, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{ Name: "Running", Type: models.ExerciseTypeCardio, }) req := &models.CreatePlanRequest{ Name: "Cardio Day", Exercises: []models.PlanExerciseInput{ {ExerciseID: exercise.ID, Duration: 1800, Order: 1}, }, } plan, err := planRepo.Create(ctx, req) if err != nil { t.Fatalf("Create failed: %v", err) } if plan.Exercises[0].Duration != 1800 { t.Errorf("duration = %d, want 1800", plan.Exercises[0].Duration) } }) t.Run("GetByID_found", func(t *testing.T) { tdb.truncateTables(t) created, _ := planRepo.Create(ctx, &models.CreatePlanRequest{ Name: "Test Plan", Description: "Test description", }) plan, err := planRepo.GetByID(ctx, created.ID) if err != nil { t.Fatalf("GetByID failed: %v", err) } if plan == nil { t.Fatal("expected plan, got nil") } if plan.ID != created.ID { t.Errorf("ID = %d, want %d", plan.ID, created.ID) } }) t.Run("GetByID_with_exercises", func(t *testing.T) { tdb.truncateTables(t) exercise, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{ Name: "Squat", Type: models.ExerciseTypeStrength, }) created, _ := planRepo.Create(ctx, &models.CreatePlanRequest{ Name: "Leg Day", Exercises: []models.PlanExerciseInput{ {ExerciseID: exercise.ID, Sets: 5, Reps: 5, Order: 1}, }, }) plan, err := planRepo.GetByID(ctx, created.ID) if err != nil { t.Fatalf("GetByID failed: %v", err) } if len(plan.Exercises) != 1 { t.Fatalf("expected 1 exercise, got %d", len(plan.Exercises)) } if plan.Exercises[0].Exercise.Name != "Squat" { t.Errorf("exercise name = %q, want Squat", plan.Exercises[0].Exercise.Name) } }) t.Run("GetByID_not_found", func(t *testing.T) { tdb.truncateTables(t) plan, err := planRepo.GetByID(ctx, 99999) if err != nil { t.Fatalf("GetByID failed: %v", err) } if plan != nil { t.Errorf("expected nil, got %+v", plan) } }) t.Run("Update_found", func(t *testing.T) { tdb.truncateTables(t) created, _ := planRepo.Create(ctx, &models.CreatePlanRequest{ Name: "Old Name", Description: "Old description", }) updated, err := planRepo.Update(ctx, created.ID, &models.CreatePlanRequest{ Name: "New Name", Description: "New description", }) if err != nil { t.Fatalf("Update failed: %v", err) } if updated == nil { t.Fatal("expected updated plan, got nil") } if updated.Name != "New Name" { t.Errorf("name = %q, want New Name", updated.Name) } if updated.Description != "New description" { t.Errorf("description = %q, want New description", updated.Description) } }) t.Run("Update_replaces_exercises", func(t *testing.T) { tdb.truncateTables(t) exercise1, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{ Name: "Old Exercise", Type: models.ExerciseTypeStrength, }) exercise2, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{ Name: "New Exercise", Type: models.ExerciseTypeStrength, }) created, _ := planRepo.Create(ctx, &models.CreatePlanRequest{ Name: "Test Plan", Exercises: []models.PlanExerciseInput{ {ExerciseID: exercise1.ID, Sets: 3, Reps: 10, Order: 1}, }, }) updated, err := planRepo.Update(ctx, created.ID, &models.CreatePlanRequest{ Name: "Test Plan", Exercises: []models.PlanExerciseInput{ {ExerciseID: exercise2.ID, Sets: 4, Reps: 12, Order: 1}, }, }) if err != nil { t.Fatalf("Update failed: %v", err) } if len(updated.Exercises) != 1 { t.Fatalf("expected 1 exercise, got %d", len(updated.Exercises)) } if updated.Exercises[0].Exercise.Name != "New Exercise" { t.Errorf("exercise name = %q, want New Exercise", updated.Exercises[0].Exercise.Name) } if updated.Exercises[0].Sets != 4 { t.Errorf("sets = %d, want 4", updated.Exercises[0].Sets) } }) t.Run("Update_not_found", func(t *testing.T) { tdb.truncateTables(t) updated, err := planRepo.Update(ctx, 99999, &models.CreatePlanRequest{ Name: "Test", }) 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, _ := planRepo.Create(ctx, &models.CreatePlanRequest{ Name: "To Delete", }) err := planRepo.Delete(ctx, created.ID) if err != nil { t.Fatalf("Delete failed: %v", err) } plan, _ := planRepo.GetByID(ctx, created.ID) if plan != nil { t.Error("expected plan to be deleted") } }) t.Run("Delete_cascades_exercises", func(t *testing.T) { tdb.truncateTables(t) exercise, _ := exerciseRepo.Create(ctx, &models.CreateExerciseRequest{ Name: "Test Exercise", Type: models.ExerciseTypeStrength, }) created, _ := planRepo.Create(ctx, &models.CreatePlanRequest{ Name: "Plan with Exercises", Exercises: []models.PlanExerciseInput{ {ExerciseID: exercise.ID, Sets: 3, Reps: 10, Order: 1}, }, }) err := planRepo.Delete(ctx, created.ID) if err != nil { t.Fatalf("Delete failed: %v", err) } // Verify plan exercises are deleted var count int tdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM plan_exercises WHERE plan_id = $1", created.ID).Scan(&count) if count != 0 { t.Errorf("expected 0 plan_exercises, 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 := planRepo.Delete(ctx, 99999) if err != sql.ErrNoRows { t.Errorf("expected sql.ErrNoRows, got %v", err) } }) t.Run("List_multiple", func(t *testing.T) { tdb.truncateTables(t) planRepo.Create(ctx, &models.CreatePlanRequest{Name: "Alpha Plan"}) planRepo.Create(ctx, &models.CreatePlanRequest{Name: "Beta Plan"}) planRepo.Create(ctx, &models.CreatePlanRequest{Name: "Gamma Plan"}) plans, err := planRepo.List(ctx) if err != nil { t.Fatalf("List failed: %v", err) } if len(plans) != 3 { t.Errorf("len = %d, want 3", len(plans)) } // Results are ordered by name if plans[0].Name != "Alpha Plan" { t.Errorf("first plan = %q, want Alpha Plan", plans[0].Name) } }) }