From 03bb303bf9f1c07fba565c7771c5ad5d0c1ae70e Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Thu, 29 May 2025 14:10:32 +0100 Subject: [PATCH] rm loading, prm test --- client/src/App.tsx | 2 - client/src/components/AuthDebugger.tsx | 26 ++-- .../__tests__/AuthDebugger.test.tsx | 123 +++++++++++++++++- client/src/lib/auth-types.ts | 2 - 4 files changed, 131 insertions(+), 22 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 74cdca19..a332f9aa 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -329,8 +329,6 @@ const App = () => { } } catch (error) { console.error("Error loading OAuth tokens:", error); - } finally { - updateAuthState({ loading: false }); } }; diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index b88758d4..9757af95 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -60,23 +60,23 @@ const AuthDebugger = ({ authState, updateAuthState, }: AuthDebuggerProps) => { - // Initialize loading state + // Check for existing tokens on mount useEffect(() => { - if (authState.loading && serverUrl) { - // Check if we have existing tokens + if (serverUrl && !authState.oauthTokens) { const checkTokens = async () => { const provider = new DebugInspectorOAuthClientProvider(serverUrl); const existingTokens = await provider.tokens(); - - updateAuthState({ - loading: false, - oauthTokens: existingTokens || null, - }); + if (existingTokens) { + updateAuthState({ + oauthTokens: existingTokens, + oauthStep: "complete", + }); + } }; - checkTokens(); } - }, [serverUrl, authState.loading, updateAuthState]); + }, [serverUrl]); // Only run when serverUrl changes + const startOAuthFlow = useCallback(() => { if (!serverUrl) { updateAuthState({ @@ -241,10 +241,7 @@ const AuthDebugger = ({ )} - {authState.loading ? ( -

Loading authentication status...

- ) : ( -
+
{authState.oauthTokens && (

Access Token:

@@ -286,7 +283,6 @@ const AuthDebugger = ({ the standard automatic flow.

- )}
{ mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); mockRegisterClient.mockResolvedValue(mockOAuthClientInfo); - mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(null); + mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue( + new Error("No protected resource metadata found") + ); mockStartAuthorization.mockImplementation(async (_sseUrl, options) => { const authUrl = new URL("https://oauth.example.com/authorize"); @@ -448,7 +450,7 @@ describe("AuthDebugger", () => { await act(async () => { renderAuthDebugger({ updateAuthState, - authState: { ...defaultAuthState, loading: false }, + authState: { ...defaultAuthState }, }); }); @@ -471,7 +473,7 @@ describe("AuthDebugger", () => { ).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE); expect(storedStateCall).toBeDefined(); - const storedState = JSON.parse(storedStateCall![1]); + const storedState = JSON.parse(storedStateCall![1] as string); expect(storedState).toMatchObject({ oauthStep: "authorization_code", @@ -487,4 +489,119 @@ describe("AuthDebugger", () => { }); }); }); + + describe("OAuth Protected Resource Metadata", () => { + it("should successfully fetch and display protected resource metadata", async () => { + const updateAuthState = jest.fn(); + const mockResourceMetadata = { + resource: "https://example.com/api", + authorization_servers: ["https://custom-auth.example.com"], + bearer_methods_supported: ["header", "body"], + resource_documentation: "https://example.com/api/docs", + resource_policy_uri: "https://example.com/api/policy", + }; + + // Mock successful metadata discovery + mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue( + mockResourceMetadata + ); + mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { ...defaultAuthState }, + }); + }); + + // Click Guided OAuth Flow to start the process + await act(async () => { + fireEvent.click(screen.getByText("Guided OAuth Flow")); + }); + + // Verify that the flow started with metadata discovery + expect(updateAuthState).toHaveBeenCalledWith({ + oauthStep: "metadata_discovery", + authorizationUrl: null, + statusMessage: null, + latestError: null, + }); + + // Click Continue to trigger metadata discovery + const continueButton = await screen.findByText("Continue"); + await act(async () => { + fireEvent.click(continueButton); + }); + + // Wait for the metadata to be fetched + await waitFor(() => { + expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith( + "https://example.com" + ); + }); + + // Verify the state was updated with the resource metadata + await waitFor(() => { + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + resourceMetadata: mockResourceMetadata, + authServerUrl: new URL("https://custom-auth.example.com"), + oauthStep: "client_registration", + }) + ); + }); + }); + + it("should handle protected resource metadata fetch failure gracefully", async () => { + const updateAuthState = jest.fn(); + const mockError = new Error("Failed to fetch resource metadata"); + + // Mock failed metadata discovery + mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError); + // But OAuth metadata should still work with the original URL + mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { ...defaultAuthState }, + }); + }); + + // Click Guided OAuth Flow + await act(async () => { + fireEvent.click(screen.getByText("Guided OAuth Flow")); + }); + + // Click Continue to trigger metadata discovery + const continueButton = await screen.findByText("Continue"); + await act(async () => { + fireEvent.click(continueButton); + }); + + // Wait for the metadata fetch to fail + await waitFor(() => { + expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith( + "https://example.com" + ); + }); + + // Verify the flow continues despite the error + await waitFor(() => { + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + resourceMetadataError: mockError, + // Should use the original server URL as fallback + authServerUrl: new URL("https://example.com"), + oauthStep: "client_registration", + }) + ); + }); + + // Verify that regular OAuth metadata discovery was still called + expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( + new URL("https://example.com") + ); + }); + }); }); diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts index e69dcd12..daf1bc15 100644 --- a/client/src/lib/auth-types.ts +++ b/client/src/lib/auth-types.ts @@ -27,7 +27,6 @@ export interface StatusMessage { export interface AuthDebuggerState { isInitiatingAuth: boolean; oauthTokens: OAuthTokens | null; - loading: boolean; oauthStep: OAuthStep; resourceMetadata: OAuthProtectedResourceMetadata | null; resourceMetadataError: @@ -47,7 +46,6 @@ export interface AuthDebuggerState { export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = { isInitiatingAuth: false, oauthTokens: null, - loading: true, oauthStep: "metadata_discovery", oauthMetadata: null, resourceMetadata: null,