Merge branch 'main' into 450-template-queries

This commit is contained in:
Cliff Hall 2025-06-04 14:54:57 -04:00 • committed by GitHub
commit cc28a891e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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"),
);
});
});
});