Skip to content

Commit 9f6aa38

Browse files
author
itsuki
committed
add/modify tests for new added auth functions
1 parent 3c69e5d commit 9f6aa38

File tree

1 file changed

+164
-0
lines changed

1 file changed

+164
-0
lines changed

src/client/auth.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
exchangeAuthorization,
55
refreshAuthorization,
66
registerClient,
7+
discoverOAuthProtectedResourceMetadata,
8+
extractResourceMetadataUrl,
79
} from "./auth.js";
810

911
// Mock fetch globally
@@ -15,6 +17,168 @@ describe("OAuth Authorization", () => {
1517
mockFetch.mockReset();
1618
});
1719

20+
describe("extractResourceMetadataUrl", () => {
21+
it("returns resource metadata url when present", async () => {
22+
const resourceUrl = "https://resource.example.com/.well-known/oauth-protected-resource"
23+
const mockResponse = {
24+
headers: {
25+
get: jest.fn((name) => name === "WWW-Authenticate" ? `Bearer realm="mcp", resource_metadata="${resourceUrl}"` : null),
26+
}
27+
} as unknown as Response
28+
29+
expect(extractResourceMetadataUrl(mockResponse)).toEqual(new URL(resourceUrl));
30+
});
31+
32+
it("returns undefined if not bearer", async () => {
33+
const resourceUrl = "https://resource.example.com/.well-known/oauth-protected-resource"
34+
const mockResponse = {
35+
headers: {
36+
get: jest.fn((name) => name === "WWW-Authenticate" ? `Basic realm="mcp", resource_metadata="${resourceUrl}"` : null),
37+
}
38+
} as unknown as Response
39+
40+
expect(extractResourceMetadataUrl(mockResponse)).toBeUndefined();
41+
});
42+
43+
it("returns undefined if resource_metadata not present", async () => {
44+
const mockResponse = {
45+
headers: {
46+
get: jest.fn((name) => name === "WWW-Authenticate" ? `Basic realm="mcp"` : null),
47+
}
48+
} as unknown as Response
49+
50+
expect(extractResourceMetadataUrl(mockResponse)).toBeUndefined();
51+
});
52+
53+
it("returns undefined on invalid url", async () => {
54+
const resourceUrl = "invalid-url"
55+
const mockResponse = {
56+
headers: {
57+
get: jest.fn((name) => name === "WWW-Authenticate" ? `Basic realm="mcp", resource_metadata="${resourceUrl}"` : null),
58+
}
59+
} as unknown as Response
60+
61+
expect(extractResourceMetadataUrl(mockResponse)).toBeUndefined();
62+
});
63+
});
64+
65+
describe("discoverOAuthProtectedResourceMetadata", () => {
66+
const validMetadata = {
67+
resource: "https://resource.example.com",
68+
authorization_servers: ["https://auth.example.com"],
69+
};
70+
71+
it("returns metadata when discovery succeeds", async () => {
72+
mockFetch.mockResolvedValueOnce({
73+
ok: true,
74+
status: 200,
75+
json: async () => validMetadata,
76+
});
77+
78+
const metadata = await discoverOAuthProtectedResourceMetadata("https://resource.example.com");
79+
expect(metadata).toEqual(validMetadata);
80+
const calls = mockFetch.mock.calls;
81+
expect(calls.length).toBe(1);
82+
const [url, options] = calls[0];
83+
expect(url.toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
84+
expect(options.headers).toEqual({
85+
"MCP-Protocol-Version": "2024-11-05"
86+
});
87+
});
88+
89+
it("returns metadata when first fetch fails but second without MCP header succeeds", async () => {
90+
// Set up a counter to control behavior
91+
let callCount = 0;
92+
93+
// Mock implementation that changes behavior based on call count
94+
mockFetch.mockImplementation((_url, _options) => {
95+
callCount++;
96+
97+
if (callCount === 1) {
98+
// First call with MCP header - fail with TypeError (simulating CORS error)
99+
// We need to use TypeError specifically because that's what the implementation checks for
100+
return Promise.reject(new TypeError("Network error"));
101+
} else {
102+
// Second call without header - succeed
103+
return Promise.resolve({
104+
ok: true,
105+
status: 200,
106+
json: async () => validMetadata
107+
});
108+
}
109+
});
110+
111+
// Should succeed with the second call
112+
const metadata = await discoverOAuthProtectedResourceMetadata("https://resource.example.com");
113+
expect(metadata).toEqual(validMetadata);
114+
115+
// Verify both calls were made
116+
expect(mockFetch).toHaveBeenCalledTimes(2);
117+
118+
// Verify first call had MCP header
119+
expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version");
120+
});
121+
122+
it("throws an error when all fetch attempts fail", async () => {
123+
// Set up a counter to control behavior
124+
let callCount = 0;
125+
126+
// Mock implementation that changes behavior based on call count
127+
mockFetch.mockImplementation((_url, _options) => {
128+
callCount++;
129+
130+
if (callCount === 1) {
131+
// First call - fail with TypeError
132+
return Promise.reject(new TypeError("First failure"));
133+
} else {
134+
// Second call - fail with different error
135+
return Promise.reject(new Error("Second failure"));
136+
}
137+
});
138+
139+
// Should fail with the second error
140+
await expect(discoverOAuthProtectedResourceMetadata("https://resource.example.com"))
141+
.rejects.toThrow("Second failure");
142+
143+
// Verify both calls were made
144+
expect(mockFetch).toHaveBeenCalledTimes(2);
145+
});
146+
147+
it("throws on 404 errors", async () => {
148+
mockFetch.mockResolvedValueOnce({
149+
ok: false,
150+
status: 404,
151+
});
152+
153+
await expect(discoverOAuthProtectedResourceMetadata("https://resource.example.com"))
154+
.rejects.toThrow("Resource server does not implement OAuth 2.0 Protected Resource Metadata.");
155+
});
156+
157+
it("throws on non-404 errors", async () => {
158+
mockFetch.mockResolvedValueOnce({
159+
ok: false,
160+
status: 500,
161+
});
162+
163+
await expect(discoverOAuthProtectedResourceMetadata("https://resource.example.com"))
164+
.rejects.toThrow("HTTP 500");
165+
});
166+
167+
it("validates metadata schema", async () => {
168+
mockFetch.mockResolvedValueOnce({
169+
ok: true,
170+
status: 200,
171+
json: async () => ({
172+
// Missing required fields
173+
scopes_supported: ["email", "mcp"],
174+
}),
175+
});
176+
177+
await expect(discoverOAuthProtectedResourceMetadata("https://resource.example.com"))
178+
.rejects.toThrow();
179+
});
180+
});
181+
18182
describe("discoverOAuthMetadata", () => {
19183
const validMetadata = {
20184
issuer: "https://auth.example.com",

0 commit comments

Comments
 (0)