@toolsdk.ai/registry 1.0.133 → 1.0.134
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
// Mock dependencies before importing the module under test
|
|
3
|
+
vi.mock("../../package/package-handler", () => ({
|
|
4
|
+
repository: {
|
|
5
|
+
getPackageConfig: vi.fn(),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("../oauth-session", () => ({
|
|
9
|
+
oauthSessionStore: {
|
|
10
|
+
set: vi.fn(),
|
|
11
|
+
get: vi.fn(),
|
|
12
|
+
getByState: vi.fn(),
|
|
13
|
+
delete: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
vi.mock("../oauth-utils", () => ({
|
|
17
|
+
discoverProtectedResourceMetadata: vi.fn(),
|
|
18
|
+
discoverAuthServerMetadata: vi.fn(),
|
|
19
|
+
verifyPKCESupport: vi.fn(),
|
|
20
|
+
registerClient: vi.fn(),
|
|
21
|
+
generatePKCE: vi.fn(),
|
|
22
|
+
generateState: vi.fn(),
|
|
23
|
+
generateSessionId: vi.fn(),
|
|
24
|
+
buildAuthorizationUrl: vi.fn(),
|
|
25
|
+
getCanonicalResourceUri: vi.fn(),
|
|
26
|
+
exchangeCodeForTokens: vi.fn(),
|
|
27
|
+
refreshAccessToken: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
vi.mock("../../shared/config/environment", () => ({
|
|
30
|
+
getServerPort: vi.fn(() => 3000),
|
|
31
|
+
}));
|
|
32
|
+
// Import after mocking
|
|
33
|
+
import { repository } from "../../package/package-handler";
|
|
34
|
+
import { handleCallback, handleRefresh, prepareOAuth } from "../oauth-handler";
|
|
35
|
+
import { oauthSessionStore } from "../oauth-session";
|
|
36
|
+
import { buildAuthorizationUrl, discoverAuthServerMetadata, discoverProtectedResourceMetadata, exchangeCodeForTokens, generatePKCE, generateSessionId, generateState, getCanonicalResourceUri, refreshAccessToken, registerClient, verifyPKCESupport, } from "../oauth-utils";
|
|
37
|
+
// Helper functions to create mock data
|
|
38
|
+
function createMockResourceMetadata(overrides = {}) {
|
|
39
|
+
return Object.assign({ resource: "https://mcp.example.com/server", authorization_servers: ["https://auth.example.com"], scopes_supported: ["read", "write"] }, overrides);
|
|
40
|
+
}
|
|
41
|
+
function createMockOAuthMetadata(overrides = {}) {
|
|
42
|
+
return Object.assign({ issuer: "https://auth.example.com", authorization_endpoint: "https://auth.example.com/authorize", token_endpoint: "https://auth.example.com/token", registration_endpoint: "https://auth.example.com/register", code_challenge_methods_supported: ["S256"], scopes_supported: ["read", "write"] }, overrides);
|
|
43
|
+
}
|
|
44
|
+
function createMockSession(overrides = {}) {
|
|
45
|
+
return Object.assign({ sessionId: "test-session-id", state: "test-state", codeVerifier: "test-code-verifier", codeChallenge: "test-code-challenge", clientInfo: {
|
|
46
|
+
client_id: "test-client-id",
|
|
47
|
+
client_secret: "test-client-secret",
|
|
48
|
+
}, callbackBaseUrl: "http://localhost:3003/callback", mcpServerUrl: "https://mcp.example.com/server", packageName: "test-package", oauthMetadata: createMockOAuthMetadata(), createdAt: Date.now() }, overrides);
|
|
49
|
+
}
|
|
50
|
+
describe("oauth-handler", () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
// Setup default mock implementations
|
|
54
|
+
vi.mocked(generatePKCE).mockReturnValue({
|
|
55
|
+
codeVerifier: "mock-code-verifier",
|
|
56
|
+
codeChallenge: "mock-code-challenge",
|
|
57
|
+
codeChallengeMethod: "S256",
|
|
58
|
+
});
|
|
59
|
+
vi.mocked(generateState).mockReturnValue("mock-state");
|
|
60
|
+
vi.mocked(generateSessionId).mockReturnValue("mock-session-id");
|
|
61
|
+
vi.mocked(getCanonicalResourceUri).mockReturnValue("https://mcp.example.com/server");
|
|
62
|
+
vi.mocked(buildAuthorizationUrl).mockReturnValue("https://auth.example.com/authorize?...");
|
|
63
|
+
});
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
vi.restoreAllMocks();
|
|
66
|
+
});
|
|
67
|
+
describe("prepareOAuth", () => {
|
|
68
|
+
describe("successful OAuth flow preparation", () => {
|
|
69
|
+
it("should successfully prepare OAuth flow with direct mcpServerUrl", async () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
72
|
+
const oauthMetadata = createMockOAuthMetadata();
|
|
73
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
74
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
75
|
+
vi.mocked(verifyPKCESupport).mockReturnValue({ supported: true, advertised: true });
|
|
76
|
+
vi.mocked(registerClient).mockResolvedValue({
|
|
77
|
+
client_id: "registered-client-id",
|
|
78
|
+
client_secret: "registered-secret",
|
|
79
|
+
});
|
|
80
|
+
// Act
|
|
81
|
+
const result = await prepareOAuth({
|
|
82
|
+
packageName: "test-package",
|
|
83
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
84
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
85
|
+
});
|
|
86
|
+
// Assert
|
|
87
|
+
expect(result.success).toBe(true);
|
|
88
|
+
expect(result.code).toBe(200);
|
|
89
|
+
expect(result.data).toHaveProperty("authUrl");
|
|
90
|
+
expect(result.data).toHaveProperty("sessionId");
|
|
91
|
+
expect(result.data).toHaveProperty("mcpServerUrl");
|
|
92
|
+
expect(oauthSessionStore.set).toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
it("should successfully prepare OAuth flow using package config", async () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
vi.mocked(repository.getPackageConfig).mockReturnValue({
|
|
97
|
+
name: "test-package",
|
|
98
|
+
remotes: [{ type: "streamable-http", url: "https://mcp.example.com/server" }],
|
|
99
|
+
});
|
|
100
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
101
|
+
const oauthMetadata = createMockOAuthMetadata();
|
|
102
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
103
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
104
|
+
vi.mocked(verifyPKCESupport).mockReturnValue({ supported: true, advertised: true });
|
|
105
|
+
vi.mocked(registerClient).mockResolvedValue({
|
|
106
|
+
client_id: "registered-client-id",
|
|
107
|
+
});
|
|
108
|
+
// Act
|
|
109
|
+
const result = await prepareOAuth({
|
|
110
|
+
packageName: "test-package",
|
|
111
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
112
|
+
});
|
|
113
|
+
// Assert
|
|
114
|
+
expect(result.success).toBe(true);
|
|
115
|
+
expect(result.code).toBe(200);
|
|
116
|
+
});
|
|
117
|
+
it("should use default client when no registration endpoint", async () => {
|
|
118
|
+
// Arrange
|
|
119
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
120
|
+
const oauthMetadata = createMockOAuthMetadata({ registration_endpoint: undefined });
|
|
121
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
122
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
123
|
+
vi.mocked(verifyPKCESupport).mockReturnValue({ supported: true, advertised: true });
|
|
124
|
+
// Act
|
|
125
|
+
const result = await prepareOAuth({
|
|
126
|
+
packageName: "test-package",
|
|
127
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
128
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
129
|
+
});
|
|
130
|
+
// Assert
|
|
131
|
+
expect(result.success).toBe(true);
|
|
132
|
+
expect(registerClient).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
it("should log warning but proceed when PKCE is not advertised", async () => {
|
|
135
|
+
// Arrange
|
|
136
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
137
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
138
|
+
const oauthMetadata = createMockOAuthMetadata();
|
|
139
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
140
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
141
|
+
vi.mocked(verifyPKCESupport).mockReturnValue({ supported: true, advertised: false });
|
|
142
|
+
vi.mocked(registerClient).mockResolvedValue({ client_id: "test-client" });
|
|
143
|
+
// Act
|
|
144
|
+
const result = await prepareOAuth({
|
|
145
|
+
packageName: "test-package",
|
|
146
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
147
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
148
|
+
});
|
|
149
|
+
// Assert
|
|
150
|
+
expect(result.success).toBe(true);
|
|
151
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("does not advertise code_challenge_methods_supported"));
|
|
152
|
+
consoleSpy.mockRestore();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe("error handling during discovery/registration", () => {
|
|
156
|
+
it("should return error when package does not support OAuth", async () => {
|
|
157
|
+
// Arrange
|
|
158
|
+
vi.mocked(repository.getPackageConfig).mockReturnValue({
|
|
159
|
+
packageName: "test-package",
|
|
160
|
+
type: "mcp-server",
|
|
161
|
+
runtime: "node",
|
|
162
|
+
remotes: [],
|
|
163
|
+
});
|
|
164
|
+
// Act
|
|
165
|
+
const result = await prepareOAuth({
|
|
166
|
+
packageName: "test-package",
|
|
167
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
168
|
+
});
|
|
169
|
+
// Assert
|
|
170
|
+
expect(result.success).toBe(false);
|
|
171
|
+
expect(result.code).toBe(400);
|
|
172
|
+
expect(result.message).toContain("does not support OAuth");
|
|
173
|
+
});
|
|
174
|
+
it("should return error when no authorization servers found", async () => {
|
|
175
|
+
// Arrange
|
|
176
|
+
const resourceMetadata = createMockResourceMetadata({ authorization_servers: [] });
|
|
177
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
178
|
+
// Act
|
|
179
|
+
const result = await prepareOAuth({
|
|
180
|
+
packageName: "test-package",
|
|
181
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
182
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
183
|
+
});
|
|
184
|
+
// Assert
|
|
185
|
+
expect(result.success).toBe(false);
|
|
186
|
+
expect(result.code).toBe(400);
|
|
187
|
+
expect(result.message).toContain("No authorization servers found");
|
|
188
|
+
});
|
|
189
|
+
it("should return error when PKCE is explicitly not supported", async () => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
192
|
+
const oauthMetadata = createMockOAuthMetadata();
|
|
193
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
194
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
195
|
+
vi.mocked(verifyPKCESupport).mockReturnValue({ supported: false, advertised: true });
|
|
196
|
+
// Act
|
|
197
|
+
const result = await prepareOAuth({
|
|
198
|
+
packageName: "test-package",
|
|
199
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
200
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
201
|
+
});
|
|
202
|
+
// Assert
|
|
203
|
+
expect(result.success).toBe(false);
|
|
204
|
+
expect(result.code).toBe(400);
|
|
205
|
+
expect(result.message).toContain("does not support PKCE");
|
|
206
|
+
});
|
|
207
|
+
it("should return error when resource metadata discovery fails", async () => {
|
|
208
|
+
// Arrange
|
|
209
|
+
vi.mocked(discoverProtectedResourceMetadata).mockRejectedValue(new Error("Discovery failed"));
|
|
210
|
+
// Act
|
|
211
|
+
const result = await prepareOAuth({
|
|
212
|
+
packageName: "test-package",
|
|
213
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
214
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
215
|
+
});
|
|
216
|
+
// Assert
|
|
217
|
+
expect(result.success).toBe(false);
|
|
218
|
+
expect(result.code).toBe(500);
|
|
219
|
+
expect(result.message).toBe("Discovery failed");
|
|
220
|
+
});
|
|
221
|
+
it("should return error when auth server metadata discovery fails", async () => {
|
|
222
|
+
// Arrange
|
|
223
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
224
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
225
|
+
vi.mocked(discoverAuthServerMetadata).mockRejectedValue(new Error("Auth server discovery failed"));
|
|
226
|
+
// Act
|
|
227
|
+
const result = await prepareOAuth({
|
|
228
|
+
packageName: "test-package",
|
|
229
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
230
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
231
|
+
});
|
|
232
|
+
// Assert
|
|
233
|
+
expect(result.success).toBe(false);
|
|
234
|
+
expect(result.code).toBe(500);
|
|
235
|
+
expect(result.message).toBe("Auth server discovery failed");
|
|
236
|
+
});
|
|
237
|
+
it("should return error when client registration fails", async () => {
|
|
238
|
+
// Arrange
|
|
239
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
240
|
+
const oauthMetadata = createMockOAuthMetadata();
|
|
241
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
242
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
243
|
+
vi.mocked(verifyPKCESupport).mockReturnValue({ supported: true, advertised: true });
|
|
244
|
+
vi.mocked(registerClient).mockRejectedValue(new Error("Registration failed"));
|
|
245
|
+
// Act
|
|
246
|
+
const result = await prepareOAuth({
|
|
247
|
+
packageName: "test-package",
|
|
248
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
249
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
250
|
+
});
|
|
251
|
+
// Assert
|
|
252
|
+
expect(result.success).toBe(false);
|
|
253
|
+
expect(result.code).toBe(500);
|
|
254
|
+
expect(result.message).toBe("Registration failed");
|
|
255
|
+
});
|
|
256
|
+
it("should handle non-Error exceptions gracefully", async () => {
|
|
257
|
+
// Arrange
|
|
258
|
+
vi.mocked(discoverProtectedResourceMetadata).mockRejectedValue("Unknown error");
|
|
259
|
+
// Act
|
|
260
|
+
const result = await prepareOAuth({
|
|
261
|
+
packageName: "test-package",
|
|
262
|
+
callbackBaseUrl: "http://localhost:3003/callback",
|
|
263
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
264
|
+
});
|
|
265
|
+
// Assert
|
|
266
|
+
expect(result.success).toBe(false);
|
|
267
|
+
expect(result.code).toBe(500);
|
|
268
|
+
expect(result.message).toBe("Unknown error during OAuth preparation");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
describe("handleCallback", () => {
|
|
273
|
+
describe("callback handling with valid/invalid state", () => {
|
|
274
|
+
it("should return error when OAuth error is present in params", async () => {
|
|
275
|
+
// Act
|
|
276
|
+
const result = await handleCallback({
|
|
277
|
+
error: "access_denied",
|
|
278
|
+
error_description: "User denied access",
|
|
279
|
+
});
|
|
280
|
+
// Assert
|
|
281
|
+
expect(result.success).toBe(false);
|
|
282
|
+
expect(result.error).toBe("access_denied");
|
|
283
|
+
expect(result.error_description).toBe("User denied access");
|
|
284
|
+
expect(result.html).toContain("Authorization Failed");
|
|
285
|
+
});
|
|
286
|
+
it("should return error when code is missing", async () => {
|
|
287
|
+
// Act
|
|
288
|
+
const result = await handleCallback({
|
|
289
|
+
state: "test-state",
|
|
290
|
+
});
|
|
291
|
+
// Assert
|
|
292
|
+
expect(result.success).toBe(false);
|
|
293
|
+
expect(result.error).toBe("invalid_request");
|
|
294
|
+
expect(result.error_description).toBe("Missing code or state parameter");
|
|
295
|
+
});
|
|
296
|
+
it("should return error when state is missing", async () => {
|
|
297
|
+
// Act
|
|
298
|
+
const result = await handleCallback({
|
|
299
|
+
code: "test-code",
|
|
300
|
+
});
|
|
301
|
+
// Assert
|
|
302
|
+
expect(result.success).toBe(false);
|
|
303
|
+
expect(result.error).toBe("invalid_request");
|
|
304
|
+
expect(result.error_description).toBe("Missing code or state parameter");
|
|
305
|
+
});
|
|
306
|
+
it("should return error when session is not found by state", async () => {
|
|
307
|
+
// Arrange
|
|
308
|
+
vi.mocked(oauthSessionStore.getByState).mockReturnValue(undefined);
|
|
309
|
+
// Act
|
|
310
|
+
const result = await handleCallback({
|
|
311
|
+
code: "test-code",
|
|
312
|
+
state: "invalid-state",
|
|
313
|
+
});
|
|
314
|
+
// Assert
|
|
315
|
+
expect(result.success).toBe(false);
|
|
316
|
+
expect(result.error).toBe("invalid_state");
|
|
317
|
+
expect(result.error_description).toBe("Invalid or expired state parameter");
|
|
318
|
+
expect(result.html).toContain("Invalid or expired authorization session");
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
describe("token exchange success and failure scenarios", () => {
|
|
322
|
+
it("should successfully exchange code for tokens", async () => {
|
|
323
|
+
// Arrange
|
|
324
|
+
const mockSession = createMockSession();
|
|
325
|
+
vi.mocked(oauthSessionStore.getByState).mockReturnValue(mockSession);
|
|
326
|
+
vi.mocked(exchangeCodeForTokens).mockResolvedValue({
|
|
327
|
+
access_token: "test-access-token",
|
|
328
|
+
token_type: "Bearer",
|
|
329
|
+
expires_in: 3600,
|
|
330
|
+
refresh_token: "test-refresh-token",
|
|
331
|
+
});
|
|
332
|
+
// Mock fetch for callback POST
|
|
333
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
334
|
+
global.fetch = mockFetch;
|
|
335
|
+
// Act
|
|
336
|
+
const result = await handleCallback({
|
|
337
|
+
code: "test-code",
|
|
338
|
+
state: "test-state",
|
|
339
|
+
});
|
|
340
|
+
// Assert
|
|
341
|
+
expect(result.success).toBe(true);
|
|
342
|
+
expect(result.sessionId).toBe(mockSession.sessionId);
|
|
343
|
+
expect(result.html).toContain("Authorization Successful");
|
|
344
|
+
expect(oauthSessionStore.delete).toHaveBeenCalledWith(mockSession.sessionId);
|
|
345
|
+
});
|
|
346
|
+
it("should POST callback data to callbackBaseUrl", async () => {
|
|
347
|
+
// Arrange
|
|
348
|
+
const mockSession = createMockSession();
|
|
349
|
+
vi.mocked(oauthSessionStore.getByState).mockReturnValue(mockSession);
|
|
350
|
+
vi.mocked(exchangeCodeForTokens).mockResolvedValue({
|
|
351
|
+
access_token: "test-access-token",
|
|
352
|
+
token_type: "Bearer",
|
|
353
|
+
});
|
|
354
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
355
|
+
global.fetch = mockFetch;
|
|
356
|
+
// Act
|
|
357
|
+
await handleCallback({
|
|
358
|
+
code: "test-code",
|
|
359
|
+
state: "test-state",
|
|
360
|
+
});
|
|
361
|
+
// Assert
|
|
362
|
+
expect(mockFetch).toHaveBeenCalledWith(mockSession.callbackBaseUrl, expect.objectContaining({
|
|
363
|
+
method: "POST",
|
|
364
|
+
headers: { "Content-Type": "application/json" },
|
|
365
|
+
body: expect.stringContaining("test-access-token"),
|
|
366
|
+
}));
|
|
367
|
+
});
|
|
368
|
+
it("should succeed even if callback POST fails", async () => {
|
|
369
|
+
// Arrange
|
|
370
|
+
const mockSession = createMockSession();
|
|
371
|
+
vi.mocked(oauthSessionStore.getByState).mockReturnValue(mockSession);
|
|
372
|
+
vi.mocked(exchangeCodeForTokens).mockResolvedValue({
|
|
373
|
+
access_token: "test-access-token",
|
|
374
|
+
token_type: "Bearer",
|
|
375
|
+
});
|
|
376
|
+
// Mock fetch to fail
|
|
377
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error("Network error"));
|
|
378
|
+
global.fetch = mockFetch;
|
|
379
|
+
// Act
|
|
380
|
+
const result = await handleCallback({
|
|
381
|
+
code: "test-code",
|
|
382
|
+
state: "test-state",
|
|
383
|
+
});
|
|
384
|
+
// Assert - should still succeed
|
|
385
|
+
expect(result.success).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
it("should log warning when callback POST returns non-ok status", async () => {
|
|
388
|
+
// Arrange
|
|
389
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
390
|
+
const mockSession = createMockSession();
|
|
391
|
+
vi.mocked(oauthSessionStore.getByState).mockReturnValue(mockSession);
|
|
392
|
+
vi.mocked(exchangeCodeForTokens).mockResolvedValue({
|
|
393
|
+
access_token: "test-access-token",
|
|
394
|
+
token_type: "Bearer",
|
|
395
|
+
});
|
|
396
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: false, status: 500 });
|
|
397
|
+
global.fetch = mockFetch;
|
|
398
|
+
// Act
|
|
399
|
+
const result = await handleCallback({
|
|
400
|
+
code: "test-code",
|
|
401
|
+
state: "test-state",
|
|
402
|
+
});
|
|
403
|
+
// Assert
|
|
404
|
+
expect(result.success).toBe(true);
|
|
405
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Callback POST returned"));
|
|
406
|
+
consoleSpy.mockRestore();
|
|
407
|
+
});
|
|
408
|
+
it("should return error when token exchange fails", async () => {
|
|
409
|
+
// Arrange
|
|
410
|
+
const mockSession = createMockSession();
|
|
411
|
+
vi.mocked(oauthSessionStore.getByState).mockReturnValue(mockSession);
|
|
412
|
+
vi.mocked(exchangeCodeForTokens).mockRejectedValue(new Error("Invalid grant"));
|
|
413
|
+
// Act
|
|
414
|
+
const result = await handleCallback({
|
|
415
|
+
code: "test-code",
|
|
416
|
+
state: "test-state",
|
|
417
|
+
});
|
|
418
|
+
// Assert
|
|
419
|
+
expect(result.success).toBe(false);
|
|
420
|
+
expect(result.error).toBe("token_exchange_failed");
|
|
421
|
+
expect(result.error_description).toBe("Invalid grant");
|
|
422
|
+
expect(oauthSessionStore.delete).toHaveBeenCalledWith(mockSession.sessionId);
|
|
423
|
+
});
|
|
424
|
+
it("should handle non-Error exceptions during token exchange", async () => {
|
|
425
|
+
// Arrange
|
|
426
|
+
const mockSession = createMockSession();
|
|
427
|
+
vi.mocked(oauthSessionStore.getByState).mockReturnValue(mockSession);
|
|
428
|
+
vi.mocked(exchangeCodeForTokens).mockRejectedValue("Unknown error");
|
|
429
|
+
// Act
|
|
430
|
+
const result = await handleCallback({
|
|
431
|
+
code: "test-code",
|
|
432
|
+
state: "test-state",
|
|
433
|
+
});
|
|
434
|
+
// Assert
|
|
435
|
+
expect(result.success).toBe(false);
|
|
436
|
+
expect(result.error_description).toBe("Token exchange failed");
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
describe("HTML response generation", () => {
|
|
440
|
+
it("should generate success HTML with postMessage script", async () => {
|
|
441
|
+
// Arrange
|
|
442
|
+
const mockSession = createMockSession();
|
|
443
|
+
vi.mocked(oauthSessionStore.getByState).mockReturnValue(mockSession);
|
|
444
|
+
vi.mocked(exchangeCodeForTokens).mockResolvedValue({
|
|
445
|
+
access_token: "test-access-token",
|
|
446
|
+
token_type: "Bearer",
|
|
447
|
+
});
|
|
448
|
+
global.fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
449
|
+
// Act
|
|
450
|
+
const result = await handleCallback({
|
|
451
|
+
code: "test-code",
|
|
452
|
+
state: "test-state",
|
|
453
|
+
});
|
|
454
|
+
// Assert
|
|
455
|
+
expect(result.html).toContain("oauth-callback");
|
|
456
|
+
expect(result.html).toContain("postMessage");
|
|
457
|
+
expect(result.html).toContain("window.opener");
|
|
458
|
+
});
|
|
459
|
+
it("should generate error HTML with error message", async () => {
|
|
460
|
+
// Act
|
|
461
|
+
const result = await handleCallback({
|
|
462
|
+
error: "access_denied",
|
|
463
|
+
error_description: "User denied access",
|
|
464
|
+
});
|
|
465
|
+
// Assert
|
|
466
|
+
expect(result.html).toContain("User denied access");
|
|
467
|
+
expect(result.html).toContain("Authorization Failed");
|
|
468
|
+
});
|
|
469
|
+
it("should use error code when error_description is not provided", async () => {
|
|
470
|
+
// Act
|
|
471
|
+
const result = await handleCallback({
|
|
472
|
+
error: "server_error",
|
|
473
|
+
});
|
|
474
|
+
// Assert
|
|
475
|
+
expect(result.html).toContain("server_error");
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
describe("handleRefresh", () => {
|
|
480
|
+
describe("refresh token functionality", () => {
|
|
481
|
+
it("should successfully refresh access token", async () => {
|
|
482
|
+
// Arrange
|
|
483
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
484
|
+
const oauthMetadata = createMockOAuthMetadata();
|
|
485
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
486
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
487
|
+
vi.mocked(refreshAccessToken).mockResolvedValue({
|
|
488
|
+
access_token: "new-access-token",
|
|
489
|
+
token_type: "Bearer",
|
|
490
|
+
expires_in: 3600,
|
|
491
|
+
refresh_token: "new-refresh-token",
|
|
492
|
+
});
|
|
493
|
+
// Act
|
|
494
|
+
const result = await handleRefresh({
|
|
495
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
496
|
+
refreshToken: "old-refresh-token",
|
|
497
|
+
clientId: "test-client-id",
|
|
498
|
+
clientSecret: "test-client-secret",
|
|
499
|
+
});
|
|
500
|
+
// Assert
|
|
501
|
+
expect(result.success).toBe(true);
|
|
502
|
+
expect(result.code).toBe(200);
|
|
503
|
+
expect(result.data).toEqual({
|
|
504
|
+
access_token: "new-access-token",
|
|
505
|
+
token_type: "Bearer",
|
|
506
|
+
expires_in: 3600,
|
|
507
|
+
refresh_token: "new-refresh-token",
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
it("should return error when no authorization servers found", async () => {
|
|
511
|
+
// Arrange
|
|
512
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue({
|
|
513
|
+
resource: "https://mcp.example.com/server",
|
|
514
|
+
authorization_servers: [],
|
|
515
|
+
});
|
|
516
|
+
// Act
|
|
517
|
+
const result = await handleRefresh({
|
|
518
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
519
|
+
refreshToken: "old-refresh-token",
|
|
520
|
+
clientId: "test-client-id",
|
|
521
|
+
});
|
|
522
|
+
// Assert
|
|
523
|
+
expect(result.success).toBe(false);
|
|
524
|
+
expect(result.code).toBe(400);
|
|
525
|
+
expect(result.message).toContain("No authorization servers found");
|
|
526
|
+
});
|
|
527
|
+
it("should return error when resource metadata discovery fails", async () => {
|
|
528
|
+
// Arrange
|
|
529
|
+
vi.mocked(discoverProtectedResourceMetadata).mockRejectedValue(new Error("Discovery failed"));
|
|
530
|
+
// Act
|
|
531
|
+
const result = await handleRefresh({
|
|
532
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
533
|
+
refreshToken: "old-refresh-token",
|
|
534
|
+
clientId: "test-client-id",
|
|
535
|
+
});
|
|
536
|
+
// Assert
|
|
537
|
+
expect(result.success).toBe(false);
|
|
538
|
+
expect(result.code).toBe(500);
|
|
539
|
+
expect(result.message).toBe("Discovery failed");
|
|
540
|
+
});
|
|
541
|
+
it("should return error when refresh token request fails", async () => {
|
|
542
|
+
// Arrange
|
|
543
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
544
|
+
const oauthMetadata = createMockOAuthMetadata();
|
|
545
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
546
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
547
|
+
vi.mocked(refreshAccessToken).mockRejectedValue(new Error("Invalid refresh token"));
|
|
548
|
+
// Act
|
|
549
|
+
const result = await handleRefresh({
|
|
550
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
551
|
+
refreshToken: "invalid-refresh-token",
|
|
552
|
+
clientId: "test-client-id",
|
|
553
|
+
});
|
|
554
|
+
// Assert
|
|
555
|
+
expect(result.success).toBe(false);
|
|
556
|
+
expect(result.code).toBe(500);
|
|
557
|
+
expect(result.message).toBe("Invalid refresh token");
|
|
558
|
+
});
|
|
559
|
+
it("should handle non-Error exceptions during refresh", async () => {
|
|
560
|
+
// Arrange
|
|
561
|
+
vi.mocked(discoverProtectedResourceMetadata).mockRejectedValue("Unknown error");
|
|
562
|
+
// Act
|
|
563
|
+
const result = await handleRefresh({
|
|
564
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
565
|
+
refreshToken: "old-refresh-token",
|
|
566
|
+
clientId: "test-client-id",
|
|
567
|
+
});
|
|
568
|
+
// Assert
|
|
569
|
+
expect(result.success).toBe(false);
|
|
570
|
+
expect(result.message).toBe("Token refresh failed");
|
|
571
|
+
});
|
|
572
|
+
it("should work without client secret", async () => {
|
|
573
|
+
// Arrange
|
|
574
|
+
const resourceMetadata = createMockResourceMetadata();
|
|
575
|
+
const oauthMetadata = createMockOAuthMetadata();
|
|
576
|
+
vi.mocked(discoverProtectedResourceMetadata).mockResolvedValue(resourceMetadata);
|
|
577
|
+
vi.mocked(discoverAuthServerMetadata).mockResolvedValue(oauthMetadata);
|
|
578
|
+
vi.mocked(refreshAccessToken).mockResolvedValue({
|
|
579
|
+
access_token: "new-access-token",
|
|
580
|
+
token_type: "Bearer",
|
|
581
|
+
});
|
|
582
|
+
// Act
|
|
583
|
+
const result = await handleRefresh({
|
|
584
|
+
mcpServerUrl: "https://mcp.example.com/server",
|
|
585
|
+
refreshToken: "old-refresh-token",
|
|
586
|
+
clientId: "test-client-id",
|
|
587
|
+
// No clientSecret
|
|
588
|
+
});
|
|
589
|
+
// Assert
|
|
590
|
+
expect(result.success).toBe(true);
|
|
591
|
+
expect(refreshAccessToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
592
|
+
clientId: "test-client-id",
|
|
593
|
+
clientSecret: undefined,
|
|
594
|
+
}));
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
});
|