package test import ( "RenewCMS/api/pkg/apperr" "RenewCMS/api/pkg/authtoken" "net/http" "net/http/httptest" "testing" ) type authTokenProbe struct { reached bool claims authtoken.Claims err error } func authTokenRouter(auth *authtoken.Auth, probe *authTokenProbe, guarded bool) http.Handler { final := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { probe.reached = true probe.claims, probe.err = authtoken.FromContext(r.Context()) w.WriteHeader(http.StatusOK) }) if guarded { return authtoken.Verifier(auth)(authtoken.Authenticator(final)) } return authtoken.Verifier(auth)(final) } func serveAuthToken(handler http.Handler, configure func(*http.Request)) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, "/", nil) configure(req) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) return rec } func TestAuthTokenVerifierAndAuthenticator(t *testing.T) { auth := authtoken.New([]byte("test-secret")) valid, _ := auth.Encode(map[string]any{"user_id": uint32(5)}) other, _ := auth.Encode(map[string]any{"user_id": uint32(9)}) t.Run("A valid jwt cookie reaches the handler with its claims", func(t *testing.T) { probe := &authTokenProbe{} rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) { r.AddCookie(&http.Cookie{Name: "jwt", Value: valid}) }) if rec.Code != http.StatusOK || probe.claims["user_id"] != float64(5) { t.Errorf("expected 200 with user 5, got %d and %v", rec.Code, probe.claims) } }) t.Run("A bearer header in any letter case is accepted", func(t *testing.T) { for _, scheme := range []string{"Bearer ", "BEARER ", "bearer "} { probe := &authTokenProbe{} rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) { r.Header.Set("Authorization", scheme+valid) }) if rec.Code != http.StatusOK { t.Errorf("scheme %q: expected 200, got %d", scheme, rec.Code) } } }) t.Run("The bearer header wins over the cookie", func(t *testing.T) { probe := &authTokenProbe{} serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) { r.Header.Set("Authorization", "Bearer "+valid) r.AddCookie(&http.Cookie{Name: "jwt", Value: other}) }) if probe.claims["user_id"] != float64(5) { t.Errorf("expected the header token, got %v", probe.claims) } }) t.Run("A malformed Authorization header falls back to the cookie", func(t *testing.T) { for _, header := range []string{"Bearer", "Bearer ", "Basic abc", "Token " + valid} { probe := &authTokenProbe{} rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) { r.Header.Set("Authorization", header) r.AddCookie(&http.Cookie{Name: "jwt", Value: valid}) }) if rec.Code != http.StatusOK { t.Errorf("header %q: expected the cookie to be used, got %d", header, rec.Code) } } }) t.Run("The authenticator answers 401 with the standard message when no token is sent", func(t *testing.T) { probe := &authTokenProbe{} rec := serveAuthToken(authTokenRouter(auth, probe, true), func(*http.Request) {}) if rec.Code != http.StatusUnauthorized || probe.reached { t.Fatalf("expected a blocked 401, got %d reached=%v", rec.Code, probe.reached) } if got := rec.Body.String(); got != apperr.UnauthorizedErrorMessage+"\n" { t.Errorf("unexpected body %q", got) } }) t.Run("The authenticator rejects a forged token", func(t *testing.T) { probe := &authTokenProbe{} forged, _ := authtoken.New([]byte("another-secret")).Encode(map[string]any{"user_id": 1}) rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) { r.AddCookie(&http.Cookie{Name: "jwt", Value: forged}) }) if rec.Code != http.StatusUnauthorized || probe.reached { t.Errorf("expected a blocked 401, got %d reached=%v", rec.Code, probe.reached) } }) t.Run("Without the authenticator the handler sees the verification error", func(t *testing.T) { probe := &authTokenProbe{} serveAuthToken(authTokenRouter(auth, probe, false), func(r *http.Request) { r.AddCookie(&http.Cookie{Name: "jwt", Value: "garbage"}) }) if !probe.reached || probe.err == nil || probe.claims != nil { t.Errorf("expected an error and no claims, got %v / %v", probe.claims, probe.err) } }) t.Run("A context that never passed the verifier reports no token", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) if _, err := authtoken.FromContext(req.Context()); err != authtoken.ErrNoToken { t.Errorf("expected ErrNoToken, got %v", err) } }) t.Run("A context carrying nil claims and no error still reports no token", func(t *testing.T) { ctx := authtoken.NewContext(httptest.NewRequest(http.MethodGet, "/", nil).Context(), nil, nil) if _, err := authtoken.FromContext(ctx); err != authtoken.ErrNoToken { t.Errorf("expected ErrNoToken, got %v", err) } }) t.Run("A context injected with claims is authenticated", func(t *testing.T) { probe := &authTokenProbe{} handler := authtoken.Authenticator(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { probe.reached = true })) req := httptest.NewRequest(http.MethodGet, "/", nil) req = req.WithContext(authtoken.NewContext(req.Context(), authtoken.Claims{"user_id": float64(1)}, nil)) handler.ServeHTTP(httptest.NewRecorder(), req) if !probe.reached { t.Error("expected the handler to be reached") } }) }