UNPKG

@paultaku/node-mock-server

Version:

A TypeScript-based mock server with automatic Swagger-based mock file generation

570 lines (547 loc) 22.4 kB
/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ 252: /***/ ((module) => { module.exports = require("express"); /***/ }), /***/ 422: /***/ (function(module, exports, __webpack_require__) { /* module decorator */ module = __webpack_require__.nmd(module); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.startMockServer = startMockServer; const express_1 = __importDefault(__webpack_require__(252)); const path_1 = __importDefault(__webpack_require__(928)); const fs_extra_1 = __importDefault(__webpack_require__(652)); const zod_1 = __webpack_require__(569); const status_manager_1 = __webpack_require__(537); const DEFAULT_MOCK_ROOT = path_1.default.resolve(__dirname, "../mock"); const DEFAULT_MOCK_FILE = "successful-operation-200.json"; // Store the mock response state of each endpoint let mockStates = new Map(); // Only allow letters, numbers, -, _, {}, not : * ? ( ) [ ] etc. function isValidMockPart(part) { return /^[a-zA-Z0-9_\-{}]+$/.test(part); } // Get all mock endpoint templates (e.g. user/{username}/GET) async function getAllMockTemplates(mockRoot) { async function walk(dir, parts = []) { const entries = await fs_extra_1.default.readdir(dir); let results = []; for (const entry of entries) { if (!isValidMockPart(entry)) continue; // Skip invalid names const fullPath = path_1.default.join(dir, entry); const stat = await fs_extra_1.default.stat(fullPath); if (stat.isDirectory()) { // Check if there are json files under this directory (i.e. method directory) const files = await fs_extra_1.default.readdir(fullPath); const jsonFiles = files.filter((f) => f.endsWith(".json") && f !== "status.json"); if (jsonFiles.length > 0) { // This is a method directory, push parts+method results.push([...parts, entry]); } // Continue recursion results = results.concat(await walk(fullPath, [...parts, entry])); } } return results; } return walk(mockRoot); } // Path parameter template matching function matchTemplate(requestParts, templates, method) { let bestMatch = null; for (const tpl of templates) { if (tpl.length !== requestParts.length + 1) continue; // +1 for method const lastElement = tpl[tpl.length - 1]; if (!lastElement || lastElement.toUpperCase() !== method) continue; let params = {}; let matched = true; for (let i = 0; i < requestParts.length; i++) { const tplElement = tpl[i]; if (!tplElement) continue; if (tplElement.startsWith("{") && tplElement.endsWith("}")) { params[tplElement.slice(1, -1)] = requestParts[i] || ""; } else if (tplElement !== requestParts[i]) { matched = false; break; } } if (matched) { bestMatch = { template: tpl, params }; break; } } return bestMatch; } // Get all available mock files for an endpoint async function getAvailableMockFiles(endpointDir) { try { const files = await fs_extra_1.default.readdir(endpointDir); return files.filter((file) => file.endsWith(".json") && file !== "status.json"); } catch (error) { return []; } } // Get the mock state key for the current endpoint function getMockStateKey(path, method) { return `${method.toUpperCase()}:${path}`; } // Create Express app function createApp(mockRoot = DEFAULT_MOCK_ROOT) { const app = (0, express_1.default)(); // Middleware app.use(express_1.default.json()); app.use(express_1.default.static(path_1.default.join(__dirname, "./public"))); // Load all status.json files when service starts (async () => { const templates = await getAllMockTemplates(mockRoot); mockStates = await (0, status_manager_1.loadAllStatusJson)(mockRoot, templates); console.log("[status-manager] All status.json files loaded"); })(); // API endpoint: get all available endpoints app.get("/_mock/endpoints", async (req, res) => { try { console.log("Getting all mock templates..."); const templates = await getAllMockTemplates(mockRoot); console.log("Templates found:", templates); const endpoints = []; for (const template of templates) { const method = template[template.length - 1] || ""; const pathParts = template.slice(0, -1); const apiPath = "/" + pathParts.join("/"); const stateKey = getMockStateKey(apiPath, method); const currentMock = mockStates.get(stateKey) || DEFAULT_MOCK_FILE; // Read status.json to get delayMillisecond const statusPath = (0, status_manager_1.getStatusJsonPath)(mockRoot, apiPath, method); let delayMillisecond = undefined; try { const status = await (0, status_manager_1.readStatusJson)(statusPath); if (status && typeof status.delayMillisecond === "number") { delayMillisecond = status.delayMillisecond; } } catch { } endpoints.push({ path: apiPath, method: method, currentMock: currentMock, availableMocks: [], // Explicit type delayMillisecond, }); } // Get available mock files for each endpoint for (const endpoint of endpoints) { const endpointDir = path_1.default.join(mockRoot, ...endpoint.path.replace(/^\//, "").split("/"), endpoint.method); endpoint.availableMocks = await getAvailableMockFiles(endpointDir); } res.json(endpoints); } catch (error) { res .status(500) .json({ error: "Failed to get endpoints", detail: String(error) }); } }); // API endpoint: set the mock response for an endpoint const SetMockRequestSchema = zod_1.z.object({ path: zod_1.z.string(), method: zod_1.z.string(), mockFile: zod_1.z.string(), }); // API endpoint: update mock status via /_mock URL segment app.post("/_mock/update", async (req, res) => { try { const { path: apiPath, method, mockFile, delayMillisecond } = req.body; if (!apiPath || !method) { return res .status(400) .json({ error: "Missing required parameters: path and method" }); } const statusPath = (0, status_manager_1.getStatusJsonPath)(mockRoot, apiPath, method); let status = await (0, status_manager_1.readStatusJson)(statusPath); if (!status) status = { selected: DEFAULT_MOCK_FILE }; // Update mock file if provided if (mockFile) { // Validate if the mock file exists const endpointDir = path_1.default.join(mockRoot, ...apiPath.replace(/^\//, "").split("/"), method.toUpperCase()); const mockFilePath = path_1.default.join(endpointDir, mockFile); if (!(await fs_extra_1.default.pathExists(mockFilePath))) { return res .status(400) .json({ error: "Mock file not found", file: mockFilePath }); } status.selected = mockFile; const stateKey = getMockStateKey(apiPath, method); mockStates.set(stateKey, mockFile); } // Update delay if provided if (typeof delayMillisecond === "number") { if (delayMillisecond < 0 || delayMillisecond > 60000) { return res .status(400) .json({ error: "Delay must be between 0 and 60000 milliseconds" }); } status.delayMillisecond = delayMillisecond; } // Write updated status await fs_extra_1.default.writeJson(statusPath, status, { spaces: 2 }); console.log(`[status-manager] Updated ${statusPath} via /_mock/update`); return res.json({ success: true, message: "Mock status updated successfully", status: { selected: status.selected, delayMillisecond: status.delayMillisecond, }, }); } catch (error) { return res .status(500) .json({ error: "Failed to update mock status", detail: String(error) }); } }); // API endpoint: get mock status via /_mock URL segment app.get("/_mock/status", async (req, res) => { try { const { method, path: apiPath } = req.query; if (!method || !apiPath || typeof method !== "string" || typeof apiPath !== "string") { return res .status(400) .json({ error: "Missing method or path parameter" }); } const statusPath = (0, status_manager_1.getStatusJsonPath)(mockRoot, apiPath, method); const status = await (0, status_manager_1.readStatusJson)(statusPath); const stateKey = getMockStateKey(apiPath, method); const currentMock = mockStates.get(stateKey) || DEFAULT_MOCK_FILE; return res.json({ path: apiPath, method: method, currentMock: status?.selected || currentMock, delayMillisecond: status?.delayMillisecond || 0, }); } catch (error) { return res .status(500) .json({ error: "Failed to get mock status", detail: String(error) }); } }); // New: API for setting delay // 1. /api/set-delay validation and writing const SetDelayRequestSchema = zod_1.z.object({ path: zod_1.z.string(), method: zod_1.z.string(), delayMillisecond: zod_1.z.number().min(0).max(60000), }); app.post("/_mock/set-delay", async (req, res) => { try { const { path: apiPath, method, delayMillisecond, } = SetDelayRequestSchema.parse(req.body); const statusPath = (0, status_manager_1.getStatusJsonPath)(mockRoot, apiPath, method); let status = await (0, status_manager_1.readStatusJson)(statusPath); if (!status) status = { selected: DEFAULT_MOCK_FILE }; await fs_extra_1.default.writeJson(statusPath, { ...status, delayMillisecond }, { spaces: 2 }); console.log(`[status-manager] Set delay ${delayMillisecond}ms for ${statusPath}`); return res.json({ success: true, message: `Delay set to ${delayMillisecond}ms`, }); } catch (error) { if (error instanceof zod_1.z.ZodError) { return res .status(400) .json({ error: "Invalid request data", details: error.errors }); } else { return res .status(500) .json({ error: "Failed to set delay", detail: String(error) }); } } }); // The main mock server logic app.use(async (req, res, next) => { try { const reqPath = req.path.replace(/^\//, ""); const method = req.method.toUpperCase(); const requestParts = reqPath ? reqPath.split("/") : []; // Skip API endpoints if (reqPath.startsWith("api/")) { return next(); } const templates = await getAllMockTemplates(mockRoot); const match = matchTemplate(requestParts, templates, method); let endpointDir; let apiPath; if (match) { endpointDir = path_1.default.join(mockRoot, ...match.template); apiPath = "/" + match.template.slice(0, -1).join("/"); } else { // fallback: exact path endpointDir = path_1.default.join(mockRoot, ...requestParts, method); apiPath = "/" + requestParts.join("/"); } // Select mock response file const stateKey = getMockStateKey(apiPath, method); let mockFile = mockStates.get(stateKey); if (!mockFile) { // fallback: read status.json or default const statusPath = (0, status_manager_1.getStatusJsonPath)(mockRoot, apiPath, method); const status = await (0, status_manager_1.readStatusJson)(statusPath); if (status && status.selected) { mockFile = status.selected; mockStates.set(stateKey, mockFile); console.log(`[status-manager] fallback read ${statusPath} -> ${mockFile}`); } else { mockFile = DEFAULT_MOCK_FILE; mockStates.set(stateKey, mockFile); console.log(`[status-manager] fallback default ${stateKey} -> ${mockFile}`); } } const filePath = path_1.default.join(endpointDir, mockFile); if (!(await fs_extra_1.default.pathExists(filePath))) { return res.status(404).json({ error: "Mock file not found", file: filePath, availableFiles: await getAvailableMockFiles(endpointDir), }); } const mock = await fs_extra_1.default.readJson(filePath); // Set headers if (Array.isArray(mock.header)) { for (const h of mock.header) { if (h && h.key && h.value) { res.setHeader(h.key, h.value); } } } // Set status code const statusMatch = mockFile.match(/-(\d+)\.json$/); if (statusMatch && statusMatch[1]) { res.status(parseInt(statusMatch[1])); } // Read status.json to get delayMillisecond const statusPath = (0, status_manager_1.getStatusJsonPath)(mockRoot, apiPath, method); const status = await (0, status_manager_1.readStatusJson)(statusPath); let delayMillisecond = 0; if (status) { if (typeof status.delayMillisecond === "number" && status.delayMillisecond > 0) { delayMillisecond = status.delayMillisecond; } else if (typeof status.delayMillisecond === "number" && status.delayMillisecond > 0) { // Compatible with old field delayMillisecond = status.delayMillisecond * 1000; } if (delayMillisecond > 0) { console.log(`[status-manager] ${apiPath} ${method} delay ${delayMillisecond}ms`); } } // Delay response if (delayMillisecond > 0) { await new Promise((resolve) => setTimeout(resolve, delayMillisecond)); } return res.json(mock.body); } catch (error) { return res .status(500) .json({ error: "Mock server error", detail: String(error) }); } }); // 404 handling app.use((req, res) => { res.status(404).json({ error: "API endpoint not found" }); }); return app; } // Function to start the server function startMockServer(port = 3001, mockRoot) { const resolvedMockRoot = mockRoot ? path_1.default.resolve(mockRoot) : DEFAULT_MOCK_ROOT; return new Promise((resolve, reject) => { try { const app = createApp(resolvedMockRoot); const server = app.listen(port, () => { console.log(`Mock server running at http://localhost:${port}`); console.log(`API endpoints available at http://localhost:${port}/_mock/endpoints`); console.log(`Mock root directory: ${resolvedMockRoot}`); resolve(); }); server.on("error", (error) => { reject(error); }); } catch (error) { reject(error); } }); } // If this file is run directly, start the server if (__webpack_require__.c[__webpack_require__.s] === module) { const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3001; const MOCK_ROOT = process.env.MOCK_ROOT; startMockServer(PORT, MOCK_ROOT).catch((error) => { console.error("Failed to start mock server:", error); process.exit(1); }); } /***/ }), /***/ 537: /***/ (function(__unused_webpack_module, exports, __webpack_require__) { var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.getStatusJsonPath = getStatusJsonPath; exports.readStatusJson = readStatusJson; exports.writeStatusJson = writeStatusJson; exports.loadAllStatusJson = loadAllStatusJson; const fs_extra_1 = __importDefault(__webpack_require__(652)); const path_1 = __importDefault(__webpack_require__(928)); const DEFAULT_MOCK_FILE = "successful-operation-200.json"; /** * 获取 status.json 路径 * @param mockRoot mock 根目录 * @param endpointPath 形如 /pet/{petId} * @param method HTTP 方法(GET/POST/...) */ function getStatusJsonPath(mockRoot, endpointPath, method) { return path_1.default.join(mockRoot, ...endpointPath.replace(/^\//, "").split("/"), method.toUpperCase(), "status.json"); } /** * 读取 status.json * @param statusPath status.json 文件路径 * @returns StatusJson 对象或 null */ async function readStatusJson(statusPath) { try { const data = await fs_extra_1.default.readJson(statusPath); if (typeof data.selected === "string" && data.selected.endsWith(".json")) { return data; } return null; } catch { return null; } } /** * 写入 status.json(原子写入) * @param statusPath status.json 文件路径 * @param selected 选中的 mock 文件名 */ async function writeStatusJson(statusPath, selected) { await fs_extra_1.default.writeJson(statusPath, { selected }, { spaces: 2 }); } /** * 初始化所有 endpoint 的 mock 选择状态 * @param mockRoot mock 根目录 * @param templates 所有 endpoint 模板数组 * @returns Map<stateKey, selectedMockFile> */ async function loadAllStatusJson(mockRoot, templates) { const stateMap = new Map(); for (const template of templates) { const method = template[template.length - 1] || ""; const endpointPath = "/" + template.slice(0, -1).join("/"); if (!method) continue; // 跳过无效 method const statusPath = getStatusJsonPath(mockRoot, endpointPath, method); const status = await readStatusJson(statusPath); if (status && status.selected) { stateMap.set(`${method.toUpperCase()}:${endpointPath}`, status.selected); } else { // fallback stateMap.set(`${method.toUpperCase()}:${endpointPath}`, DEFAULT_MOCK_FILE); } } return stateMap; } /***/ }), /***/ 569: /***/ ((module) => { module.exports = require("zod"); /***/ }), /***/ 652: /***/ ((module) => { module.exports = require("fs-extra"); /***/ }), /***/ 928: /***/ ((module) => { module.exports = require("path"); /***/ }) /******/ }); /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ var cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { /******/ id: moduleId, /******/ loaded: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ // expose the module cache /******/ __webpack_require__.c = __webpack_module_cache__; /******/ /************************************************************************/ /******/ /* webpack/runtime/node module decorator */ /******/ (() => { /******/ __webpack_require__.nmd = (module) => { /******/ module.paths = []; /******/ if (!module.children) module.children = []; /******/ return module; /******/ }; /******/ })(); /******/ /************************************************************************/ /******/ /******/ // module cache are used so entry inlining is disabled /******/ // startup /******/ // Load entry module and return exports /******/ var __webpack_exports__ = __webpack_require__(__webpack_require__.s = 422); /******/ module.exports = __webpack_exports__; /******/ /******/ })() ; //# sourceMappingURL=server.js.map