Files
2026-10-03 20:28:25 +07:00

163 lines
4.9 KiB
Go

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)
})
}