package middleware import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "5803025015/w04/internal/models" ) // ---------- hand-written fake ---------- // fakeSessionStore is a hand-written in-memory SessionStore for tests. type fakeSessionStore struct { sessions map[string]*models.Session err error // if set, Get returns this error } func newFakeSessionStore() *fakeSessionStore { return &fakeSessionStore{sessions: make(map[string]*models.Session)} } func (f *fakeSessionStore) Get(ctx context.Context, token string) (*models.Session, error) { if f.err != nil { return nil, f.err } s, ok := f.sessions[token] if !ok { return nil, errors.New("not found") } return s, nil } // ---------- helpers ---------- // callMiddleware runs RequireAuth around a next handler that records whether // it was called and what user id it saw. func callMiddleware( t *testing.T, store SessionStore, authHeader string, ) (rec *httptest.ResponseRecorder, called *bool, seenUserID *string) { t.Helper() calledVal := false seenID := "" next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calledVal = true seenID = GetUserID(r.Context()) w.WriteHeader(http.StatusOK) }) mw := NewAuthMiddleware(store) handler := mw.RequireAuth(next) req := httptest.NewRequest(http.MethodGet, "/protected", nil) if authHeader != "" { req.Header.Set("Authorization", authHeader) } rec = httptest.NewRecorder() handler.ServeHTTP(rec, req) return rec, &calledVal, &seenID } // assertJSONError asserts that the body is valid JSON and has "error" key. func assertJSONError(t *testing.T, body []byte) { t.Helper() var m map[string]any if err := json.Unmarshal(body, &m); err != nil { t.Fatalf("body is not valid JSON: %v; body=%q", err, string(body)) } if _, ok := m["error"]; !ok { t.Fatalf("expected 'error' key in JSON body; got %v", m) } } // ---------- tests ---------- func TestRequireAuth(t *testing.T) { t.Run("no Authorization header -> 401, next not called", func(t *testing.T) { store := newFakeSessionStore() rec, called, _ := callMiddleware(t, store, "") if rec.Code != http.StatusUnauthorized { t.Fatalf("want 401, got %d", rec.Code) } if *called { t.Fatal("next handler should NOT have been called") } if ct := rec.Header().Get("Content-Type"); ct != "application/json" { t.Fatalf("want Content-Type application/json, got %q", ct) } assertJSONError(t, rec.Body.Bytes()) }) t.Run("Basic scheme -> 401", func(t *testing.T) { store := newFakeSessionStore() rec, called, _ := callMiddleware(t, store, "Basic abc") if rec.Code != http.StatusUnauthorized { t.Fatalf("want 401, got %d", rec.Code) } if *called { t.Fatal("next handler should NOT have been called") } assertJSONError(t, rec.Body.Bytes()) t.Run("RequireAuthFunc wrapper works", func(t *testing.T) { store := newFakeSessionStore() store.sessions["tok"] = &models.Session{Token: "tok", UserID: "u1"} called := false next := func(w http.ResponseWriter, r *http.Request) { called = true if GetUserID(r.Context()) != "u1" { t.Errorf("wrong user id in context") } w.WriteHeader(http.StatusOK) } mw := NewAuthMiddleware(store) h := mw.RequireAuthFunc(next) req := httptest.NewRequest(http.MethodGet, "/x", nil) req.Header.Set("Authorization", "Bearer tok") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK || !called { t.Fatalf("want 200 + called, got %d called=%v", rec.Code, called) } }) t.Run("RequireAuth top-level function works", func(t *testing.T) { store := newFakeSessionStore() store.sessions["tok"] = &models.Session{Token: "tok", UserID: "u1"} called := false next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true w.WriteHeader(http.StatusOK) }) mwFunc := RequireAuth(store) h := mwFunc(next) req := httptest.NewRequest(http.MethodGet, "/x", nil) req.Header.Set("Authorization", "Bearer tok") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK || !called { t.Fatalf("want 200 + called, got %d called=%v", rec.Code, called) } }) }) t.Run("Bearer with empty token -> 401", func(t *testing.T) { store := newFakeSessionStore() rec, called, _ := callMiddleware(t, store, "Bearer ") if rec.Code != http.StatusUnauthorized { t.Fatalf("want 401, got %d", rec.Code) } if *called { t.Fatal("next handler should NOT have been called") } assertJSONError(t, rec.Body.Bytes()) }) t.Run("unknown token -> 401", func(t *testing.T) { store := newFakeSessionStore() rec, called, _ := callMiddleware(t, store, "Bearer not-a-real-token") if rec.Code != http.StatusUnauthorized { t.Fatalf("want 401, got %d", rec.Code) } if *called { t.Fatal("next handler should NOT have been called") } assertJSONError(t, rec.Body.Bytes()) }) t.Run("valid token -> next called with user id in context", func(t *testing.T) { store := newFakeSessionStore() store.sessions["valid-token"] = &models.Session{ Token: "valid-token", UserID: "user-123", } rec, called, seenID := callMiddleware(t, store, "Bearer valid-token") if rec.Code != http.StatusOK { t.Fatalf("want 200, got %d", rec.Code) } if !*called { t.Fatal("next handler SHOULD have been called") } if *seenID != "user-123" { t.Fatalf("want user id 'user-123' in context, got %q", *seenID) } }) t.Run("store error -> 401 with JSON body", func(t *testing.T) { store := newFakeSessionStore() store.err = errors.New("boom") rec, called, _ := callMiddleware(t, store, "Bearer valid-token") if rec.Code != http.StatusUnauthorized { t.Fatalf("want 401, got %d", rec.Code) } if *called { t.Fatal("next handler should NOT have been called") } if ct := rec.Header().Get("Content-Type"); ct != "application/json" { t.Fatalf("want Content-Type application/json, got %q", ct) } assertJSONError(t, rec.Body.Bytes()) }) }