apiveritas
Version:
Lightweight CLI tool for consumer-driven API contract testing via JSON schema and payload comparisons.
154 lines (153 loc) • 6.55 kB
JavaScript
;
/**
* @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;