package client import ( "encoding/json" "net/http" "net/http/httptest" "testing" "completed/internal/model" ) func newTestServer(t *testing.T, handler http.HandlerFunc) (*Client, *httptest.Server) { t.Helper() srv := httptest.NewServer(handler) t.Cleanup(srv.Close) return New(srv.URL), srv } func writeJSON(t *testing.T, w http.ResponseWriter, status int, v any) { t.Helper() w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { t.Fatalf("encode response: %v", err) } } func TestLoginSuccessStoresToken(t *testing.T) { c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/api/auth/login" { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } var in model.LoginInput if err := json.NewDecoder(r.Body).Decode(&in); err != nil { t.Fatalf("decode body: %v", err) } if in.Email != "alice@example.com" || in.Password != "secret" { t.Fatalf("unexpected credentials: %+v", in) } writeJSON(t, w, http.StatusOK, model.AuthResponse{ Token: "tok-123", User: model.User{ID: "u1", Email: in.Email, Name: "Alice"}, }) }) resp, err := c.Login(t.Context(), "alice@example.com", "secret") if err != nil { t.Fatalf("Login: %v", err) } if resp.Token != "tok-123" { t.Errorf("Token = %q, want tok-123", resp.Token) } if c.Token != "tok-123" { t.Errorf("Client.Token = %q, want tok-123 (Login must store it)", c.Token) } } func TestLoginFailureDoesNotStoreToken(t *testing.T) { c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusUnauthorized, map[string]any{ "title": "Unauthorized", "status": 401, "detail": "invalid email or password", }) }) c.Token = "" // sanity _, err := c.Login(t.Context(), "alice@example.com", "wrong") if err == nil { t.Fatal("expected an error, got nil") } apiErr, ok := err.(*APIError) if !ok { t.Fatalf("expected *APIError, got %T: %v", err, err) } if apiErr.Status != http.StatusUnauthorized { t.Errorf("Status = %d, want 401", apiErr.Status) } if apiErr.Error() != "invalid email or password" { t.Errorf("Error() = %q, want the detail message", apiErr.Error()) } if c.Token != "" { t.Errorf("Token = %q, want empty after a failed login", c.Token) } } func TestListGamesSendsBearerToken(t *testing.T) { c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Authorization"); got != "Bearer tok-abc" { t.Errorf("Authorization header = %q, want %q", got, "Bearer tok-abc") } writeJSON(t, w, http.StatusOK, []model.Game{ {ID: "g1", Name: "Doom", PlatformName: "PC", Score: 95, ReleaseDate: "1993-12-10"}, }) }) c.Token = "tok-abc" games, err := c.ListGames(t.Context()) if err != nil { t.Fatalf("ListGames: %v", err) } if len(games) != 1 || games[0].Name != "Doom" { t.Errorf("games = %+v, want one game named Doom", games) } } func TestListGamesUnauthorized(t *testing.T) { c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusUnauthorized, map[string]any{ "title": "Unauthorized", "status": 401, "detail": "authentication required", }) }) // No token set. _, err := c.ListGames(t.Context()) apiErr, ok := err.(*APIError) if !ok { t.Fatalf("expected *APIError, got %T: %v", err, err) } if apiErr.Status != http.StatusUnauthorized { t.Errorf("Status = %d, want 401", apiErr.Status) } } func TestCreateGameSendsBodyAndParsesResponse(t *testing.T) { c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/api/games" { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } var in model.GameInput if err := json.NewDecoder(r.Body).Decode(&in); err != nil { t.Fatalf("decode body: %v", err) } if in.Name != "Half-Life" || in.Score != 96 { t.Fatalf("unexpected body: %+v", in) } writeJSON(t, w, http.StatusOK, model.Game{ ID: "g2", Name: in.Name, PlatformID: in.PlatformID, Score: in.Score, ReleaseDate: in.ReleaseDate, }) }) g, err := c.CreateGame(t.Context(), model.GameInput{ Name: "Half-Life", PlatformID: "p1", Score: 96, ReleaseDate: "1998-11-19", }) if err != nil { t.Fatalf("CreateGame: %v", err) } if g.ID != "g2" { t.Errorf("ID = %q, want g2", g.ID) } } func TestCreatePlatformForbidden(t *testing.T) { c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { writeJSON(t, w, http.StatusForbidden, map[string]any{ "title": "Forbidden", "status": 403, "detail": "admin access required", }) }) c.Token = "tok-non-admin" _, err := c.CreatePlatform(t.Context(), "PC") apiErr, ok := err.(*APIError) if !ok { t.Fatalf("expected *APIError, got %T: %v", err, err) } if apiErr.Status != http.StatusForbidden { t.Errorf("Status = %d, want 403", apiErr.Status) } } func TestLogoutClearsTokenEvenOnError(t *testing.T) { c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) }) c.Token = "tok-abc" _ = c.Logout(t.Context()) if c.Token != "" { t.Errorf("Token = %q, want empty after Logout regardless of the response", c.Token) } } func TestDefaultBaseURL(t *testing.T) { c := New("") if c.BaseURL != DefaultBaseURL { t.Errorf("BaseURL = %q, want %q", c.BaseURL, DefaultBaseURL) } }