218 lines
5.9 KiB
Go
218 lines
5.9 KiB
Go
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())
|
|
})
|
|
}
|