UNPKG

apiveritas

Version:

Lightweight CLI tool for consumer-driven API contract testing via JSON schema and payload comparisons.

169 lines (146 loc) 5.89 kB
/** * @file MockServer.ts * @author Mario Galea * @description * The MockServer class provides an Express-based HTTP server that serves static * JSON mock responses for API contract testing. It maps incoming HTTP requests * to JSON files in a configured directory based on the request method and path, * then returns the file contents as the HTTP response. The server supports * basic authentication and can be started and stopped programmatically, making * it suitable for development and CI environments where a mock backend is needed. */ import express, { RequestHandler } from 'express'; import basicAuth from 'express-basic-auth'; import fs from 'fs'; import path from 'path'; import bodyParser from 'body-parser'; import http from 'http'; import { Logger } from '../utils/Logger'; /** * MockServer serves static mock JSON responses based on HTTP method and URL path. * It is typically used in CI or development environments for API contract testing. */ export class MockServer { private app = express(); private readonly port = 3000; private readonly mockDir = path.resolve('apiveritas/tests/mock/mock-responses'); private logger = new Logger({ level: 'info' }); private server?: http.Server; /** * Constructs a new MockServer instance. * Sets up middleware for JSON parsing and basic authentication, * checks the mock responses directory existence, * and configures routing for serving mock responses. */ constructor() { this.logger.info('Initializing MockServer...'); this.logger.info(`Expected mock response directory: ${this.mockDir}`); const mockDirExists = fs.existsSync(this.mockDir); if (!mockDirExists) { this.logger.warn(`Mock directory does not exist: ${this.mockDir}`); } else { this.logger.info('Mock directory found.'); } this.app.use(bodyParser.json()); this.app.use(basicAuth({ users: { 'admin': 'secret' }, unauthorizedResponse: () => 'Unauthorized', })); this.logger.info('Basic authentication configured for user: "admin"'); this.setupRoutes(); this.logger.info('Finished setting up routes and middleware.'); } /** * Sets up a universal route handler for all incoming HTTP requests. * The handler attempts to locate a corresponding mock JSON response file * based on HTTP method and path, then returns its contents as the response. * If no matching file is found or the file contains invalid JSON, * appropriate HTTP error codes and messages are returned. * * @private * @returns {void} */ private setupRoutes(): void { const handler: RequestHandler = (req, res) => { const { method, path: reqPath } = req; this.logger.info(`Incoming request: ${method} ${reqPath}`); const match = this.findMatchingTest(method, reqPath); if (!match) { this.logger.warn(`No mock match found for: ${method} ${reqPath}`); res.status(404).send(`No mock response found for ${method} ${reqPath}`); return; } this.logger.info(`Matched to file: ${match}.json`); const mockFile = path.join(this.mockDir, `${match}.json`); if (!fs.existsSync(mockFile)) { this.logger.error(`Mock file not found: ${mockFile}`); res.status(500).send(`Mock file not found: ${match}.json`); return; } const content = fs.readFileSync(mockFile, 'utf-8'); try { const json = JSON.parse(content); res.json(json); } catch (e) { this.logger.error(`Invalid JSON in mock file: ${match}.json`); res.status(500).send('Invalid mock JSON file'); } }; this.app.all(/.*/, handler); } /** * Matches an incoming HTTP request method and path to a mock JSON file name. * Converts the request path to a safe filename by stripping leading/trailing slashes * and replacing internal slashes with underscores. * * Example: GET /api/user -> GET_api_user.json * * @private * @param {string} method - The HTTP method (e.g., GET, POST). * @param {string} reqPath - The request URL path. * @returns {string | null} The matching filename without extension, or null if no file exists. */ private findMatchingTest(method: string, reqPath: string): string | null { const cleanPath = reqPath.replace(/^\/|\/$/g, '').replace(/\//g, '_'); const fileName = `${method.toUpperCase()}_${cleanPath}`; const mockFilePath = path.join(this.mockDir, `${fileName}.json`); return fs.existsSync(mockFilePath) ? fileName : null; } /** * Starts the mock server and begins listening on the configured port. * * @public * @returns {Promise<void>} A promise that resolves once the server is successfully started. */ public start(): Promise<void> { return new Promise((resolve) => { this.server = this.app.listen(this.port, () => { this.logger.info(`Mock server is running at http://localhost:${this.port}`); resolve(); }); }); } /** * Stops the mock server if it is currently running. * * @public * @returns {Promise<void>} A promise that resolves once the server is stopped. * If the server is not running, the promise resolves immediately. * If an error occurs while closing the server, the promise rejects with the error. */ public stop(): Promise<void> { return new Promise((resolve, reject) => { if (!this.server) { return resolve(); } this.server.close((err) => { if (err) { this.logger.error('Error while stopping mock server.'); return reject(err); } this.logger.info('Mock server stopped.'); resolve(); }); }); } }