package auth import ( "encoding/json" "errors" "net/http" "net/http/httptest" "sync/atomic" "testing" ) // newTestServer starts an httptest.Server that routes MCIAS endpoints. func newTestServer(t *testing.T, loginHandler, validateHandler http.HandlerFunc) *httptest.Server { t.Helper() mux := http.NewServeMux() if loginHandler != nil { mux.HandleFunc("POST /v1/auth/login", loginHandler) } if validateHandler != nil { mux.HandleFunc("POST /v1/token/validate", validateHandler) } srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return srv } func newTestClient(t *testing.T, serverURL string) *Client { t.Helper() c, err := NewClient(serverURL, "", "mcr-test", []string{"env:test"}) if err != nil { t.Fatalf("NewClient: %v", err) } return c } func TestLoginSuccess(t *testing.T) { srv := newTestServer(t, func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "token": "tok-abc", "expires_at": "2099-01-01T00:00:00Z", }) }, nil) c := newTestClient(t, srv.URL) token, _, err := c.Login("alice", "secret") if err != nil { t.Fatalf("Login: %v", err) } if token != "tok-abc" { t.Fatalf("token: got %q, want %q", token, "tok-abc") } } func TestLoginFailure(t *testing.T) { srv := newTestServer(t, func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"invalid credentials"}`)) }, nil) c := newTestClient(t, srv.URL) _, _, err := c.Login("alice", "wrong") if !errors.Is(err, ErrUnauthorized) { t.Fatalf("Login error: got %v, want %v", err, ErrUnauthorized) } } func TestValidateSuccess(t *testing.T) { srv := newTestServer(t, nil, func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "valid": true, "username": "alice", "roles": []string{"reader", "writer"}, }) }) c := newTestClient(t, srv.URL) claims, err := c.ValidateToken("valid-token-123") if err != nil { t.Fatalf("ValidateToken: %v", err) } if claims.Subject != "alice" { t.Fatalf("subject: got %q, want %q", claims.Subject, "alice") } if len(claims.Roles) != 2 || claims.Roles[0] != "reader" || claims.Roles[1] != "writer" { t.Fatalf("roles: got %v, want [reader writer]", claims.Roles) } } func TestValidateRevoked(t *testing.T) { srv := newTestServer(t, nil, func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{"valid": false}) }) c := newTestClient(t, srv.URL) _, err := c.ValidateToken("revoked-token") if !errors.Is(err, ErrUnauthorized) { t.Fatalf("ValidateToken error: got %v, want %v", err, ErrUnauthorized) } } func TestValidateCacheHit(t *testing.T) { var callCount atomic.Int64 srv := newTestServer(t, nil, func(w http.ResponseWriter, _ *http.Request) { callCount.Add(1) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "valid": true, "username": "bob", "roles": []string{"admin"}, }) }) c := newTestClient(t, srv.URL) // First call — should hit the server. claims1, err := c.ValidateToken("cached-token") if err != nil { t.Fatalf("first ValidateToken: %v", err) } if callCount.Load() != 1 { t.Fatalf("expected 1 server call after first validate, got %d", callCount.Load()) } // Second call — should come from cache (mcdsl/auth handles this). claims2, err := c.ValidateToken("cached-token") if err != nil { t.Fatalf("second ValidateToken: %v", err) } if callCount.Load() != 1 { t.Fatalf("expected 1 server call after second validate (cache hit), got %d", callCount.Load()) } if claims1.Subject != claims2.Subject { t.Fatalf("cached claims mismatch: %q vs %q", claims1.Subject, claims2.Subject) } }