Merge branch 'main' into 450-template-queries
This commit is contained in:
commit
cc28a891e9
4 changed files with 500 additions and 162 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useMemo, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DebugInspectorOAuthClientProvider } from "../lib/auth";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { AuthDebuggerState } from "../lib/auth-types";
|
||||
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types";
|
||||
import { OAuthFlowProgress } from "./OAuthFlowProgress";
|
||||
import { OAuthStateMachine } from "../lib/oauth-state-machine";
|
||||
import { SESSION_KEYS } from "../lib/constants";
|
||||
|
||||
export interface AuthDebuggerProps {
|
||||
serverUrl: string;
|
||||
|
|
@ -59,6 +60,27 @@ const AuthDebugger = ({
|
|||
authState,
|
||||
updateAuthState,
|
||||
}: AuthDebuggerProps) => {
|
||||
// Check for existing tokens on mount
|
||||
useEffect(() => {
|
||||
if (serverUrl && !authState.oauthTokens) {
|
||||
const checkTokens = async () => {
|
||||
try {
|
||||
const provider = new DebugInspectorOAuthClientProvider(serverUrl);
|
||||
const existingTokens = await provider.tokens();
|
||||
if (existingTokens) {
|
||||
updateAuthState({
|
||||
oauthTokens: existingTokens,
|
||||
oauthStep: "complete",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load existing OAuth tokens:", error);
|
||||
}
|
||||
};
|
||||
checkTokens();
|
||||
}
|
||||
}, [serverUrl, updateAuthState, authState.oauthTokens]);
|
||||
|
||||
const startOAuthFlow = useCallback(() => {
|
||||
if (!serverUrl) {
|
||||
updateAuthState({
|
||||
|
|
@ -141,6 +163,11 @@ const AuthDebugger = ({
|
|||
currentState.oauthStep === "authorization_code" &&
|
||||
currentState.authorizationUrl
|
||||
) {
|
||||
// Store the current auth state before redirecting
|
||||
sessionStorage.setItem(
|
||||
SESSION_KEYS.AUTH_DEBUGGER_STATE,
|
||||
JSON.stringify(currentState),
|
||||
);
|
||||
// Open the authorization URL automatically
|
||||
window.location.href = currentState.authorizationUrl;
|
||||
break;
|
||||
|
|
@ -178,13 +205,7 @@ const AuthDebugger = ({
|
|||
);
|
||||
serverAuthProvider.clear();
|
||||
updateAuthState({
|
||||
oauthTokens: null,
|
||||
oauthStep: "metadata_discovery",
|
||||
latestError: null,
|
||||
oauthClientInfo: null,
|
||||
authorizationCode: "",
|
||||
validationError: null,
|
||||
oauthMetadata: null,
|
||||
...EMPTY_DEBUGGER_STATE,
|
||||
statusMessage: {
|
||||
type: "success",
|
||||
message: "OAuth tokens cleared successfully",
|
||||
|
|
@ -224,52 +245,48 @@ const AuthDebugger = ({
|
|||
<StatusMessage message={authState.statusMessage} />
|
||||
)}
|
||||
|
||||
{authState.loading ? (
|
||||
<p>Loading authentication status...</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{authState.oauthTokens && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Access Token:</p>
|
||||
<div className="bg-muted p-2 rounded-md text-xs overflow-x-auto">
|
||||
{authState.oauthTokens.access_token.substring(0, 25)}...
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{authState.oauthTokens && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Access Token:</p>
|
||||
<div className="bg-muted p-2 rounded-md text-xs overflow-x-auto">
|
||||
{authState.oauthTokens.access_token.substring(0, 25)}...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={startOAuthFlow}
|
||||
disabled={authState.isInitiatingAuth}
|
||||
>
|
||||
{authState.oauthTokens
|
||||
? "Guided Token Refresh"
|
||||
: "Guided OAuth Flow"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleQuickOAuth}
|
||||
disabled={authState.isInitiatingAuth}
|
||||
>
|
||||
{authState.isInitiatingAuth
|
||||
? "Initiating..."
|
||||
: authState.oauthTokens
|
||||
? "Quick Refresh"
|
||||
: "Quick OAuth Flow"}
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onClick={handleClearOAuth}>
|
||||
Clear OAuth State
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose "Guided" for step-by-step instructions or "Quick" for
|
||||
the standard automatic flow.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={startOAuthFlow}
|
||||
disabled={authState.isInitiatingAuth}
|
||||
>
|
||||
{authState.oauthTokens
|
||||
? "Guided Token Refresh"
|
||||
: "Guided OAuth Flow"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleQuickOAuth}
|
||||
disabled={authState.isInitiatingAuth}
|
||||
>
|
||||
{authState.isInitiatingAuth
|
||||
? "Initiating..."
|
||||
: authState.oauthTokens
|
||||
? "Quick Refresh"
|
||||
: "Quick OAuth Flow"}
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onClick={handleClearOAuth}>
|
||||
Clear OAuth State
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose "Guided" for step-by-step instructions or "Quick" for
|
||||
the standard automatic flow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OAuthFlowProgress
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ import {
|
|||
generateOAuthErrorDescription,
|
||||
parseOAuthCallbackParams,
|
||||
} from "@/utils/oauthUtils.ts";
|
||||
import { AuthDebuggerState } from "@/lib/auth-types";
|
||||
|
||||
interface OAuthCallbackProps {
|
||||
onConnect: ({
|
||||
authorizationCode,
|
||||
errorMsg,
|
||||
restoredState,
|
||||
}: {
|
||||
authorizationCode?: string;
|
||||
errorMsg?: string;
|
||||
restoredState?: AuthDebuggerState;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
|
|
@ -35,6 +38,21 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => {
|
|||
|
||||
const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);
|
||||
|
||||
// Try to restore the auth state
|
||||
const storedState = sessionStorage.getItem(
|
||||
SESSION_KEYS.AUTH_DEBUGGER_STATE,
|
||||
);
|
||||
let restoredState = null;
|
||||
if (storedState) {
|
||||
try {
|
||||
restoredState = JSON.parse(storedState);
|
||||
// Clean up the stored state
|
||||
sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse stored auth state:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ServerURL isn't set, this can happen if we've opened the
|
||||
// authentication request in a new tab, so we don't have the same
|
||||
// session storage
|
||||
|
|
@ -50,8 +68,8 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => {
|
|||
}
|
||||
|
||||
// Instead of storing in sessionStorage, pass the code directly
|
||||
// to the auth state manager through onConnect
|
||||
onConnect({ authorizationCode: params.code });
|
||||
// to the auth state manager through onConnect, along with restored state
|
||||
onConnect({ authorizationCode: params.code, restoredState });
|
||||
};
|
||||
|
||||
handleCallback().finally(() => {
|
||||
|
|
|
|||
|
|
@ -128,15 +128,86 @@ export const OAuthFlowProgress = ({
|
|||
label="Metadata Discovery"
|
||||
{...getStepProps("metadata_discovery")}
|
||||
>
|
||||
{provider.getServerMetadata() && (
|
||||
{authState.oauthMetadata && (
|
||||
<details className="text-xs mt-2">
|
||||
<summary className="cursor-pointer text-muted-foreground font-medium">
|
||||
Retrieved OAuth Metadata from {serverUrl}
|
||||
/.well-known/oauth-authorization-server
|
||||
OAuth Metadata Sources
|
||||
{!authState.resourceMetadata && " ℹ️"}
|
||||
</summary>
|
||||
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
|
||||
{JSON.stringify(provider.getServerMetadata(), null, 2)}
|
||||
</pre>
|
||||
|
||||
{authState.resourceMetadata && (
|
||||
<div className="mt-2">
|
||||
<p className="font-medium">Resource Metadata:</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
From{" "}
|
||||
{
|
||||
new URL(
|
||||
"/.well-known/oauth-protected-resource",
|
||||
serverUrl,
|
||||
).href
|
||||
}
|
||||
</p>
|
||||
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
|
||||
{JSON.stringify(authState.resourceMetadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authState.resourceMetadataError && (
|
||||
<div className="mt-2 p-3 border border-blue-300 bg-blue-50 rounded-md">
|
||||
<p className="text-sm font-medium text-blue-700">
|
||||
ℹ️ No resource metadata available from{" "}
|
||||
<a
|
||||
href={
|
||||
new URL(
|
||||
"/.well-known/oauth-protected-resource",
|
||||
serverUrl,
|
||||
).href
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
{
|
||||
new URL(
|
||||
"/.well-known/oauth-protected-resource",
|
||||
serverUrl,
|
||||
).href
|
||||
}
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
Resource metadata was added in the{" "}
|
||||
<a href="https://modelcontextprotocol.io/specification/draft/basic/authorization#2-3-1-authorization-server-location">
|
||||
2025-DRAFT-v2 specification update
|
||||
</a>
|
||||
<br />
|
||||
{authState.resourceMetadataError.message}
|
||||
{authState.resourceMetadataError instanceof TypeError &&
|
||||
" (This could indicate the endpoint doesn't exist or does not have CORS configured)"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authState.oauthMetadata && (
|
||||
<div className="mt-2">
|
||||
<p className="font-medium">Authorization Server Metadata:</p>
|
||||
{authState.authServerUrl && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
From{" "}
|
||||
{
|
||||
new URL(
|
||||
"/.well-known/oauth-authorization-server",
|
||||
authState.authServerUrl,
|
||||
).href
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
|
||||
{JSON.stringify(authState.oauthMetadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</details>
|
||||
)}
|
||||
</OAuthStepDetails>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
|||
registerClient: jest.fn(),
|
||||
startAuthorization: jest.fn(),
|
||||
exchangeAuthorization: jest.fn(),
|
||||
discoverOAuthProtectedResourceMetadata: jest.fn(),
|
||||
}));
|
||||
|
||||
// Import the functions to get their types
|
||||
|
|
@ -49,8 +50,10 @@ import {
|
|||
startAuthorization,
|
||||
exchangeAuthorization,
|
||||
auth,
|
||||
discoverOAuthProtectedResourceMetadata,
|
||||
} from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
|
||||
|
||||
// Type the mocked functions properly
|
||||
const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction<
|
||||
|
|
@ -66,6 +69,10 @@ const mockExchangeAuthorization = exchangeAuthorization as jest.MockedFunction<
|
|||
typeof exchangeAuthorization
|
||||
>;
|
||||
const mockAuth = auth as jest.MockedFunction<typeof auth>;
|
||||
const mockDiscoverOAuthProtectedResourceMetadata =
|
||||
discoverOAuthProtectedResourceMetadata as jest.MockedFunction<
|
||||
typeof discoverOAuthProtectedResourceMetadata
|
||||
>;
|
||||
|
||||
const sessionStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
|
|
@ -84,19 +91,7 @@ Object.defineProperty(window, "location", {
|
|||
});
|
||||
|
||||
describe("AuthDebugger", () => {
|
||||
const defaultAuthState = {
|
||||
isInitiatingAuth: false,
|
||||
oauthTokens: null,
|
||||
loading: false,
|
||||
oauthStep: "metadata_discovery" as const,
|
||||
oauthMetadata: null,
|
||||
oauthClientInfo: null,
|
||||
authorizationUrl: null,
|
||||
authorizationCode: "",
|
||||
latestError: null,
|
||||
statusMessage: null,
|
||||
validationError: null,
|
||||
};
|
||||
const defaultAuthState = EMPTY_DEBUGGER_STATE;
|
||||
|
||||
const defaultProps = {
|
||||
serverUrl: "https://example.com",
|
||||
|
|
@ -109,8 +104,14 @@ describe("AuthDebugger", () => {
|
|||
jest.clearAllMocks();
|
||||
sessionStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
// Supress
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
|
||||
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
|
||||
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
|
||||
new Error("No protected resource metadata found"),
|
||||
);
|
||||
mockStartAuthorization.mockImplementation(async (_sseUrl, options) => {
|
||||
const authUrl = new URL("https://oauth.example.com/authorize");
|
||||
|
||||
|
|
@ -126,6 +127,10 @@ describe("AuthDebugger", () => {
|
|||
mockExchangeAuthorization.mockResolvedValue(mockOAuthTokens);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderAuthDebugger = (props: Partial<AuthDebuggerProps> = {}) => {
|
||||
const mergedProps = {
|
||||
...defaultProps,
|
||||
|
|
@ -204,7 +209,7 @@ describe("AuthDebugger", () => {
|
|||
|
||||
// Should first discover and save OAuth metadata
|
||||
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
|
||||
"https://example.com",
|
||||
new URL("https://example.com"),
|
||||
);
|
||||
|
||||
// Check that updateAuthState was called with the right info message
|
||||
|
|
@ -316,6 +321,11 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
|
||||
expect(updateAuthState).toHaveBeenCalledWith({
|
||||
authServerUrl: null,
|
||||
authorizationUrl: null,
|
||||
isInitiatingAuth: false,
|
||||
resourceMetadata: null,
|
||||
resourceMetadataError: null,
|
||||
oauthTokens: null,
|
||||
oauthStep: "metadata_discovery",
|
||||
latestError: null,
|
||||
|
|
@ -357,7 +367,7 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
|
||||
expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
|
||||
"https://example.com",
|
||||
new URL("https://example.com"),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -432,4 +442,186 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("OAuth State Persistence", () => {
|
||||
it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => {
|
||||
const updateAuthState = jest.fn();
|
||||
|
||||
// Mock window.location.href setter
|
||||
const originalLocation = window.location;
|
||||
const locationMock = {
|
||||
...originalLocation,
|
||||
href: "",
|
||||
origin: "http://localhost:3000",
|
||||
};
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: locationMock,
|
||||
});
|
||||
|
||||
// Setup mocks for OAuth flow
|
||||
mockStartAuthorization.mockResolvedValue({
|
||||
authorizationUrl: new URL(
|
||||
"https://oauth.example.com/authorize?client_id=test_client_id&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback%2Fdebug",
|
||||
),
|
||||
codeVerifier: "test_verifier",
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
renderAuthDebugger({
|
||||
updateAuthState,
|
||||
authState: { ...defaultAuthState },
|
||||
});
|
||||
});
|
||||
|
||||
// Click Quick OAuth Flow
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText("Quick OAuth Flow"));
|
||||
});
|
||||
|
||||
// Wait for the flow to reach the authorization step
|
||||
await waitFor(() => {
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith(
|
||||
SESSION_KEYS.AUTH_DEBUGGER_STATE,
|
||||
expect.stringContaining('"oauthStep":"authorization_code"'),
|
||||
);
|
||||
});
|
||||
|
||||
// Verify the stored state includes all the accumulated data
|
||||
const storedStateCall = (
|
||||
sessionStorage.setItem as jest.Mock
|
||||
).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE);
|
||||
|
||||
expect(storedStateCall).toBeDefined();
|
||||
const storedState = JSON.parse(storedStateCall![1] as string);
|
||||
|
||||
expect(storedState).toMatchObject({
|
||||
oauthStep: "authorization_code",
|
||||
authorizationUrl: expect.stringMatching(
|
||||
/^https:\/\/oauth\.example\.com\/authorize/,
|
||||
),
|
||||
oauthMetadata: expect.objectContaining({
|
||||
token_endpoint: "https://oauth.example.com/token",
|
||||
}),
|
||||
oauthClientInfo: expect.objectContaining({
|
||||
client_id: "test_client_id",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue