buroventures-harald-code-core
Version:
Harald Code Core - Core functionality for AI-powered coding assistant
521 lines • 24 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as http from 'node:http';
import * as crypto from 'node:crypto';
import { MCPOAuthProvider, } from './oauth-provider.js';
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
// Mock dependencies
const mockOpenBrowserSecurely = vi.hoisted(() => vi.fn());
vi.mock('../utils/secure-browser-launcher.js', () => ({
openBrowserSecurely: mockOpenBrowserSecurely,
}));
vi.mock('node:crypto');
vi.mock('./oauth-token-storage.js');
// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;
// Define a reusable mock server with .listen, .close, and .on methods
const mockHttpServer = {
listen: vi.fn(),
close: vi.fn(),
on: vi.fn(),
};
vi.mock('node:http', () => ({
createServer: vi.fn(() => mockHttpServer),
}));
describe('MCPOAuthProvider', () => {
const mockConfig = {
enabled: true,
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
authorizationUrl: 'https://auth.example.com/authorize',
tokenUrl: 'https://auth.example.com/token',
scopes: ['read', 'write'],
redirectUri: 'http://localhost:7777/oauth/callback',
};
const mockToken = {
accessToken: 'access_token_123',
refreshToken: 'refresh_token_456',
tokenType: 'Bearer',
scope: 'read write',
expiresAt: Date.now() + 3600000,
};
const mockTokenResponse = {
access_token: 'access_token_123',
token_type: 'Bearer',
expires_in: 3600,
refresh_token: 'refresh_token_456',
scope: 'read write',
};
beforeEach(() => {
vi.clearAllMocks();
mockOpenBrowserSecurely.mockClear();
vi.spyOn(console, 'log').mockImplementation(() => { });
vi.spyOn(console, 'warn').mockImplementation(() => { });
vi.spyOn(console, 'error').mockImplementation(() => { });
// Mock crypto functions
vi.mocked(crypto.randomBytes).mockImplementation((size) => {
if (size === 32)
return Buffer.from('code_verifier_mock_32_bytes_long');
if (size === 16)
return Buffer.from('state_mock_16_by');
return Buffer.alloc(size);
});
vi.mocked(crypto.createHash).mockReturnValue({
update: vi.fn().mockReturnThis(),
digest: vi.fn().mockReturnValue('code_challenge_mock'),
});
// Mock randomBytes to return predictable values for state
vi.mocked(crypto.randomBytes).mockImplementation((size) => {
if (size === 32) {
return Buffer.from('mock_code_verifier_32_bytes_long_string');
}
else if (size === 16) {
return Buffer.from('mock_state_16_bytes');
}
return Buffer.alloc(size);
});
// Mock token storage
vi.mocked(MCPOAuthTokenStorage.saveToken).mockResolvedValue(undefined);
vi.mocked(MCPOAuthTokenStorage.getToken).mockResolvedValue(null);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('authenticate', () => {
it('should perform complete OAuth flow with PKCE', async () => {
// Mock HTTP server callback
let callbackHandler;
vi.mocked(http.createServer).mockImplementation((handler) => {
callbackHandler = handler;
return mockHttpServer;
});
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
// Simulate OAuth callback
setTimeout(() => {
const mockReq = {
url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
};
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
};
callbackHandler(mockReq, mockRes);
}, 10);
});
// Mock token exchange
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTokenResponse),
});
const result = await MCPOAuthProvider.authenticate('test-server', mockConfig);
expect(result).toEqual({
accessToken: 'access_token_123',
refreshToken: 'refresh_token_456',
tokenType: 'Bearer',
scope: 'read write',
expiresAt: expect.any(Number),
});
expect(mockOpenBrowserSecurely).toHaveBeenCalledWith(expect.stringContaining('authorize'));
expect(MCPOAuthTokenStorage.saveToken).toHaveBeenCalledWith('test-server', expect.objectContaining({ accessToken: 'access_token_123' }), 'test-client-id', 'https://auth.example.com/token', undefined);
});
it('should handle OAuth discovery when no authorization URL provided', async () => {
// Use a mutable config object
const configWithoutAuth = { ...mockConfig };
delete configWithoutAuth.authorizationUrl;
delete configWithoutAuth.tokenUrl;
const mockResourceMetadata = {
authorization_servers: ['https://discovered.auth.com'],
};
const mockAuthServerMetadata = {
authorization_endpoint: 'https://discovered.auth.com/authorize',
token_endpoint: 'https://discovered.auth.com/token',
scopes_supported: ['read', 'write'],
};
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResourceMetadata),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockAuthServerMetadata),
});
// Patch config after discovery
configWithoutAuth.authorizationUrl =
mockAuthServerMetadata.authorization_endpoint;
configWithoutAuth.tokenUrl = mockAuthServerMetadata.token_endpoint;
configWithoutAuth.scopes = mockAuthServerMetadata.scopes_supported;
// Setup callback handler
let callbackHandler;
vi.mocked(http.createServer).mockImplementation((handler) => {
callbackHandler = handler;
return mockHttpServer;
});
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
setTimeout(() => {
const mockReq = {
url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
};
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
};
callbackHandler(mockReq, mockRes);
}, 10);
});
// Mock token exchange with discovered endpoint
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTokenResponse),
});
const result = await MCPOAuthProvider.authenticate('test-server', configWithoutAuth, 'https://api.example.com');
expect(result).toBeDefined();
expect(mockFetch).toHaveBeenCalledWith('https://discovered.auth.com/token', expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}));
});
it('should perform dynamic client registration when no client ID provided', async () => {
const configWithoutClient = { ...mockConfig };
delete configWithoutClient.clientId;
const mockRegistrationResponse = {
client_id: 'test_client_id',
client_secret: 'test_client_secret',
redirect_uris: ['http://localhost:7777/oauth/callback'],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none',
};
const mockAuthServerMetadata = {
authorization_endpoint: 'https://auth.example.com/authorize',
token_endpoint: 'https://auth.example.com/token',
registration_endpoint: 'https://auth.example.com/register',
};
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockAuthServerMetadata),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockRegistrationResponse),
});
// Setup callback handler
let callbackHandler;
vi.mocked(http.createServer).mockImplementation((handler) => {
callbackHandler = handler;
return mockHttpServer;
});
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
setTimeout(() => {
const mockReq = {
url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
};
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
};
callbackHandler(mockReq, mockRes);
}, 10);
});
// Mock token exchange
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTokenResponse),
});
const result = await MCPOAuthProvider.authenticate('test-server', configWithoutClient);
expect(result).toBeDefined();
expect(mockFetch).toHaveBeenCalledWith('https://auth.example.com/register', expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}));
});
it('should handle OAuth callback errors', async () => {
let callbackHandler;
vi.mocked(http.createServer).mockImplementation((handler) => {
callbackHandler = handler;
return mockHttpServer;
});
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
setTimeout(() => {
const mockReq = {
url: '/oauth/callback?error=access_denied&error_description=User%20denied%20access',
};
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
};
callbackHandler(mockReq, mockRes);
}, 10);
});
await expect(MCPOAuthProvider.authenticate('test-server', mockConfig)).rejects.toThrow('OAuth error: access_denied');
});
it('should handle state mismatch in callback', async () => {
let callbackHandler;
vi.mocked(http.createServer).mockImplementation((handler) => {
callbackHandler = handler;
return mockHttpServer;
});
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
setTimeout(() => {
const mockReq = {
url: '/oauth/callback?code=auth_code_123&state=wrong_state',
};
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
};
callbackHandler(mockReq, mockRes);
}, 10);
});
await expect(MCPOAuthProvider.authenticate('test-server', mockConfig)).rejects.toThrow('State mismatch - possible CSRF attack');
});
it('should handle token exchange failure', async () => {
let callbackHandler;
vi.mocked(http.createServer).mockImplementation((handler) => {
callbackHandler = handler;
return mockHttpServer;
});
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
setTimeout(() => {
const mockReq = {
url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
};
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
};
callbackHandler(mockReq, mockRes);
}, 10);
});
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: () => Promise.resolve('Invalid grant'),
});
await expect(MCPOAuthProvider.authenticate('test-server', mockConfig)).rejects.toThrow('Token exchange failed: 400 - Invalid grant');
});
it('should handle callback timeout', async () => {
vi.mocked(http.createServer).mockImplementation(() => mockHttpServer);
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
// Don't trigger callback - simulate timeout
});
// Mock setTimeout to trigger timeout immediately
const originalSetTimeout = global.setTimeout;
global.setTimeout = vi.fn((callback, delay) => {
if (delay === 5 * 60 * 1000) {
// 5 minute timeout
callback();
}
return originalSetTimeout(callback, 0);
});
await expect(MCPOAuthProvider.authenticate('test-server', mockConfig)).rejects.toThrow('OAuth callback timeout');
global.setTimeout = originalSetTimeout;
});
});
describe('refreshAccessToken', () => {
it('should refresh token successfully', async () => {
const refreshResponse = {
access_token: 'new_access_token',
token_type: 'Bearer',
expires_in: 3600,
refresh_token: 'new_refresh_token',
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(refreshResponse),
});
const result = await MCPOAuthProvider.refreshAccessToken(mockConfig, 'old_refresh_token', 'https://auth.example.com/token');
expect(result).toEqual(refreshResponse);
expect(mockFetch).toHaveBeenCalledWith('https://auth.example.com/token', expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: expect.stringContaining('grant_type=refresh_token'),
}));
});
it('should include client secret in refresh request when available', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTokenResponse),
});
await MCPOAuthProvider.refreshAccessToken(mockConfig, 'refresh_token', 'https://auth.example.com/token');
const fetchCall = mockFetch.mock.calls[0];
expect(fetchCall[1].body).toContain('client_secret=test-client-secret');
});
it('should handle refresh token failure', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: () => Promise.resolve('Invalid refresh token'),
});
await expect(MCPOAuthProvider.refreshAccessToken(mockConfig, 'invalid_refresh_token', 'https://auth.example.com/token')).rejects.toThrow('Token refresh failed: 400 - Invalid refresh token');
});
});
describe('getValidToken', () => {
it('should return valid token when not expired', async () => {
const validCredentials = {
serverName: 'test-server',
token: mockToken,
clientId: 'test-client-id',
tokenUrl: 'https://auth.example.com/token',
updatedAt: Date.now(),
};
vi.mocked(MCPOAuthTokenStorage.getToken).mockResolvedValue(validCredentials);
vi.mocked(MCPOAuthTokenStorage.isTokenExpired).mockReturnValue(false);
const result = await MCPOAuthProvider.getValidToken('test-server', mockConfig);
expect(result).toBe('access_token_123');
});
it('should refresh expired token and return new token', async () => {
const expiredCredentials = {
serverName: 'test-server',
token: { ...mockToken, expiresAt: Date.now() - 3600000 },
clientId: 'test-client-id',
tokenUrl: 'https://auth.example.com/token',
updatedAt: Date.now(),
};
vi.mocked(MCPOAuthTokenStorage.getToken).mockResolvedValue(expiredCredentials);
vi.mocked(MCPOAuthTokenStorage.isTokenExpired).mockReturnValue(true);
const refreshResponse = {
access_token: 'new_access_token',
token_type: 'Bearer',
expires_in: 3600,
refresh_token: 'new_refresh_token',
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(refreshResponse),
});
const result = await MCPOAuthProvider.getValidToken('test-server', mockConfig);
expect(result).toBe('new_access_token');
expect(MCPOAuthTokenStorage.saveToken).toHaveBeenCalledWith('test-server', expect.objectContaining({ accessToken: 'new_access_token' }), 'test-client-id', 'https://auth.example.com/token', undefined);
});
it('should return null when no credentials exist', async () => {
vi.mocked(MCPOAuthTokenStorage.getToken).mockResolvedValue(null);
const result = await MCPOAuthProvider.getValidToken('test-server', mockConfig);
expect(result).toBeNull();
});
it('should handle refresh failure and remove invalid token', async () => {
const expiredCredentials = {
serverName: 'test-server',
token: { ...mockToken, expiresAt: Date.now() - 3600000 },
clientId: 'test-client-id',
tokenUrl: 'https://auth.example.com/token',
updatedAt: Date.now(),
};
vi.mocked(MCPOAuthTokenStorage.getToken).mockResolvedValue(expiredCredentials);
vi.mocked(MCPOAuthTokenStorage.isTokenExpired).mockReturnValue(true);
vi.mocked(MCPOAuthTokenStorage.removeToken).mockResolvedValue(undefined);
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: () => Promise.resolve('Invalid refresh token'),
});
const result = await MCPOAuthProvider.getValidToken('test-server', mockConfig);
expect(result).toBeNull();
expect(MCPOAuthTokenStorage.removeToken).toHaveBeenCalledWith('test-server');
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Failed to refresh token'));
});
it('should return null for token without refresh capability', async () => {
const tokenWithoutRefresh = {
serverName: 'test-server',
token: {
...mockToken,
refreshToken: undefined,
expiresAt: Date.now() - 3600000,
},
clientId: 'test-client-id',
tokenUrl: 'https://auth.example.com/token',
updatedAt: Date.now(),
};
vi.mocked(MCPOAuthTokenStorage.getToken).mockResolvedValue(tokenWithoutRefresh);
vi.mocked(MCPOAuthTokenStorage.isTokenExpired).mockReturnValue(true);
const result = await MCPOAuthProvider.getValidToken('test-server', mockConfig);
expect(result).toBeNull();
});
});
describe('PKCE parameter generation', () => {
it('should generate valid PKCE parameters', async () => {
// Test is implicit in the authenticate flow tests, but we can verify
// the crypto mocks are called correctly
let callbackHandler;
vi.mocked(http.createServer).mockImplementation((handler) => {
callbackHandler = handler;
return mockHttpServer;
});
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
setTimeout(() => {
const mockReq = {
url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
};
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
};
callbackHandler(mockReq, mockRes);
}, 10);
});
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTokenResponse),
});
await MCPOAuthProvider.authenticate('test-server', mockConfig);
expect(crypto.randomBytes).toHaveBeenCalledWith(32); // code verifier
expect(crypto.randomBytes).toHaveBeenCalledWith(16); // state
expect(crypto.createHash).toHaveBeenCalledWith('sha256');
});
});
describe('Authorization URL building', () => {
it('should build correct authorization URL with all parameters', async () => {
// Mock to capture the URL that would be opened
let capturedUrl;
mockOpenBrowserSecurely.mockImplementation((url) => {
capturedUrl = url;
return Promise.resolve();
});
let callbackHandler;
vi.mocked(http.createServer).mockImplementation((handler) => {
callbackHandler = handler;
return mockHttpServer;
});
mockHttpServer.listen.mockImplementation((port, callback) => {
callback?.();
setTimeout(() => {
const mockReq = {
url: '/oauth/callback?code=auth_code_123&state=bW9ja19zdGF0ZV8xNl9ieXRlcw',
};
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
};
callbackHandler(mockReq, mockRes);
}, 10);
});
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockTokenResponse),
});
await MCPOAuthProvider.authenticate('test-server', mockConfig);
expect(capturedUrl).toBeDefined();
expect(capturedUrl).toContain('response_type=code');
expect(capturedUrl).toContain('client_id=test-client-id');
expect(capturedUrl).toContain('code_challenge=code_challenge_mock');
expect(capturedUrl).toContain('code_challenge_method=S256');
expect(capturedUrl).toContain('scope=read+write');
expect(capturedUrl).toContain('resource=https%3A%2F%2Fauth.example.com');
});
});
});
//# sourceMappingURL=oauth-provider.test.js.map