Tuas W06
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"5803025020/w04/internal/models"
|
||||
)
|
||||
|
||||
// SessionLookup is the small interface RequireAuth needs to resolve a bearer
|
||||
// token to the session (and therefore the user id) it belongs to. Defined
|
||||
// here (the consumer) so internal/store never needs to import middleware.
|
||||
// *store.SessionStore satisfies this implicitly via its existing Get method.
|
||||
type SessionLookup interface {
|
||||
Get(ctx context.Context, token string) (*models.Session, error)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user