UNPKG

apiveritas

Version:

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

154 lines (153 loc) 6.55 kB
"use strict"; /** * @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. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MockServer = void 0; const express_1 = __importDefault(require("express")); const express_basic_auth_1 = __importDefault(require("express-basic-auth")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const body_parser_1 = __importDefault(require("body-parser")); const Logger_1 = require("../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. */ class MockServer { /** * 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.app = (0, express_1.default)(); this.port = 3000; this.mockDir = path_1.default.resolve('apiveritas/tests/mock/mock-responses'); this.logger = new Logger_1.Logger({ level: 'info' }); this.logger.info('Initializing MockServer...'); this.logger.info(`Expected mock response directory: ${this.mockDir}`); const mockDirExists = fs_1.default.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(body_parser_1.default.json()); this.app.use((0, express_basic_auth_1.default)({ 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} */ setupRoutes() { const handler = (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_1.default.join(this.mockDir, `${match}.json`); if (!fs_1.default.existsSync(mockFile)) { this.logger.error(`Mock file not found: ${mockFile}`); res.status(500).send(`Mock file not found: ${match}.json`); return; } const content = fs_1.default.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. */ findMatchingTest(method, reqPath) { const cleanPath = reqPath.replace(/^\/|\/$/g, '').replace(/\//g, '_'); const fileName = `${method.toUpperCase()}_${cleanPath}`; const mockFilePath = path_1.default.join(this.mockDir, `${fileName}.json`); return fs_1.default.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. */ start() { 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. */ stop() { 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(); }); }); } } exports.MockServer = MockServer;