mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
294 lines
9.0 KiB
TypeScript
294 lines
9.0 KiB
TypeScript
import {
|
|
createOAuthClient,
|
|
findOAuthClientById,
|
|
updateOAuthClient,
|
|
deleteOAuthClient,
|
|
saveAuthorizationCode,
|
|
getAuthorizationCode,
|
|
revokeAuthorizationCode,
|
|
saveToken,
|
|
getToken,
|
|
revokeToken,
|
|
} from '../../src/models/OAuth.js';
|
|
import { IOAuthClient, IOAuthToken } from '../../src/types/index.js';
|
|
|
|
// Mock in-memory storage for OAuth clients and tokens
|
|
let mockOAuthClients: IOAuthClient[] = [];
|
|
let mockOAuthTokens: IOAuthToken[] = [];
|
|
|
|
// Mock the DAO factory to use in-memory storage for tests
|
|
jest.mock('../../src/dao/index.js', () => {
|
|
const originalModule = jest.requireActual('../../src/dao/index.js');
|
|
|
|
return {
|
|
...originalModule,
|
|
getOAuthClientDao: jest.fn(() => ({
|
|
findAll: jest.fn(async () => [...mockOAuthClients]),
|
|
findByClientId: jest.fn(
|
|
async (clientId: string) => mockOAuthClients.find((c) => c.clientId === clientId) || null,
|
|
),
|
|
create: jest.fn(async (client: IOAuthClient) => {
|
|
mockOAuthClients.push(client);
|
|
return client;
|
|
}),
|
|
update: jest.fn(async (clientId: string, updates: Partial<IOAuthClient>) => {
|
|
const index = mockOAuthClients.findIndex((c) => c.clientId === clientId);
|
|
if (index === -1) return null;
|
|
mockOAuthClients[index] = { ...mockOAuthClients[index], ...updates };
|
|
return mockOAuthClients[index];
|
|
}),
|
|
delete: jest.fn(async (clientId: string) => {
|
|
const index = mockOAuthClients.findIndex((c) => c.clientId === clientId);
|
|
if (index === -1) return false;
|
|
mockOAuthClients.splice(index, 1);
|
|
return true;
|
|
}),
|
|
})),
|
|
getOAuthTokenDao: jest.fn(() => ({
|
|
findAll: jest.fn(async () => [...mockOAuthTokens]),
|
|
findByAccessToken: jest.fn(
|
|
async (accessToken: string) =>
|
|
mockOAuthTokens.find((t) => t.accessToken === accessToken) || null,
|
|
),
|
|
findByRefreshToken: jest.fn(
|
|
async (refreshToken: string) =>
|
|
mockOAuthTokens.find((t) => t.refreshToken === refreshToken) || null,
|
|
),
|
|
create: jest.fn(async (token: IOAuthToken) => {
|
|
mockOAuthTokens.push(token);
|
|
return token;
|
|
}),
|
|
revokeToken: jest.fn(async (token: string) => {
|
|
const index = mockOAuthTokens.findIndex(
|
|
(t) => t.accessToken === token || t.refreshToken === token,
|
|
);
|
|
if (index === -1) return false;
|
|
mockOAuthTokens.splice(index, 1);
|
|
return true;
|
|
}),
|
|
cleanupExpired: jest.fn(async () => {
|
|
const now = new Date();
|
|
mockOAuthTokens = mockOAuthTokens.filter((t) => {
|
|
const accessExpired = t.accessTokenExpiresAt < now;
|
|
const refreshExpired =
|
|
!t.refreshToken || (t.refreshTokenExpiresAt && t.refreshTokenExpiresAt < now);
|
|
return !accessExpired || !refreshExpired;
|
|
});
|
|
}),
|
|
})),
|
|
};
|
|
});
|
|
|
|
describe('OAuth Model', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
// Reset mock storage before each test
|
|
mockOAuthClients = [];
|
|
mockOAuthTokens = [];
|
|
});
|
|
|
|
describe('OAuth Client Management', () => {
|
|
test('should create a new OAuth client', async () => {
|
|
const client: IOAuthClient = {
|
|
clientId: 'test-client',
|
|
clientSecret: 'test-secret',
|
|
name: 'Test Client',
|
|
redirectUris: ['http://localhost:3000/callback'],
|
|
grants: ['authorization_code', 'refresh_token'],
|
|
scopes: ['read', 'write'],
|
|
};
|
|
|
|
const created = await createOAuthClient(client);
|
|
expect(created).toEqual(client);
|
|
|
|
const found = await findOAuthClientById('test-client');
|
|
expect(found).toEqual(client);
|
|
});
|
|
|
|
test('should not create duplicate OAuth client', async () => {
|
|
const client: IOAuthClient = {
|
|
clientId: 'test-client',
|
|
clientSecret: 'test-secret',
|
|
name: 'Test Client',
|
|
redirectUris: ['http://localhost:3000/callback'],
|
|
grants: ['authorization_code'],
|
|
scopes: ['read'],
|
|
};
|
|
|
|
await createOAuthClient(client);
|
|
await expect(createOAuthClient(client)).rejects.toThrow();
|
|
});
|
|
|
|
test('should update an OAuth client', async () => {
|
|
const client: IOAuthClient = {
|
|
clientId: 'test-client',
|
|
clientSecret: 'test-secret',
|
|
name: 'Test Client',
|
|
redirectUris: ['http://localhost:3000/callback'],
|
|
grants: ['authorization_code'],
|
|
scopes: ['read'],
|
|
};
|
|
|
|
await createOAuthClient(client);
|
|
|
|
const updated = await updateOAuthClient('test-client', {
|
|
name: 'Updated Client',
|
|
scopes: ['read', 'write'],
|
|
});
|
|
|
|
expect(updated?.name).toBe('Updated Client');
|
|
expect(updated?.scopes).toEqual(['read', 'write']);
|
|
});
|
|
|
|
test('should delete an OAuth client', async () => {
|
|
const client: IOAuthClient = {
|
|
clientId: 'test-client',
|
|
clientSecret: 'test-secret',
|
|
name: 'Test Client',
|
|
redirectUris: ['http://localhost:3000/callback'],
|
|
grants: ['authorization_code'],
|
|
scopes: ['read'],
|
|
};
|
|
|
|
await createOAuthClient(client);
|
|
expect(await findOAuthClientById('test-client')).toBeDefined();
|
|
|
|
const deleted = await deleteOAuthClient('test-client');
|
|
expect(deleted).toBe(true);
|
|
expect(await findOAuthClientById('test-client')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Authorization Code Management', () => {
|
|
test('should save and retrieve authorization code', () => {
|
|
const code = saveAuthorizationCode({
|
|
redirectUri: 'http://localhost:3000/callback',
|
|
scope: 'read write',
|
|
clientId: 'test-client',
|
|
username: 'testuser',
|
|
codeChallenge: 'test-challenge',
|
|
codeChallengeMethod: 'S256',
|
|
});
|
|
|
|
expect(code).toBeDefined();
|
|
expect(typeof code).toBe('string');
|
|
|
|
const retrieved = getAuthorizationCode(code);
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved?.redirectUri).toBe('http://localhost:3000/callback');
|
|
expect(retrieved?.clientId).toBe('test-client');
|
|
expect(retrieved?.username).toBe('testuser');
|
|
});
|
|
|
|
test('should not retrieve expired authorization code', async () => {
|
|
const code = saveAuthorizationCode(
|
|
{
|
|
redirectUri: 'http://localhost:3000/callback',
|
|
scope: 'read',
|
|
clientId: 'test-client',
|
|
username: 'testuser',
|
|
},
|
|
-1, // Expired
|
|
);
|
|
|
|
// Wait a bit to ensure expiration
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const retrieved = getAuthorizationCode(code);
|
|
expect(retrieved).toBeUndefined();
|
|
});
|
|
|
|
test('should revoke authorization code', () => {
|
|
const code = saveAuthorizationCode({
|
|
redirectUri: 'http://localhost:3000/callback',
|
|
scope: 'read',
|
|
clientId: 'test-client',
|
|
username: 'testuser',
|
|
});
|
|
|
|
expect(getAuthorizationCode(code)).toBeDefined();
|
|
|
|
revokeAuthorizationCode(code);
|
|
expect(getAuthorizationCode(code)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Token Management', () => {
|
|
test('should save and retrieve token', async () => {
|
|
const token = await saveToken(
|
|
{
|
|
scope: 'read write',
|
|
clientId: 'test-client',
|
|
username: 'testuser',
|
|
},
|
|
3600, // accessTokenLifetime
|
|
86400, // refreshTokenLifetime
|
|
);
|
|
|
|
expect(token.accessToken).toBeDefined();
|
|
expect(token.refreshToken).toBeDefined();
|
|
expect(token.accessTokenExpiresAt).toBeInstanceOf(Date);
|
|
|
|
const retrieved = await getToken(token.accessToken);
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved?.clientId).toBe('test-client');
|
|
expect(retrieved?.username).toBe('testuser');
|
|
});
|
|
|
|
test('should retrieve token by refresh token', async () => {
|
|
const token = await saveToken(
|
|
{
|
|
scope: 'read',
|
|
clientId: 'test-client',
|
|
username: 'testuser',
|
|
},
|
|
3600,
|
|
86400,
|
|
);
|
|
|
|
expect(token.refreshToken).toBeDefined();
|
|
|
|
const retrieved = await getToken(token.refreshToken!);
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved?.accessToken).toBe(token.accessToken);
|
|
});
|
|
|
|
test('should not retrieve expired access token', async () => {
|
|
const token = await saveToken(
|
|
{
|
|
scope: 'read',
|
|
clientId: 'test-client',
|
|
username: 'testuser',
|
|
},
|
|
-1, // Expired
|
|
);
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const retrieved = await getToken(token.accessToken);
|
|
expect(retrieved).toBeUndefined();
|
|
});
|
|
|
|
test('should revoke token', async () => {
|
|
const token = await saveToken(
|
|
{
|
|
scope: 'read',
|
|
clientId: 'test-client',
|
|
username: 'testuser',
|
|
},
|
|
3600,
|
|
86400,
|
|
);
|
|
|
|
expect(await getToken(token.accessToken)).toBeDefined();
|
|
|
|
await revokeToken(token.accessToken);
|
|
expect(await getToken(token.accessToken)).toBeUndefined();
|
|
|
|
if (token.refreshToken) {
|
|
expect(await getToken(token.refreshToken)).toBeUndefined();
|
|
}
|
|
});
|
|
});
|
|
});
|