Ctrl K
ComunidadGuías…

internal/handler/pr_handler_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/johnzastrow/actalog/internal/domain"
	"github.com/johnzastrow/actalog/internal/repository"
)

func TestNewPRHandler(t *testing.T) {
	handler := NewPRHandler(nil, createTestLogger())
	if handler == nil {
		t.Fatal("NewPRHandler() should not return nil")
	}
}

// createTestPRHandler creates a PR handler with test database and sample data
func createTestPRHandler(t *testing.T) (*PRHandler, func()) {
	db, cleanup, err := repository.SetupTestDB()
	if err != nil {
		t.Fatalf("Failed to setup test database: %v", err)
	}

	// Create test user
	userRepo := repository.NewSQLiteUserRepository(db)
	now := time.Now()
	testUser := &domain.User{
		Email:        "prtest@example.com",
		PasswordHash: "hashedpassword",
		Role:         "athlete",
		CreatedAt:    now,
		UpdatedAt:    now,
	}
	if err := userRepo.Create(testUser); err != nil {
		cleanup()
		t.Fatalf("Failed to create test user: %v", err)
	}

	// Create a movement
	_, err = db.Exec(`
		INSERT INTO movements (name, type, description, created_at, updated_at)
		VALUES ('Back Squat', 'weightlifting', 'Barbell back squat', ?, ?)
	`, now, now)
	if err != nil {
		cleanup()
		t.Fatalf("Failed to create movement: %v", err)
	}

	// Create a workout
	_, err = db.Exec(`
		INSERT INTO workouts (name, notes, created_by, created_at, updated_at)
		VALUES ('Test Workout', 'Test notes', 1, ?, ?)
	`, now, now)
	if err != nil {
		cleanup()
		t.Fatalf("Failed to create workout: %v", err)
	}

	// Create a user workout
	_, err = db.Exec(`
		INSERT INTO user_workouts (user_id, workout_id, workout_date, created_at, updated_at)
		VALUES (1, 1, ?, ?, ?)
	`, now.Format("2006-01-02"), now, now)
	if err != nil {
		cleanup()
		t.Fatalf("Failed to create user workout: %v", err)
	}

	// Create a workout movement with PR
	_, err = db.Exec(`
		INSERT INTO workout_movements (workout_id, movement_id, sets, reps, weight, is_pr, order_index, created_at, updated_at)
		VALUES (1, 1, 5, 5, 315.0, 1, 0, ?, ?)
	`, now, now)
	if err != nil {
		cleanup()
		t.Fatalf("Failed to create workout movement: %v", err)
	}

	// Create a WOD
	_, err = db.Exec(`
		INSERT INTO wods (name, type, score_type, description, created_by, created_at, updated_at)
		VALUES ('Fran', 'For Time', 'time', '21-15-9 Thrusters and Pull-ups', 1, ?, ?)
	`, now, now)
	if err != nil {
		cleanup()
		t.Fatalf("Failed to create WOD: %v", err)
	}

	// Create a workout WOD with PR
	_, err = db.Exec(`
		INSERT INTO workout_wods (workout_id, wod_id, score_value, division, is_pr, order_index, created_at, updated_at)
		VALUES (1, 1, '5:30', 'rx', 1, 0, ?, ?)
	`, now, now)
	if err != nil {
		cleanup()
		t.Fatalf("Failed to create workout WOD: %v", err)
	}

	handler := NewPRHandler(db, createTestLogger())
	return handler, cleanup
}

// Removed struct field assignment tests:
// - TestPersonalRecord_Struct, TestPersonalRecord_NilFields
// - TestMovementPRSummary_Struct, TestMovementPRSummary_NilFields
// These tests verified Go struct assignment works, not business logic.

func TestPRHandler_GetPersonalRecords_Unauthorized(t *testing.T) {
	handler := &PRHandler{}

	req := createTestRequest(http.MethodGet, "/api/prs", "")
	rr := httptest.NewRecorder()

	handler.GetPersonalRecords(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestPRHandler_GetPRMovements_Unauthorized(t *testing.T) {
	handler := &PRHandler{}

	req := createTestRequest(http.MethodGet, "/api/prs/movements", "")
	rr := httptest.NewRecorder()

	handler.GetPRMovements(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestPRHandler_ToggleMovementPR_Unauthorized(t *testing.T) {
	handler := &PRHandler{}

	req := createTestRequest(http.MethodPut, "/api/prs/toggle?id=1", "")
	rr := httptest.NewRecorder()

	handler.ToggleMovementPR(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestPRHandler_ToggleMovementPR_MissingID(t *testing.T) {
	handler := &PRHandler{}

	req := createAuthenticatedRequest(http.MethodPut, "/api/prs/toggle", "", 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ToggleMovementPR(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Missing movement ID")
}

func TestPRHandler_ToggleMovementPR_InvalidID(t *testing.T) {
	handler := &PRHandler{}

	req := createAuthenticatedRequest(http.MethodPut, "/api/prs/toggle?id=abc", "", 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ToggleMovementPR(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid movement ID")
}

func TestPRHandler_ToggleMovementPR_InvalidIDFormats(t *testing.T) {
	handler := &PRHandler{}

	testCases := []struct {
		name string
		id   string
	}{
		{"float", "1.5"},
		{"empty", ""},
		{"special chars", "1@#$"},
		{"overflow", "99999999999999999999"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			url := "/api/prs/toggle"
			if tc.id != "" {
				url += "?id=" + tc.id
			}
			req := createAuthenticatedRequest(http.MethodPut, url, "", 1, "test@example.com", "user")
			rr := httptest.NewRecorder()

			handler.ToggleMovementPR(rr, req)

			assertStatusCode(t, rr, http.StatusBadRequest)
		})
	}
}

// Success path tests with real database

func TestPRHandler_GetPersonalRecords_Success(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	req := createAuthenticatedRequest(http.MethodGet, "/api/prs", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetPersonalRecords(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
	assertContentType(t, rr, "application/json")

	body := rr.Body.String()
	if !strings.Contains(body, "prs") {
		t.Error("Response should contain 'prs' field")
	}
	// Should have both movement and WOD PRs
	if !strings.Contains(body, "Back Squat") {
		t.Error("Response should contain the movement PR 'Back Squat'")
	}
	if !strings.Contains(body, "Fran") {
		t.Error("Response should contain the WOD PR 'Fran'")
	}
	// Check for calculated 1RM
	if !strings.Contains(body, "calculated_1rm") {
		t.Error("Response should contain 'calculated_1rm' for weight-based PRs")
	}
}

func TestPRHandler_GetPersonalRecords_WithLimit(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	req := createAuthenticatedRequest(http.MethodGet, "/api/prs?limit=1", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetPersonalRecords(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
}

func TestPRHandler_GetPersonalRecords_WithHighLimit(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	// Request with limit over max (200) - should be capped
	req := createAuthenticatedRequest(http.MethodGet, "/api/prs?limit=500", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetPersonalRecords(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
}

func TestPRHandler_GetPersonalRecords_WithInvalidLimit(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	// Request with invalid limit (should use default)
	req := createAuthenticatedRequest(http.MethodGet, "/api/prs?limit=abc", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetPersonalRecords(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
}

func TestPRHandler_GetPRMovements_Success(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	req := createAuthenticatedRequest(http.MethodGet, "/api/prs/movements", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetPRMovements(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
	assertContentType(t, rr, "application/json")

	body := rr.Body.String()
	if !strings.Contains(body, "movements") {
		t.Error("Response should contain 'movements' field")
	}
	if !strings.Contains(body, "Back Squat") {
		t.Error("Response should contain 'Back Squat' movement")
	}
	if !strings.Contains(body, "pr_count") {
		t.Error("Response should contain 'pr_count' field")
	}
	if !strings.Contains(body, "best_1rm") {
		t.Error("Response should contain 'best_1rm' field for weightlifting movements")
	}
}

func TestPRHandler_GetPRMovements_WithLimit(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	req := createAuthenticatedRequest(http.MethodGet, "/api/prs/movements?limit=5", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetPRMovements(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
}

func TestPRHandler_GetPRMovements_WithHighLimit(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	// Request with limit over max (100) - should be capped
	req := createAuthenticatedRequest(http.MethodGet, "/api/prs/movements?limit=500", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetPRMovements(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
}

func TestPRHandler_ToggleMovementPR_Success(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	// Toggle the PR for workout_movement id=1
	req := createAuthenticatedRequest(http.MethodPut, "/api/prs/toggle?id=1", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ToggleMovementPR(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
	assertContentType(t, rr, "application/json")

	body := rr.Body.String()
	if !strings.Contains(body, "is_pr") {
		t.Error("Response should contain 'is_pr' field")
	}
	if !strings.Contains(body, "message") {
		t.Error("Response should contain 'message' field")
	}
}

func TestPRHandler_ToggleMovementPR_NotFound(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	// Try to toggle a non-existent movement
	req := createAuthenticatedRequest(http.MethodPut, "/api/prs/toggle?id=9999", "", 1, "prtest@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ToggleMovementPR(rr, req)

	assertStatusCode(t, rr, http.StatusNotFound)
	assertBodyContains(t, rr, "not found")
}

func TestPRHandler_ToggleMovementPR_WrongUser(t *testing.T) {
	handler, cleanup := createTestPRHandler(t)
	defer cleanup()

	// Try to toggle with a different user (user_id=2 instead of 1)
	req := createAuthenticatedRequest(http.MethodPut, "/api/prs/toggle?id=1", "", 2, "other@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ToggleMovementPR(rr, req)

	assertStatusCode(t, rr, http.StatusNotFound)
	assertBodyContains(t, rr, "not found")
}

func TestPRHandler_GetPersonalRecords_EmptyResult(t *testing.T) {
	db, cleanup, err := repository.SetupTestDB()
	if err != nil {
		t.Fatalf("Failed to setup test database: %v", err)
	}
	defer cleanup()

	// Create user but no PRs
	userRepo := repository.NewSQLiteUserRepository(db)
	now := time.Now()
	testUser := &domain.User{
		Email:        "empty@example.com",
		PasswordHash: "hashedpassword",
		Role:         "athlete",
		CreatedAt:    now,
		UpdatedAt:    now,
	}
	if err := userRepo.Create(testUser); err != nil {
		t.Fatalf("Failed to create test user: %v", err)
	}

	handler := NewPRHandler(db, createTestLogger())

	req := createAuthenticatedRequest(http.MethodGet, "/api/prs", "", 1, "empty@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetPersonalRecords(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
	// Should return empty list
	body := rr.Body.String()
	if !strings.Contains(body, "prs") {
		t.Error("Response should contain 'prs' field")
	}
}