package handler_test import ( "database/sql" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" _ "github.com/mattn/go-sqlite3" migrate "krafttrainer/internal/migrate" "krafttrainer/internal/handler" "krafttrainer/internal/store" "krafttrainer/migrations" ) // --------------------------------------------------------------------------- // Test infrastructure // --------------------------------------------------------------------------- func newHandlerWithStore(t *testing.T) (*handler.Handler, *store.Store) { t.Helper() f, err := os.CreateTemp("", "krafttrainer-handler-test-*.db") if err != nil { t.Fatalf("create temp db file: %v", err) } f.Close() dbPath := f.Name() s, err := store.New(dbPath) if err != nil { os.Remove(dbPath) t.Fatalf("store.New: %v", err) } t.Cleanup(func() { s.Close() os.Remove(dbPath) }) if err := migrate.Run(s.DB(), migrations.FS); err != nil { t.Fatalf("migrate.Run: %v", err) } return handler.New(s), s } func seedUserH(t *testing.T, db *sql.DB, name string) int64 { t.Helper() res, err := db.Exec(`INSERT INTO users (name) VALUES (?)`, name) if err != nil { t.Fatalf("seedUser %q: %v", name, err) } id, _ := res.LastInsertId() return id } func seedTrainingSetH(t *testing.T, db *sql.DB, name string, userID int64) int64 { t.Helper() res, err := db.Exec(`INSERT INTO training_sets (name, user_id) VALUES (?, ?)`, name, userID) if err != nil { t.Fatalf("seedTrainingSet %q: %v", name, err) } id, _ := res.LastInsertId() return id } func seedExerciseH(t *testing.T, db *sql.DB, name string, userID int64) int64 { t.Helper() res, err := db.Exec( `INSERT INTO exercises (name, muscle_group, weight_step_kg, user_id) VALUES (?, 'brust', 2.5, ?)`, name, userID, ) if err != nil { t.Fatalf("seedExercise %q: %v", name, err) } id, _ := res.LastInsertId() return id } func openSessionH(t *testing.T, db *sql.DB, setID, userID int64) int64 { t.Helper() res, err := db.Exec(`INSERT INTO sessions (set_id, user_id) VALUES (?, ?)`, setID, userID) if err != nil { t.Fatalf("openSession: %v", err) } id, _ := res.LastInsertId() return id } // requestWithUserID builds a request for path with an optional X-User-ID header. func requestWithUserID(method, path string, userIDHeader string) *http.Request { r := httptest.NewRequest(method, path, nil) if userIDHeader != "" { r.Header.Set("X-User-ID", userIDHeader) } return r } // decodeBody parses the JSON response body into dst. func decodeBody(t *testing.T, rec *httptest.ResponseRecorder, dst any) { t.Helper() if err := json.NewDecoder(rec.Body).Decode(dst); err != nil { t.Fatalf("decode response body: %v", err) } } // newMux registers all handler routes on a fresh ServeMux so that Go 1.22+ // pattern matching (including the /active literal segment) is active. func newMux(h *handler.Handler) *http.ServeMux { mux := http.NewServeMux() h.RegisterRoutes(mux) return mux } // --------------------------------------------------------------------------- // handleGetActiveSession tests // --------------------------------------------------------------------------- func TestHandleGetActiveSession_MissingUserIDHeader(t *testing.T) { h, _ := newHandlerWithStore(t) mux := newMux(h) rec := httptest.NewRecorder() mux.ServeHTTP(rec, requestWithUserID("GET", "/api/v1/sessions/active", "")) if rec.Code != http.StatusBadRequest { t.Errorf("status: want 400, got %d", rec.Code) } } func TestHandleGetActiveSession_InvalidUserIDHeader(t *testing.T) { h, _ := newHandlerWithStore(t) mux := newMux(h) for _, bad := range []string{"not-a-number", "0", "-1"} { t.Run(bad, func(t *testing.T) { rec := httptest.NewRecorder() mux.ServeHTTP(rec, requestWithUserID("GET", "/api/v1/sessions/active", bad)) if rec.Code != http.StatusBadRequest { t.Errorf("header %q: status want 400, got %d", bad, rec.Code) } }) } } func TestHandleGetActiveSession_ReturnsNullWhenNoActiveSession(t *testing.T) { h, s := newHandlerWithStore(t) mux := newMux(h) userID := seedUserH(t, s.DB(), "NoSession") rec := httptest.NewRecorder() mux.ServeHTTP(rec, requestWithUserID("GET", "/api/v1/sessions/active", fmt.Sprintf("%d", userID))) if rec.Code != http.StatusOK { t.Fatalf("status: want 200, got %d", rec.Code) } // writeJSON(w, 200, nil) encodes the Go nil interface value as JSON "null". body := strings.TrimSpace(rec.Body.String()) if body != "null" { t.Errorf("body: want %q, got %q", "null", body) } } func TestHandleGetActiveSession_ReturnsSessionAndExercises(t *testing.T) { h, s := newHandlerWithStore(t) mux := newMux(h) uid := seedUserH(t, s.DB(), "ActiveUser") setID := seedTrainingSetH(t, s.DB(), "Push Tag", uid) exID := seedExerciseH(t, s.DB(), "Bankdrücken", uid) if _, err := s.DB().Exec( `INSERT INTO set_exercises (set_id, exercise_id, position) VALUES (?, ?, 0)`, setID, exID, ); err != nil { t.Fatalf("link exercise to set: %v", err) } sessionID := openSessionH(t, s.DB(), setID, uid) rec := httptest.NewRecorder() mux.ServeHTTP(rec, requestWithUserID("GET", "/api/v1/sessions/active", fmt.Sprintf("%d", uid))) if rec.Code != http.StatusOK { t.Fatalf("status: want 200, got %d; body: %s", rec.Code, rec.Body) } var resp struct { Session struct { ID int64 `json:"id"` SetID int64 `json:"set_id"` SetName string `json:"set_name"` } `json:"session"` Exercises []struct { ID int64 `json:"id"` Name string `json:"name"` } `json:"exercises"` } decodeBody(t, rec, &resp) if resp.Session.ID != sessionID { t.Errorf("session.id: want %d, got %d", sessionID, resp.Session.ID) } if resp.Session.SetID != setID { t.Errorf("session.set_id: want %d, got %d", setID, resp.Session.SetID) } if resp.Session.SetName != "Push Tag" { t.Errorf("session.set_name: want %q, got %q", "Push Tag", resp.Session.SetName) } if len(resp.Exercises) != 1 { t.Fatalf("exercises: want 1, got %d", len(resp.Exercises)) } if resp.Exercises[0].ID != exID { t.Errorf("exercise.id: want %d, got %d", exID, resp.Exercises[0].ID) } if resp.Exercises[0].Name != "Bankdrücken" { t.Errorf("exercise.name: want %q, got %q", "Bankdrücken", resp.Exercises[0].Name) } } func TestHandleGetActiveSession_ExercisesEmptyWhenSetHasNone(t *testing.T) { h, s := newHandlerWithStore(t) mux := newMux(h) uid := seedUserH(t, s.DB(), "NoExUser") setID := seedTrainingSetH(t, s.DB(), "Leeres Set", uid) openSessionH(t, s.DB(), setID, uid) rec := httptest.NewRecorder() mux.ServeHTTP(rec, requestWithUserID("GET", "/api/v1/sessions/active", fmt.Sprintf("%d", uid))) if rec.Code != http.StatusOK { t.Fatalf("status: want 200, got %d; body: %s", rec.Code, rec.Body) } var resp struct { Exercises []json.RawMessage `json:"exercises"` } decodeBody(t, rec, &resp) if len(resp.Exercises) != 0 { t.Errorf("exercises: want empty slice, got %d items", len(resp.Exercises)) } } func TestHandleGetActiveSession_ContentTypeIsJSON(t *testing.T) { h, s := newHandlerWithStore(t) mux := newMux(h) uid := seedUserH(t, s.DB(), "CTUser") rec := httptest.NewRecorder() mux.ServeHTTP(rec, requestWithUserID("GET", "/api/v1/sessions/active", fmt.Sprintf("%d", uid))) ct := rec.Header().Get("Content-Type") if !strings.HasPrefix(ct, "application/json") { t.Errorf("Content-Type: want application/json, got %q", ct) } }