Skip to main content
Glama
mock-gitlab-server.ts8.97 kB
/** * Mock GitLab API Server for Testing * Implements minimal GitLab API endpoints for testing remote authorization */ import express, { Request, Response, NextFunction } from 'express'; import { Server } from 'http'; export interface MockGitLabConfig { port: number; validTokens: string[]; /** Artificial delay for all responses (ms) - useful for timeout testing */ responseDelay?: number; /** Simulate rate limiting - return 429 after N requests */ rateLimitAfter?: number; } interface AuthenticatedRequest extends Request { gitlabToken?: string; } export class MockGitLabServer { private app: express.Application; private server: Server | null = null; private config: MockGitLabConfig; private requestCount = 0; constructor(config: MockGitLabConfig) { this.config = config; this.app = express(); this.setupMiddleware(); this.setupRoutes(); } /** * Setup middleware including auth validation */ private setupMiddleware() { this.app.use(express.json()); // Request counter for rate limiting tests this.app.use((req: Request, res: Response, next: NextFunction) => { this.requestCount++; next(); }); // Artificial delay middleware (for timeout testing) if (this.config.responseDelay) { this.app.use((req: Request, res: Response, next: NextFunction) => { setTimeout(next, this.config.responseDelay); }); } // Rate limiting middleware if (this.config.rateLimitAfter) { this.app.use((req: Request, res: Response, next: NextFunction) => { if (this.requestCount > this.config.rateLimitAfter!) { res.status(429).json({ message: 'Rate limit exceeded', retry_after: 60 }); return; } next(); }); } // Authentication middleware - applies to all /api/v4/* routes this.app.use('/api/v4', (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const authHeader = req.headers['authorization'] as string | undefined; const privateToken = req.headers['private-token'] as string | undefined; let token: string | null = null; if (authHeader) { // Extract token from "Bearer <token>" const match = authHeader.match(/^Bearer\s+(.+)$/i); token = match ? match[1].trim() : null; } else if (privateToken) { token = privateToken.trim(); } if (!token) { res.status(401).json({ message: 'Unauthorized', error: 'Missing authentication token' }); return; } if (!this.config.validTokens.includes(token)) { res.status(401).json({ message: 'Unauthorized', error: 'Invalid authentication token' }); return; } // Store validated token in request req.gitlabToken = token; next(); }); } private setupRoutes() { // GET /api/v4/user - Get current user this.app.get('/api/v4/user', (req: AuthenticatedRequest, res: Response) => { const token = req.gitlabToken || 'unknown'; res.json({ id: 1, username: `user_${token.substring(0, 8)}`, name: 'Test User', email: 'test@example.com', state: 'active' }); }); // GET /api/v4/projects/:projectId - Get project this.app.get('/api/v4/projects/:projectId', (req: AuthenticatedRequest, res: Response) => { const projectId = req.params.projectId; res.json({ id: parseInt(projectId) || 123, name: 'Test Project', path: 'test-project', path_with_namespace: 'test-group/test-project', description: 'A mock test project', visibility: 'private', created_at: '2024-01-01T00:00:00Z', web_url: `https://gitlab.mock/project/${projectId}`, namespace: { id: 1, name: 'Test Group', path: 'test-group', kind: 'group', full_path: 'test-group' } }); }); // GET /api/v4/projects/:projectId/merge_requests - List merge requests this.app.get('/api/v4/projects/:projectId/merge_requests', (req: AuthenticatedRequest, res: Response) => { res.json([ { id: 1, iid: 1, title: 'Test MR 1', state: 'opened', created_at: '2024-01-01T00:00:00Z', author: { id: 1, username: 'test-user', name: 'Test User' } }, { id: 2, iid: 2, title: 'Test MR 2', state: 'merged', created_at: '2024-01-02T00:00:00Z', author: { id: 1, username: 'test-user', name: 'Test User' } } ]); }); // GET /api/v4/projects/:projectId/merge_requests/:mr_iid - Get single MR this.app.get('/api/v4/projects/:projectId/merge_requests/:mr_iid', (req: AuthenticatedRequest, res: Response) => { const mrIid = parseInt(req.params.mr_iid); res.json({ id: mrIid, iid: mrIid, title: `Test MR ${mrIid}`, state: 'opened', created_at: '2024-01-01T00:00:00Z', author: { id: 1, username: 'test-user', name: 'Test User' }, source_branch: 'feature-branch', target_branch: 'main', merge_status: 'can_be_merged' }); }); // GET /api/v4/projects/:projectId/issues - List issues this.app.get('/api/v4/projects/:projectId/issues', (req: AuthenticatedRequest, res: Response) => { res.json([ { id: 1, iid: 1, title: 'Test Issue 1', state: 'opened', created_at: '2024-01-01T00:00:00Z', author: { id: 1, username: 'test-user', name: 'Test User' } } ]); }); // GET /api/v4/projects - List projects this.app.get('/api/v4/projects', (req: AuthenticatedRequest, res: Response) => { res.json([ { id: 123, name: 'Test Project', path: 'test-project', path_with_namespace: 'test-group/test-project', description: 'A mock test project', visibility: 'private', namespace: { id: 1, name: 'Test Group', path: 'test-group', kind: 'group', full_path: 'test-group' } } ]); }); // Health check endpoint this.app.get('/health', (req: Request, res: Response) => { res.json({ status: 'ok', message: 'Mock GitLab API is running' }); }); // Catch-all for unimplemented endpoints this.app.use((req: Request, res: Response) => { console.log(`Mock GitLab: Unimplemented endpoint: ${req.method} ${req.path}`); res.status(404).json({ message: '404 Not Found', error: 'Endpoint not implemented in mock server' }); }); } async start(): Promise<void> { return new Promise((resolve) => { this.server = this.app.listen(this.config.port, '127.0.0.1', () => { console.log(`Mock GitLab API listening on http://127.0.0.1:${this.config.port}`); resolve(); }); }); } async stop(): Promise<void> { return new Promise((resolve, reject) => { if (this.server) { this.server.close((err) => { if (err) reject(err); else { console.log('Mock GitLab API stopped'); resolve(); } }); } else { resolve(); } }); } getUrl(): string { return `http://127.0.0.1:${this.config.port}`; } } /** * Helper to find available port for mock server */ export async function findMockServerPort( basePort: number = 9000, maxAttempts: number = 10 ): Promise<number> { const net = await import('net'); const tryPort = async (port: number, attemptsLeft: number): Promise<number> => { if (attemptsLeft === 0) { throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${basePort}`); } return new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); server.on('error', async () => { try { const nextPort = await tryPort(port + 1, attemptsLeft - 1); resolve(nextPort); } catch (err) { reject(err); } }); server.listen(port, '127.0.0.1', () => { const addr = server.address(); const actualPort = typeof addr === 'object' && addr ? addr.port : port; server.close(() => { resolve(actualPort); }); }); }); }; return tryPort(basePort, maxAttempts); } /** * Reset request counter (useful for rate limit testing) */ export function resetMockServerState(server: MockGitLabServer) { (server as any).requestCount = 0; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zereight/gitlab-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server