163 lines
4.9 KiB
Go
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)
|
|
})
|
|
}
|