package middleware import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "5803025020/w04/internal/models" ) // fakeSessionLookup is a hand-written in-memory fake satisfying SessionLookup. type fakeSessionLookup struct { sessions map[string]*models.Session } func newFakeSessionLookup() *fakeSessionLookup { return &fakeSessionLookup{sessions: make(map[string]*models.Session)} } func (f *fakeSessionLookup) add(token, userID string) { f.sessions[token] = &models.Session{Token: token, UserID: userID, CreatedAt: time.Now()} } var errFakeSessionNotFound = errors.New("fake: session not found") func (f *fakeSessionLookup) Get(ctx context.Context, token string) (*models.Session, error) { sess, ok := f.sessions[token] if !ok { return nil, errFakeSessionNotFound } return sess, nil } // newRecordingNext returns a next handler that records whether it was called // and what user id was readable from the request context inside it. func newRecordingNext(called *bool, gotUserID *string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { *called = true *gotUserID = GetUserID(r.Context()) w.WriteHeader(http.StatusOK) }) } func assertJSONErrorResponse(t *testing.T, rec *httptest.ResponseRecorder) { t.Helper() ct := rec.Header().Get("Content-Type") if !strings.HasPrefix(ct, "application/json") { t.Fatalf("expected Content-Type application/json, got %q", ct) } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("expected response body to parse as JSON: %v (body=%s)", err, rec.Body.String()) } } func TestRequireAuth(t *testing.T) { fake := newFakeSessionLookup() fake.add("validtoken1234567890123456789012", "user-123") mw := NewAuthMiddleware(fake) t.Run("no Authorization header returns 401 and next is not called", func(t *testing.T) { called := false var gotUserID string rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) mw.RequireAuth(newRecordingNext(&called, &gotUserID)).ServeHTTP(rec, req) if called { t.Fatal("expected next handler NOT to be called") } if rec.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", rec.Code) } assertJSONErrorResponse(t, rec) }) t.Run("Basic scheme returns 401", func(t *testing.T) { called := false var gotUserID string rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Basic abc") mw.RequireAuth(newRecordingNext(&called, &gotUserID)).ServeHTTP(rec, req) if called { t.Fatal("expected next handler NOT to be called") } if rec.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", rec.Code) } }) t.Run("Bearer with empty token returns 401", func(t *testing.T) { called := false var gotUserID string rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Bearer ") mw.RequireAuth(newRecordingNext(&called, &gotUserID)).ServeHTTP(rec, req) if called { t.Fatal("expected next handler NOT to be called") } if rec.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", rec.Code) } }) t.Run("unknown token returns 401", func(t *testing.T) { called := false var gotUserID string rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Bearer not-a-real-token-000000000000") mw.RequireAuth(newRecordingNext(&called, &gotUserID)).ServeHTTP(rec, req) if called { t.Fatal("expected next handler NOT to be called") } if rec.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", rec.Code) } }) t.Run("valid token calls next handler with the authenticated user id in context", func(t *testing.T) { called := false var gotUserID string rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Bearer validtoken1234567890123456789012") mw.RequireAuth(newRecordingNext(&called, &gotUserID)).ServeHTTP(rec, req) if !called { t.Fatal("expected next handler to be called") } if gotUserID != "user-123" { t.Fatalf("expected user id %q inside next handler, got %q", "user-123", gotUserID) } if rec.Code != http.StatusOK { t.Fatalf("expected 200 from next handler, got %d", rec.Code) } }) t.Run("401 responses carry Content-Type application/json and a body that parses as JSON", func(t *testing.T) { called := false var gotUserID string rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Bearer not-a-real-token-000000000000") mw.RequireAuth(newRecordingNext(&called, &gotUserID)).ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", rec.Code) } assertJSONErrorResponse(t, rec) }) }