Fix tests to work with ES modules and TypeScript
- Updated Jest configuration to use ts-jest ESM preset - Added NODE_OPTIONS flag for experimental VM modules support - Fixed import statements in all test files to import Jest globals - Rewrote provider.test.ts to work with ESM (using actual implementations with mock Redis) - Fixed test expectations to match actual implementation - Fixed encryption key generation in tests (using proper 64-char hex tokens) - Standardized token_type to "Bearer" across codebase - Removed unused imports to fix lint errors - Excluded scratch folder from test runs All tests now pass successfully with proper ES module support. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Relocked
This commit is contained in:
parent
b1d1790f62
commit
93c939614e
8 changed files with 281 additions and 424 deletions
|
|
@ -1,12 +1,19 @@
|
||||||
import { createDefaultEsmPreset } from "ts-jest";
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
|
||||||
const defaultEsmPreset = createDefaultEsmPreset();
|
|
||||||
|
|
||||||
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
|
||||||
export default {
|
export default {
|
||||||
...defaultEsmPreset,
|
preset: 'ts-jest/presets/default-esm',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||||
},
|
},
|
||||||
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
|
transform: {
|
||||||
|
'^.+\\.tsx?$': [
|
||||||
|
'ts-jest',
|
||||||
|
{
|
||||||
|
useESM: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/scratch/'],
|
||||||
|
injectGlobals: true,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
463
package-lock.json
generated
463
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,7 @@
|
||||||
"dev": "tsx watch src/index.ts",
|
"dev": "tsx watch src/index.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"test": "jest"
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.15.0",
|
"@eslint/js": "^9.15.0",
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { jest, describe, beforeEach, it, expect } from '@jest/globals';
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { AuthorizationParams } from "@modelcontextprotocol/sdk/server/auth/provider.js";
|
import { AuthorizationParams } from "@modelcontextprotocol/sdk/server/auth/provider.js";
|
||||||
import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||||
|
|
@ -7,28 +8,6 @@ import { McpInstallation, PendingAuthorization, TokenExchange } from "../types.j
|
||||||
import { EverythingAuthProvider, EverythingOAuthClientsStore } from "./provider.js";
|
import { EverythingAuthProvider, EverythingOAuthClientsStore } from "./provider.js";
|
||||||
import * as authService from "../services/auth.js";
|
import * as authService from "../services/auth.js";
|
||||||
|
|
||||||
// Create mocks for the auth service functions
|
|
||||||
jest.mock("../services/auth.js", () => {
|
|
||||||
// Use actual implementations for some functions
|
|
||||||
const originalModule = jest.requireActual("../services/auth.js");
|
|
||||||
|
|
||||||
return {
|
|
||||||
...originalModule,
|
|
||||||
exchangeToken: jest.fn(),
|
|
||||||
generateToken: jest.fn(),
|
|
||||||
getClientRegistration: jest.fn(),
|
|
||||||
readMcpInstallation: jest.fn(),
|
|
||||||
readPendingAuthorization: jest.fn(),
|
|
||||||
readRefreshToken: jest.fn(),
|
|
||||||
revokeMcpInstallation: jest.fn(),
|
|
||||||
saveClientRegistration: jest.fn(),
|
|
||||||
savePendingAuthorization: jest.fn(),
|
|
||||||
saveMcpInstallation: jest.fn(),
|
|
||||||
generateMcpTokens: jest.fn(),
|
|
||||||
saveRefreshToken: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to create sample client
|
// Helper function to create sample client
|
||||||
function createTestClient(): OAuthClientInformationFull {
|
function createTestClient(): OAuthClientInformationFull {
|
||||||
return {
|
return {
|
||||||
|
|
@ -44,7 +23,8 @@ function createMockResponse() {
|
||||||
const res = {
|
const res = {
|
||||||
redirect: jest.fn().mockReturnThis(),
|
redirect: jest.fn().mockReturnThis(),
|
||||||
status: jest.fn().mockReturnThis(),
|
status: jest.fn().mockReturnThis(),
|
||||||
json: jest.fn().mockReturnThis()
|
json: jest.fn().mockReturnThis(),
|
||||||
|
send: jest.fn().mockReturnThis()
|
||||||
};
|
};
|
||||||
return res as unknown as jest.Mocked<Response>;
|
return res as unknown as jest.Mocked<Response>;
|
||||||
}
|
}
|
||||||
|
|
@ -52,10 +32,11 @@ function createMockResponse() {
|
||||||
|
|
||||||
function getMockAuthValues() {
|
function getMockAuthValues() {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
const accessToken = "test-access-token";
|
// Use properly generated tokens for encryption
|
||||||
|
const accessToken = authService.generateToken();
|
||||||
const newTokens: OAuthTokens = {
|
const newTokens: OAuthTokens = {
|
||||||
access_token: "new-access-token",
|
access_token: authService.generateToken(),
|
||||||
refresh_token: "new-refresh-token",
|
refresh_token: authService.generateToken(),
|
||||||
token_type: "bearer",
|
token_type: "bearer",
|
||||||
expires_in: 3600,
|
expires_in: 3600,
|
||||||
};
|
};
|
||||||
|
|
@ -67,7 +48,7 @@ function getMockAuthValues() {
|
||||||
mcpTokens: {
|
mcpTokens: {
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
refresh_token: "test-refresh-token",
|
refresh_token: authService.generateToken(),
|
||||||
expires_in: 3600,
|
expires_in: 3600,
|
||||||
},
|
},
|
||||||
clientId: client.client_id,
|
clientId: client.client_id,
|
||||||
|
|
@ -86,40 +67,41 @@ describe("EverythingOAuthClientsStore", () => {
|
||||||
let clientsStore: EverythingOAuthClientsStore;
|
let clientsStore: EverythingOAuthClientsStore;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const mockRedis = new MockRedisClient();
|
||||||
|
setRedisClient(mockRedis);
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
clientsStore = new EverythingOAuthClientsStore();
|
clientsStore = new EverythingOAuthClientsStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getClient", () => {
|
describe("getClient", () => {
|
||||||
it("returns undefined for non-existent client", async () => {
|
it("returns undefined for non-existent client", async () => {
|
||||||
(authService.getClientRegistration as jest.Mock).mockResolvedValueOnce(undefined);
|
|
||||||
|
|
||||||
const result = await clientsStore.getClient("non-existent");
|
const result = await clientsStore.getClient("non-existent");
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
expect(authService.getClientRegistration).toHaveBeenCalledWith("non-existent");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns client information for existing client", async () => {
|
it("returns client information for existing client", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
(authService.getClientRegistration as jest.Mock).mockResolvedValueOnce(client);
|
// First save the client
|
||||||
|
await clientsStore.registerClient(client);
|
||||||
|
|
||||||
|
// Then retrieve it
|
||||||
const result = await clientsStore.getClient(client.client_id);
|
const result = await clientsStore.getClient(client.client_id);
|
||||||
|
|
||||||
expect(result).toEqual(client);
|
expect(result).toEqual(client);
|
||||||
expect(authService.getClientRegistration).toHaveBeenCalledWith(client.client_id);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("registerClient", () => {
|
describe("registerClient", () => {
|
||||||
it("saves and returns client information", async () => {
|
it("saves and returns client information", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
(authService.saveClientRegistration as jest.Mock).mockResolvedValueOnce(undefined);
|
|
||||||
|
|
||||||
const result = await clientsStore.registerClient(client);
|
const result = await clientsStore.registerClient(client);
|
||||||
|
|
||||||
expect(result).toEqual(client);
|
expect(result).toEqual(client);
|
||||||
expect(authService.saveClientRegistration).toHaveBeenCalledWith(client.client_id, client);
|
|
||||||
|
// Verify it was saved
|
||||||
|
const retrieved = await clientsStore.getClient(client.client_id);
|
||||||
|
expect(retrieved).toEqual(client);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -134,14 +116,11 @@ describe("EverythingAuthProvider", () => {
|
||||||
mockRedis = new MockRedisClient();
|
mockRedis = new MockRedisClient();
|
||||||
setRedisClient(mockRedis);
|
setRedisClient(mockRedis);
|
||||||
|
|
||||||
// Set up token generator mock
|
|
||||||
(authService.generateToken as jest.Mock).mockImplementation(() => "mock-token");
|
|
||||||
|
|
||||||
provider = new EverythingAuthProvider();
|
provider = new EverythingAuthProvider();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("authorize", () => {
|
describe("authorize", () => {
|
||||||
it("saves pending authorization and redirects to upstream install", async () => {
|
it("saves pending authorization and sends HTML response", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
// Use a type assertion to make TypeScript ignore the mismatch
|
// Use a type assertion to make TypeScript ignore the mismatch
|
||||||
const params = {
|
const params = {
|
||||||
|
|
@ -153,16 +132,11 @@ describe("EverythingAuthProvider", () => {
|
||||||
|
|
||||||
await provider.authorize(client, params, res);
|
await provider.authorize(client, params, res);
|
||||||
|
|
||||||
// Verify pending authorization is saved
|
// Verify HTML sent with redirect
|
||||||
expect(authService.savePendingAuthorization).toHaveBeenCalledWith("mock-token", {
|
expect(res.send).toHaveBeenCalled();
|
||||||
redirectUri: params.redirectUri,
|
const sentHtml = (res.send as jest.Mock).mock.calls[0][0];
|
||||||
codeChallenge: params.codeChallenge,
|
expect(sentHtml).toContain('MCP Auth Page');
|
||||||
codeChallengeMethod: "S256",
|
expect(sentHtml).toContain('fakeupstreamauth/authorize?redirect_uri=/fakeupstreamauth/callback&state=');
|
||||||
clientId: client.client_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify redirect to upstream installation
|
|
||||||
expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining("/fakeupstreamauth/authorize?metadata=mock-token"));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -176,19 +150,18 @@ describe("EverythingAuthProvider", () => {
|
||||||
clientId: client.client_id,
|
clientId: client.client_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
(authService.readPendingAuthorization as jest.Mock).mockResolvedValueOnce(pendingAuth);
|
// First save the pending authorization
|
||||||
|
const authCode = authService.generateToken();
|
||||||
|
await authService.savePendingAuthorization(authCode, pendingAuth);
|
||||||
|
|
||||||
const result = await provider.challengeForAuthorizationCode(client, "test-code");
|
const result = await provider.challengeForAuthorizationCode(client, authCode);
|
||||||
|
|
||||||
expect(result).toBe("test-challenge");
|
expect(result).toBe("test-challenge");
|
||||||
expect(authService.readPendingAuthorization).toHaveBeenCalledWith("test-code");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error for non-existent authorization code", async () => {
|
it("throws error for non-existent authorization code", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
|
|
||||||
(authService.readPendingAuthorization as jest.Mock).mockResolvedValueOnce(undefined);
|
|
||||||
|
|
||||||
await expect(provider.challengeForAuthorizationCode(client, "test-code"))
|
await expect(provider.challengeForAuthorizationCode(client, "test-code"))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Authorization code not found");
|
.toThrow("Authorization code not found");
|
||||||
|
|
@ -203,9 +176,11 @@ describe("EverythingAuthProvider", () => {
|
||||||
clientId: "different-client-id",
|
clientId: "different-client-id",
|
||||||
};
|
};
|
||||||
|
|
||||||
(authService.readPendingAuthorization as jest.Mock).mockResolvedValueOnce(pendingAuth);
|
// Save pending auth with different client ID
|
||||||
|
const authCode = authService.generateToken();
|
||||||
|
await authService.savePendingAuthorization(authCode, pendingAuth);
|
||||||
|
|
||||||
await expect(provider.challengeForAuthorizationCode(client, "test-code"))
|
await expect(provider.challengeForAuthorizationCode(client, authCode))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Authorization code does not match client");
|
.toThrow("Authorization code does not match client");
|
||||||
});
|
});
|
||||||
|
|
@ -214,31 +189,30 @@ describe("EverythingAuthProvider", () => {
|
||||||
describe("exchangeAuthorizationCode", () => {
|
describe("exchangeAuthorizationCode", () => {
|
||||||
it("returns tokens when exchange is successful", async () => {
|
it("returns tokens when exchange is successful", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
const tokenData: TokenExchange = {
|
|
||||||
mcpAccessToken: "test-access-token",
|
|
||||||
alreadyUsed: false,
|
|
||||||
};
|
|
||||||
const { mcpInstallation } = getMockAuthValues();
|
const { mcpInstallation } = getMockAuthValues();
|
||||||
|
|
||||||
(authService.exchangeToken as jest.Mock).mockResolvedValueOnce(tokenData);
|
// Setup: save token exchange and installation
|
||||||
(authService.readMcpInstallation as jest.Mock).mockResolvedValueOnce(mcpInstallation);
|
const authCode = authService.generateToken();
|
||||||
|
const tokenExchange: TokenExchange = {
|
||||||
|
mcpAccessToken: mcpInstallation.mcpTokens.access_token,
|
||||||
|
alreadyUsed: false,
|
||||||
|
};
|
||||||
|
await authService.saveTokenExchange(authCode, tokenExchange);
|
||||||
|
await authService.saveMcpInstallation(mcpInstallation.mcpTokens.access_token, mcpInstallation);
|
||||||
|
|
||||||
const result = await provider.exchangeAuthorizationCode(client, "test-code");
|
const result = await provider.exchangeAuthorizationCode(client, authCode);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
access_token: tokenData.mcpAccessToken,
|
access_token: mcpInstallation.mcpTokens.access_token,
|
||||||
expires_in: mcpInstallation.mcpTokens.expires_in,
|
expires_in: mcpInstallation.mcpTokens.expires_in,
|
||||||
refresh_token: mcpInstallation.mcpTokens.refresh_token,
|
refresh_token: mcpInstallation.mcpTokens.refresh_token,
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
});
|
});
|
||||||
expect(authService.exchangeToken).toHaveBeenCalledWith("test-code");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error for invalid authorization code", async () => {
|
it("throws error for invalid authorization code", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
|
|
||||||
(authService.exchangeToken as jest.Mock).mockResolvedValueOnce(undefined);
|
|
||||||
|
|
||||||
await expect(provider.exchangeAuthorizationCode(client, "test-code"))
|
await expect(provider.exchangeAuthorizationCode(client, "test-code"))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Invalid authorization code");
|
.toThrow("Invalid authorization code");
|
||||||
|
|
@ -250,37 +224,26 @@ describe("EverythingAuthProvider", () => {
|
||||||
const {
|
const {
|
||||||
client,
|
client,
|
||||||
accessToken,
|
accessToken,
|
||||||
newTokens,
|
|
||||||
mcpInstallation,
|
mcpInstallation,
|
||||||
} = getMockAuthValues();
|
} = getMockAuthValues();
|
||||||
|
|
||||||
|
// Setup: save refresh token and installation
|
||||||
|
const refreshToken = authService.generateToken();
|
||||||
|
await authService.saveRefreshToken(refreshToken, accessToken);
|
||||||
|
await authService.saveMcpInstallation(accessToken, mcpInstallation);
|
||||||
|
|
||||||
// Mock service calls
|
const result = await provider.exchangeRefreshToken(client, refreshToken);
|
||||||
(authService.readRefreshToken as jest.Mock).mockResolvedValueOnce(accessToken);
|
|
||||||
(authService.readMcpInstallation as jest.Mock).mockResolvedValueOnce(mcpInstallation);
|
|
||||||
(authService.generateMcpTokens as jest.Mock).mockReturnValueOnce(newTokens);
|
|
||||||
|
|
||||||
const result = await provider.exchangeRefreshToken(client, "test-refresh-token");
|
// Should return new tokens
|
||||||
|
expect(result).toHaveProperty('access_token');
|
||||||
expect(result).toEqual(newTokens);
|
expect(result).toHaveProperty('refresh_token');
|
||||||
expect(authService.readRefreshToken).toHaveBeenCalledWith("test-refresh-token");
|
expect(result).toHaveProperty('expires_in', 3600);
|
||||||
expect(authService.readMcpInstallation).toHaveBeenCalledWith(accessToken);
|
expect(result).toHaveProperty('token_type', 'Bearer');
|
||||||
expect(authService.saveRefreshToken).toHaveBeenCalledWith(newTokens.refresh_token, newTokens.access_token);
|
|
||||||
expect(authService.saveMcpInstallation).toHaveBeenCalledWith(
|
|
||||||
newTokens.access_token,
|
|
||||||
expect.objectContaining({
|
|
||||||
...mcpInstallation,
|
|
||||||
mcpTokens: newTokens,
|
|
||||||
issuedAt: expect.any(Number),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error for invalid refresh token", async () => {
|
it("throws error for invalid refresh token", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
|
|
||||||
(authService.readRefreshToken as jest.Mock).mockResolvedValueOnce(undefined);
|
|
||||||
|
|
||||||
await expect(provider.exchangeRefreshToken(client, "test-refresh-token"))
|
await expect(provider.exchangeRefreshToken(client, "test-refresh-token"))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Invalid refresh token");
|
.toThrow("Invalid refresh token");
|
||||||
|
|
@ -288,18 +251,21 @@ describe("EverythingAuthProvider", () => {
|
||||||
|
|
||||||
it("throws error for refresh token with no installation", async () => {
|
it("throws error for refresh token with no installation", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
|
const refreshToken = authService.generateToken();
|
||||||
|
const accessToken = authService.generateToken();
|
||||||
|
|
||||||
(authService.readRefreshToken as jest.Mock).mockResolvedValueOnce("test-access-token");
|
// Only save refresh token, not the installation
|
||||||
(authService.readMcpInstallation as jest.Mock).mockResolvedValueOnce(undefined);
|
await authService.saveRefreshToken(refreshToken, accessToken);
|
||||||
|
|
||||||
await expect(provider.exchangeRefreshToken(client, "test-refresh-token"))
|
await expect(provider.exchangeRefreshToken(client, refreshToken))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Invalid refresh token");
|
.toThrow("Invalid refresh token");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error when client ID doesn't match", async () => {
|
it("throws error when client ID doesn't match", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
const accessToken = "test-access-token";
|
const accessToken = authService.generateToken();
|
||||||
|
const refreshToken = authService.generateToken();
|
||||||
|
|
||||||
const mcpInstallation: McpInstallation = {
|
const mcpInstallation: McpInstallation = {
|
||||||
fakeUpstreamInstallation: {
|
fakeUpstreamInstallation: {
|
||||||
|
|
@ -315,10 +281,10 @@ describe("EverythingAuthProvider", () => {
|
||||||
issuedAt: Date.now() / 1000,
|
issuedAt: Date.now() / 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
(authService.readRefreshToken as jest.Mock).mockResolvedValueOnce(accessToken);
|
await authService.saveRefreshToken(refreshToken, accessToken);
|
||||||
(authService.readMcpInstallation as jest.Mock).mockResolvedValueOnce(mcpInstallation);
|
await authService.saveMcpInstallation(accessToken, mcpInstallation);
|
||||||
|
|
||||||
await expect(provider.exchangeRefreshToken(client, "test-refresh-token"))
|
await expect(provider.exchangeRefreshToken(client, refreshToken))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Invalid client");
|
.toThrow("Invalid client");
|
||||||
});
|
});
|
||||||
|
|
@ -327,21 +293,15 @@ describe("EverythingAuthProvider", () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
|
|
||||||
// Simulate the refresh token not being found in Redis (expired)
|
// Simulate the refresh token not being found in Redis (expired)
|
||||||
(authService.readRefreshToken as jest.Mock).mockResolvedValueOnce(undefined);
|
|
||||||
|
|
||||||
await expect(provider.exchangeRefreshToken(client, "expired-refresh-token"))
|
await expect(provider.exchangeRefreshToken(client, "expired-refresh-token"))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Invalid refresh token");
|
.toThrow("Invalid refresh token");
|
||||||
|
|
||||||
expect(authService.readRefreshToken).toHaveBeenCalledWith("expired-refresh-token");
|
|
||||||
// Should not proceed to check the installation since the refresh token lookup failed
|
|
||||||
expect(authService.readMcpInstallation).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("verifyAccessToken", () => {
|
describe("verifyAccessToken", () => {
|
||||||
it("returns auth info for valid token", async () => {
|
it("returns auth info for valid token", async () => {
|
||||||
const accessToken = "test-access-token";
|
const accessToken = authService.generateToken();
|
||||||
const mcpInstallation: McpInstallation = {
|
const mcpInstallation: McpInstallation = {
|
||||||
fakeUpstreamInstallation: {
|
fakeUpstreamInstallation: {
|
||||||
fakeAccessTokenForDemonstration: "fake-upstream-access-token",
|
fakeAccessTokenForDemonstration: "fake-upstream-access-token",
|
||||||
|
|
@ -356,7 +316,7 @@ describe("EverythingAuthProvider", () => {
|
||||||
issuedAt: Date.now() / 1000,
|
issuedAt: Date.now() / 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
(authService.readMcpInstallation as jest.Mock).mockResolvedValueOnce(mcpInstallation);
|
await authService.saveMcpInstallation(accessToken, mcpInstallation);
|
||||||
|
|
||||||
const result = await provider.verifyAccessToken(accessToken);
|
const result = await provider.verifyAccessToken(accessToken);
|
||||||
|
|
||||||
|
|
@ -369,15 +329,13 @@ describe("EverythingAuthProvider", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error for invalid token", async () => {
|
it("throws error for invalid token", async () => {
|
||||||
(authService.readMcpInstallation as jest.Mock).mockResolvedValueOnce(undefined);
|
|
||||||
|
|
||||||
await expect(provider.verifyAccessToken("invalid-token"))
|
await expect(provider.verifyAccessToken("invalid-token"))
|
||||||
.rejects
|
.rejects
|
||||||
.toThrow("Invalid access token");
|
.toThrow("Invalid access token");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws InvalidTokenError for expired token", async () => {
|
it("throws InvalidTokenError for expired token", async () => {
|
||||||
const accessToken = "test-access-token";
|
const accessToken = authService.generateToken();
|
||||||
const oneDayInSeconds = 24 * 60 * 60;
|
const oneDayInSeconds = 24 * 60 * 60;
|
||||||
const twoDaysAgoInSeconds = Math.floor(Date.now() / 1000) - (2 * oneDayInSeconds);
|
const twoDaysAgoInSeconds = Math.floor(Date.now() / 1000) - (2 * oneDayInSeconds);
|
||||||
|
|
||||||
|
|
@ -395,7 +353,7 @@ describe("EverythingAuthProvider", () => {
|
||||||
issuedAt: twoDaysAgoInSeconds, // 2 days ago, with 1-day expiry
|
issuedAt: twoDaysAgoInSeconds, // 2 days ago, with 1-day expiry
|
||||||
};
|
};
|
||||||
|
|
||||||
(authService.readMcpInstallation as jest.Mock).mockResolvedValueOnce(mcpInstallation);
|
await authService.saveMcpInstallation(accessToken, mcpInstallation);
|
||||||
|
|
||||||
await expect(provider.verifyAccessToken(accessToken))
|
await expect(provider.verifyAccessToken(accessToken))
|
||||||
.rejects
|
.rejects
|
||||||
|
|
@ -404,16 +362,41 @@ describe("EverythingAuthProvider", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("revokeToken", () => {
|
describe("revokeToken", () => {
|
||||||
it("calls revokeMcpInstallation with token", async () => {
|
it("revokes the installation when given an access token", async () => {
|
||||||
const client = createTestClient();
|
const client = createTestClient();
|
||||||
|
const accessToken = authService.generateToken();
|
||||||
|
const mcpInstallation: McpInstallation = {
|
||||||
|
fakeUpstreamInstallation: {
|
||||||
|
fakeAccessTokenForDemonstration: "fake-upstream-access-token",
|
||||||
|
fakeRefreshTokenForDemonstration: "fake-upstream-refresh-token",
|
||||||
|
},
|
||||||
|
mcpTokens: {
|
||||||
|
access_token: accessToken,
|
||||||
|
token_type: "Bearer",
|
||||||
|
expires_in: 3600,
|
||||||
|
},
|
||||||
|
clientId: client.client_id,
|
||||||
|
issuedAt: Date.now() / 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the installation
|
||||||
|
await authService.saveMcpInstallation(accessToken, mcpInstallation);
|
||||||
|
|
||||||
|
// Verify it exists
|
||||||
|
const saved = await authService.readMcpInstallation(accessToken);
|
||||||
|
expect(saved).toBeTruthy();
|
||||||
|
|
||||||
|
// Revoke it
|
||||||
const request: OAuthTokenRevocationRequest = {
|
const request: OAuthTokenRevocationRequest = {
|
||||||
token: "test-token",
|
token: accessToken,
|
||||||
token_type_hint: "access_token"
|
token_type_hint: "access_token"
|
||||||
};
|
};
|
||||||
|
|
||||||
await provider.revokeToken(client, request);
|
await provider.revokeToken(client, request);
|
||||||
|
|
||||||
expect(authService.revokeMcpInstallation).toHaveBeenCalledWith(request.token);
|
// Verify it was revoked
|
||||||
|
const revoked = await authService.readMcpInstallation(accessToken);
|
||||||
|
expect(revoked).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -3,7 +3,6 @@ import { OAuthServerProvider, AuthorizationParams } from '@modelcontextprotocol/
|
||||||
import { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js';
|
import { OAuthRegisteredClientsStore } from '@modelcontextprotocol/sdk/server/auth/clients.js';
|
||||||
import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
|
import { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
|
||||||
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
||||||
import { BASE_URI } from '../config.js';
|
|
||||||
import {
|
import {
|
||||||
exchangeToken,
|
exchangeToken,
|
||||||
generateToken,
|
generateToken,
|
||||||
|
|
@ -101,8 +100,6 @@ export class EverythingAuthProvider implements OAuthServerProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingAuth.clientId !== client.client_id) {
|
if (pendingAuth.clientId !== client.client_id) {
|
||||||
console.log('Pending auth clientId', pendingAuth.clientId);
|
|
||||||
console.log('ClientId', client.client_id);
|
|
||||||
throw new Error('Authorization code does not match client');
|
throw new Error('Authorization code does not match client');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { describe, test, expect } from '@jest/globals';
|
||||||
import { getFakeUpstreamInstallation, getMcpAccessToken, withContext } from "./context.js";
|
import { getFakeUpstreamInstallation, getMcpAccessToken, withContext } from "./context.js";
|
||||||
import { generateToken } from "./services/auth.js";
|
import { generateToken } from "./services/auth.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import {
|
import {
|
||||||
decryptString,
|
decryptString,
|
||||||
|
|
@ -54,7 +55,7 @@ describe("auth utils", () => {
|
||||||
expect(tokens.refresh_token).toBeDefined();
|
expect(tokens.refresh_token).toBeDefined();
|
||||||
expect(tokens.refresh_token).toMatch(/^[0-9a-f]{64}$/);
|
expect(tokens.refresh_token).toMatch(/^[0-9a-f]{64}$/);
|
||||||
|
|
||||||
expect(tokens.token_type).toBe("bearer");
|
expect(tokens.token_type).toBe("Bearer");
|
||||||
expect(tokens.expires_in).toBeDefined();
|
expect(tokens.expires_in).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ export function generateMcpTokens(): OAuthTokens {
|
||||||
access_token: mcpAccessToken,
|
access_token: mcpAccessToken,
|
||||||
refresh_token: mcpRefreshToken,
|
refresh_token: mcpRefreshToken,
|
||||||
expires_in: ACCESS_TOKEN_EXPIRY_SEC,
|
expires_in: ACCESS_TOKEN_EXPIRY_SEC,
|
||||||
token_type: "bearer",
|
token_type: "Bearer",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue