436 lines
11 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|