UNPKG

@paultaku/node-mock-server

Version:

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

1,321 lines (1,290 loc) 102 kB
/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ 35: /***/ (function(__unused_webpack_module, exports, __webpack_require__) { /** * File Reader Utility * * Wrapper around fs-extra for file read operations. * Provides a consistent interface for file reading across all domains. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.readFile = readFile; exports.readJson = readJson; exports.isDirectory = isDirectory; exports.isFile = isFile; exports.readDirectory = readDirectory; const fs = __importStar(__webpack_require__(652)); /** * Read file content as a string */ async function readFile(filePath, options) { return await fs.readFile(filePath, options?.encoding || 'utf8'); } /** * Read and parse JSON file */ async function readJson(filePath) { return await fs.readJson(filePath); } /** * Check if a path is a directory */ async function isDirectory(dirPath) { try { const stats = await fs.stat(dirPath); return stats.isDirectory(); } catch { return false; } } /** * Check if a path is a file */ async function isFile(filePath) { try { const stats = await fs.stat(filePath); return stats.isFile(); } catch { return false; } } /** * List files in a directory */ async function readDirectory(dirPath) { return await fs.readdir(dirPath); } /***/ }), /***/ 108: /***/ ((__unused_webpack_module, exports) => { /** * Scenario Management Type Definitions * * This file contains TypeScript types, interfaces, enums, and error classes * for the scenario management feature. * * @see specs/004-scenario-management/data-model.md */ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ScenarioNotFoundError = exports.EmptyScenarioError = exports.DuplicateEndpointError = exports.DuplicateScenarioError = exports.ScenarioValidationError = exports.ScenarioState = exports.HttpMethod = void 0; // ============================================================================ // Enums // ============================================================================ /** * HTTP methods supported by endpoint configurations */ var HttpMethod; (function (HttpMethod) { HttpMethod["GET"] = "GET"; HttpMethod["POST"] = "POST"; HttpMethod["PUT"] = "PUT"; HttpMethod["DELETE"] = "DELETE"; HttpMethod["PATCH"] = "PATCH"; })(HttpMethod || (exports.HttpMethod = HttpMethod = {})); /** * Possible states of a scenario in its lifecycle */ var ScenarioState; (function (ScenarioState) { /** Scenario is being created but not yet saved */ ScenarioState["DRAFT"] = "DRAFT"; /** Scenario is saved to file but not active */ ScenarioState["SAVED"] = "SAVED"; /** Scenario is currently active (most recently saved) */ ScenarioState["ACTIVE"] = "ACTIVE"; /** Scenario was active but another scenario is now active */ ScenarioState["INACTIVE"] = "INACTIVE"; })(ScenarioState || (exports.ScenarioState = ScenarioState = {})); // ============================================================================ // Domain Errors // ============================================================================ /** * Error thrown when scenario validation fails */ class ScenarioValidationError extends Error { constructor(field, constraint) { super(`Validation failed for ${field}: ${constraint}`); this.name = 'ScenarioValidationError'; this.field = field; this.constraint = constraint; } } exports.ScenarioValidationError = ScenarioValidationError; /** * Error thrown when attempting to create a scenario with a name that already exists */ class DuplicateScenarioError extends Error { constructor(scenarioName) { super(`Scenario with name "${scenarioName}" already exists`); this.name = 'DuplicateScenarioError'; this.scenarioName = scenarioName; } } exports.DuplicateScenarioError = DuplicateScenarioError; /** * Error thrown when attempting to add duplicate endpoint configurations * (same path + method) to a scenario */ class DuplicateEndpointError extends Error { constructor(path, method) { super(`Endpoint ${method} ${path} is already configured in this scenario`); this.name = 'DuplicateEndpointError'; this.path = path; this.method = method; } } exports.DuplicateEndpointError = DuplicateEndpointError; /** * Error thrown when attempting to save a scenario with no endpoint configurations */ class EmptyScenarioError extends Error { constructor() { super('Scenario must contain at least one endpoint configuration'); this.name = 'EmptyScenarioError'; } } exports.EmptyScenarioError = EmptyScenarioError; /** * Error thrown when attempting to access a scenario that doesn't exist */ class ScenarioNotFoundError extends Error { constructor(scenarioName) { super(`Scenario "${scenarioName}" not found`); this.name = 'ScenarioNotFoundError'; this.scenarioName = scenarioName; } } exports.ScenarioNotFoundError = ScenarioNotFoundError; /***/ }), /***/ 154: /***/ (function(__unused_webpack_module, exports, __webpack_require__) { /** * ScenarioRepository Implementation * * File-based persistence layer for scenarios using fs-extra. * Implements the IScenarioRepository interface for CRUD operations. * * @see specs/004-scenario-management/data-model.md * @see tests/unit/scenario-repository.test.ts */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ScenarioRepository = void 0; const fs = __importStar(__webpack_require__(652)); const path = __importStar(__webpack_require__(928)); const scenario_types_1 = __webpack_require__(108); /** * File-based repository for scenario persistence * * Stores scenarios as individual JSON files in the configured directory. * Each scenario is saved as {scenarioName}.json with 2-space indentation. * * Directory structure: * mock/scenario/ * ├── scenario-1.json * ├── scenario-2.json * └── _active.json (managed by ActiveScenarioTracker) */ class ScenarioRepository { /** * @param scenarioDir Absolute path to scenario storage directory (default: mock/scenario) */ constructor(scenarioDir = path.join(process.cwd(), 'mock', 'scenario')) { this.scenarioDir = scenarioDir; } /** * Save a new scenario to the file system * * @throws DuplicateScenarioError if scenario with same name already exists */ async save(scenario) { // Ensure directory exists await fs.ensureDir(this.scenarioDir); const filePath = this.getFilePath(scenario.name); // Check for duplicate if (await fs.pathExists(filePath)) { throw new scenario_types_1.DuplicateScenarioError(scenario.name); } // Write scenario to file with formatted JSON await fs.writeJson(filePath, scenario, { spaces: 2 }); } /** * Find a scenario by name * * @returns Scenario if found, null otherwise */ async findByName(name) { const filePath = this.getFilePath(name); if (!(await fs.pathExists(filePath))) { return null; } try { const scenario = await fs.readJson(filePath); return scenario; } catch (error) { // Re-throw JSON parse errors throw error; } } /** * Find all scenarios in the directory * * Ignores files that: * - Don't have .json extension * - Start with underscore (e.g., _active.json) * - Are not valid scenario files * * @returns Array of all scenarios (empty if directory doesn't exist) */ async findAll() { // Return empty array if directory doesn't exist if (!(await fs.pathExists(this.scenarioDir))) { return []; } try { const files = await fs.readdir(this.scenarioDir); // Filter to only scenario JSON files (exclude _active.json, README.md, etc.) const scenarioFiles = files.filter((file) => file.endsWith('.json') && !file.startsWith('_')); // Read all scenario files in parallel, skipping corrupted ones const scenarioPromises = scenarioFiles.map(async (file) => { const filePath = path.join(this.scenarioDir, file); try { return (await fs.readJson(filePath)); } catch (error) { // Skip corrupted/invalid JSON files console.warn(`[scenario-repository] Skipping corrupted file: ${file}`, error); return null; } }); const results = await Promise.all(scenarioPromises); // Filter out null values (corrupted files) return results.filter((scenario) => scenario !== null); } catch (error) { // Return empty array on read errors return []; } } /** * Check if a scenario exists by name * * @returns true if scenario file exists, false otherwise */ async exists(name) { const filePath = this.getFilePath(name); return await fs.pathExists(filePath); } /** * Update an existing scenario * * @throws ScenarioNotFoundError if scenario doesn't exist */ async update(scenario) { const filePath = this.getFilePath(scenario.name); // Verify scenario exists if (!(await fs.pathExists(filePath))) { throw new scenario_types_1.ScenarioNotFoundError(scenario.name); } // Overwrite existing scenario file await fs.writeJson(filePath, scenario, { spaces: 2 }); } /** * Delete a scenario by name * * @throws ScenarioNotFoundError if scenario doesn't exist */ async delete(name) { const filePath = this.getFilePath(name); // Verify scenario exists if (!(await fs.pathExists(filePath))) { throw new scenario_types_1.ScenarioNotFoundError(name); } // Delete the file await fs.remove(filePath); } /** * Get the file path for a scenario name * @private */ getFilePath(name) { return path.join(this.scenarioDir, `${name}.json`); } } exports.ScenarioRepository = ScenarioRepository; /***/ }), /***/ 223: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { /** * Server Runtime Domain - Public Interface * * This domain handles serving mock responses at runtime. * Manages Express server, route matching, and response rendering. * * Bounded Context: Server Runtime * Responsibility: Serve mock responses for HTTP requests */ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createMultiServerManager = exports.createMockServer = exports.MultiServerManager = exports.MockServerManager = exports.startMockServer = void 0; // Main server functions var mock_server_1 = __webpack_require__(249); Object.defineProperty(exports, "startMockServer", ({ enumerable: true, get: function () { return mock_server_1.startMockServer; } })); // Server management var server_manager_1 = __webpack_require__(558); Object.defineProperty(exports, "MockServerManager", ({ enumerable: true, get: function () { return server_manager_1.MockServerManager; } })); Object.defineProperty(exports, "MultiServerManager", ({ enumerable: true, get: function () { return server_manager_1.MultiServerManager; } })); Object.defineProperty(exports, "createMockServer", ({ enumerable: true, get: function () { return server_manager_1.createMockServer; } })); Object.defineProperty(exports, "createMultiServerManager", ({ enumerable: true, get: function () { return server_manager_1.createMultiServerManager; } })); // DO NOT import internal utilities (route-matcher, status-tracker, response-renderer) // These are domain-internal implementation details /***/ }), /***/ 249: /***/ (function(__unused_webpack_module, exports, __webpack_require__) { /** * Mock Server * * Main Express server implementation for serving mock responses. * Aggregate root for the Server Runtime domain. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createApp = createApp; exports.startMockServer = startMockServer; const express_1 = __importDefault(__webpack_require__(252)); const path_1 = __importDefault(__webpack_require__(928)); const zod_1 = __webpack_require__(569); const route_matcher_1 = __webpack_require__(929); const status_tracker_1 = __webpack_require__(724); const response_renderer_1 = __webpack_require__(918); const file_system_1 = __webpack_require__(453); const validation_schemas_1 = __webpack_require__(276); const endpoint_file_generator_1 = __webpack_require__(733); const scenario_manager_1 = __webpack_require__(839); const scenario_repository_1 = __webpack_require__(154); const active_scenario_tracker_1 = __webpack_require__(593); const scenario_applicator_1 = __webpack_require__(651); const scenario_types_1 = __webpack_require__(108); const validation_schemas_2 = __webpack_require__(276); const DEFAULT_MOCK_ROOT = path_1.default.resolve(__dirname, "../../../mock"); const DEFAULT_MOCK_FILE = "successful-operation-200.json"; /** * Create Express application with mock server middleware * @param mockRoot - Root directory for mock files * @returns Express application */ 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"))); // API endpoint: get all available endpoints app.get("/_mock/endpoints", async (req, res) => { try { console.log("Getting all mock templates..."); const templates = await (0, route_matcher_1.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("/"); // Read status.json to get currentMock and delayMillisecond const statusPath = (0, status_tracker_1.getStatusJsonPath)(mockRoot, apiPath, method); let currentMock = DEFAULT_MOCK_FILE; let delayMillisecond = undefined; try { const status = await (0, status_tracker_1.readStatusJson)(statusPath); if (status) { if (status.selected) { currentMock = status.selected; } if (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 (0, route_matcher_1.getAvailableMockFiles)(endpointDir); } res.json(endpoints); } catch (error) { res .status(500) .json({ error: "Failed to get endpoints", detail: String(error) }); } }); // API endpoint: create new mock endpoint app.post("/_mock/endpoints", async (req, res) => { try { // Validate request with Zod const validationResult = validation_schemas_1.CreateEndpointRequestSchema.safeParse(req.body); if (!validationResult.success) { return res .status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST) .json((0, validation_schemas_1.formatValidationErrors)(validationResult.error)); } const { path: apiPath, method } = validationResult.data; // Check for duplicates const pathSegments = apiPath.substring(1).split("/"); const endpointDir = path_1.default.join(mockRoot, ...pathSegments, method.toUpperCase()); if (await (0, file_system_1.fileExists)(endpointDir)) { return res.status(validation_schemas_1.HTTP_STATUS.CONFLICT).json({ error: "Endpoint already exists", existingEndpoint: { path: apiPath, method, mockDirectory: endpointDir, }, }); } // Generate files const result = await (0, endpoint_file_generator_1.generateEndpointFiles)({ mockRoot, path: apiPath, method, }); // Return success return res.status(validation_schemas_1.HTTP_STATUS.CREATED).json({ success: true, message: "Endpoint created successfully", endpoint: { path: apiPath, method, filesCreated: result.filesCreated, availableAt: `http://localhost:3000${apiPath.replace(/{[^}]+}/g, "123")}`, mockDirectory: result.mockDirectory, }, }); } catch (error) { return res.status(validation_schemas_1.HTTP_STATUS.INTERNAL_ERROR).json({ error: "Failed to create endpoint", detail: String(error), }); } }); // 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_tracker_1.getStatusJsonPath)(mockRoot, apiPath, method); let status = await (0, status_tracker_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 (0, file_system_1.fileExists)(mockFilePath))) { return res .status(400) .json({ error: "Mock file not found", file: mockFilePath }); } status.selected = 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 (0, file_system_1.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_tracker_1.getStatusJsonPath)(mockRoot, apiPath, method); const status = await (0, status_tracker_1.readStatusJson)(statusPath); return res.json({ path: apiPath, method: method, currentMock: status?.selected || DEFAULT_MOCK_FILE, delayMillisecond: status?.delayMillisecond || 0, }); } catch (error) { return res .status(500) .json({ error: "Failed to get mock status", detail: String(error) }); } }); // API endpoint: set delay for an endpoint 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_tracker_1.getStatusJsonPath)(mockRoot, apiPath, method); let status = await (0, status_tracker_1.readStatusJson)(statusPath); if (!status) status = { selected: DEFAULT_MOCK_FILE }; await (0, file_system_1.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) }); } } }); // ============================================================================ // Scenario Management API Endpoints // Feature: 004-scenario-management // ============================================================================ // Initialize scenario manager const scenarioDir = path_1.default.join(mockRoot, "scenario"); const scenarioRepository = new scenario_repository_1.ScenarioRepository(scenarioDir); const activeScenarioTracker = new active_scenario_tracker_1.ActiveScenarioTracker(scenarioDir); const scenarioApplicator = new scenario_applicator_1.ScenarioApplicator(mockRoot); const scenarioManager = new scenario_manager_1.ScenarioManager(scenarioRepository, activeScenarioTracker, scenarioApplicator); // API endpoint: create a new scenario app.post("/_mock/scenarios", async (req, res) => { try { // Validate request with Zod const validationResult = validation_schemas_2.CreateScenarioRequestSchema.safeParse(req.body); if (!validationResult.success) { return res .status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST) .json((0, validation_schemas_1.formatScenarioValidationErrors)(validationResult.error)); } const createRequest = validationResult.data; // Create scenario using scenario manager const scenario = await scenarioManager.create(createRequest); return res.status(validation_schemas_1.HTTP_STATUS.CREATED).json({ scenario, message: `Scenario '${scenario.name}' created successfully`, }); } catch (error) { if (error instanceof scenario_types_1.DuplicateScenarioError) { return res.status(validation_schemas_1.HTTP_STATUS.CONFLICT).json({ error: error.message, }); } else if (error instanceof scenario_types_1.EmptyScenarioError) { return res.status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST).json({ error: error.message, }); } else if (error instanceof scenario_types_1.DuplicateEndpointError) { return res.status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST).json({ error: error.message, }); } else if (error instanceof zod_1.z.ZodError) { return res .status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST) .json((0, validation_schemas_1.formatScenarioValidationErrors)(error)); } else { return res.status(validation_schemas_1.HTTP_STATUS.INTERNAL_ERROR).json({ error: "Failed to create scenario", detail: String(error), }); } } }); // API endpoint: list all scenarios with active indicator app.get("/_mock/scenarios", async (req, res) => { try { const result = await scenarioManager.list(); return res.json({ scenarios: result.scenarios, activeScenario: result.activeScenario, }); } catch (error) { return res.status(validation_schemas_1.HTTP_STATUS.INTERNAL_ERROR).json({ error: "Failed to list scenarios", detail: String(error), }); } }); // API endpoint: get active scenario app.get("/_mock/scenarios/active", async (req, res) => { try { const activeScenario = await activeScenarioTracker.getActive(); return res.json({ activeScenario, lastUpdated: new Date().toISOString(), }); } catch (error) { return res.status(validation_schemas_1.HTTP_STATUS.INTERNAL_ERROR).json({ error: "Failed to get active scenario", detail: String(error), }); } }); // API endpoint: apply scenario (update status.json files) app.put("/_mock/scenarios/:name/activate", async (req, res) => { try { const { name } = req.params; if (!name) { return res.status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST).json({ error: "Scenario name is required", }); } // Verify scenario exists const scenarioExists = await scenarioRepository.exists(name); if (!scenarioExists) { return res.status(404).json({ error: `Scenario "${name}" not found`, }); } // Get the scenario and apply it (updates status.json files only) const scenario = await scenarioManager.get(name); const applicationResult = await scenarioApplicator.apply(scenario); // Set the scenario as active await activeScenarioTracker.setActive(name); return res.json({ success: true, message: `Scenario "${name}" applied successfully. Status.json files updated.`, applicationResult, }); } catch (error) { return res.status(validation_schemas_1.HTTP_STATUS.INTERNAL_ERROR).json({ error: "Failed to apply scenario", detail: String(error), }); } }); // API endpoint: get scenario by name app.get("/_mock/scenarios/:name", async (req, res) => { try { const { name } = req.params; if (!name) { return res.status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST).json({ error: "Scenario name is required", }); } const scenario = await scenarioManager.get(name); return res.json({ scenario, }); } catch (error) { if (error instanceof scenario_types_1.ScenarioNotFoundError) { return res.status(404).json({ error: error.message, }); } else { return res.status(validation_schemas_1.HTTP_STATUS.INTERNAL_ERROR).json({ error: "Failed to get scenario", detail: String(error), }); } } }); // API endpoint: update an existing scenario app.put("/_mock/scenarios/:name", async (req, res) => { try { const { name } = req.params; if (!name) { return res.status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST).json({ error: "Scenario name is required", }); } // Validate request with Zod const validationResult = validation_schemas_2.UpdateScenarioRequestSchema.safeParse(req.body); if (!validationResult.success) { return res .status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST) .json((0, validation_schemas_1.formatScenarioValidationErrors)(validationResult.error)); } const updateRequest = validationResult.data; // Update scenario using scenario manager const scenario = await scenarioManager.update(name, updateRequest); return res.json({ scenario, message: `Scenario '${scenario.name}' updated successfully`, }); } catch (error) { if (error instanceof scenario_types_1.ScenarioNotFoundError) { return res.status(404).json({ error: error.message, }); } else if (error instanceof scenario_types_1.EmptyScenarioError) { return res.status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST).json({ error: error.message, }); } else if (error instanceof scenario_types_1.DuplicateEndpointError) { return res.status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST).json({ error: error.message, }); } else if (error instanceof zod_1.z.ZodError) { return res .status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST) .json((0, validation_schemas_1.formatScenarioValidationErrors)(error)); } else { return res.status(validation_schemas_1.HTTP_STATUS.INTERNAL_ERROR).json({ error: "Failed to update scenario", detail: String(error), }); } } }); // API endpoint: delete a scenario app.delete("/_mock/scenarios/:name", async (req, res) => { try { const { name } = req.params; if (!name) { return res.status(validation_schemas_1.HTTP_STATUS.BAD_REQUEST).json({ error: "Scenario name is required", }); } // Delete scenario using scenario manager await scenarioManager.delete(name); return res.json({ success: true, message: `Scenario '${name}' deleted successfully`, }); } catch (error) { if (error instanceof scenario_types_1.ScenarioNotFoundError) { return res.status(404).json({ error: error.message, }); } else { return res.status(validation_schemas_1.HTTP_STATUS.INTERNAL_ERROR).json({ error: "Failed to delete scenario", 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 (0, route_matcher_1.getAllMockTemplates)(mockRoot); const match = (0, route_matcher_1.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("/"); } // Read mock response file from status.json const statusPath = (0, status_tracker_1.getStatusJsonPath)(mockRoot, apiPath, method); const status = await (0, status_tracker_1.readStatusJson)(statusPath); let mockFile; if (status && status.selected) { mockFile = status.selected; } else { mockFile = DEFAULT_MOCK_FILE; } const filePath = path_1.default.join(endpointDir, mockFile); if (!(await (0, response_renderer_1.mockFileExists)(filePath))) { return res.status(404).json({ error: "Mock file not found", file: filePath, availableFiles: await (0, route_matcher_1.getAvailableMockFiles)(endpointDir), }); } const mock = await (0, response_renderer_1.readMockResponse)(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 statusCode = (0, response_renderer_1.extractStatusCode)(mockFile); if (statusCode) { res.status(statusCode); } // Get delayMillisecond from status (already read above) let delayMillisecond = 0; if (status) { if (typeof status.delayMillisecond === "number" && status.delayMillisecond > 0) { delayMillisecond = status.delayMillisecond; } 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; } /** * Start the mock server * @param port - Port number to listen on * @param mockRoot - Root directory for mock files * @returns Promise that resolves when server starts */ 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); } }); } /***/ }), /***/ 252: /***/ ((module) => { module.exports = require("express"); /***/ }), /***/ 276: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { /** * API Contract: Create Endpoint * Feature: 003-add-endpoint * Endpoint: POST /_mock/endpoints * * This contract defines the request/response schema for creating new mock endpoints * through the dashboard UI. It uses Zod for runtime validation and TypeScript type * inference to ensure type safety across frontend and backend. */ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ScenarioErrorResponseSchema = exports.DeleteScenarioResponseSchema = exports.ActiveScenarioResponseSchema = exports.ListScenariosResponseSchema = exports.ScenarioResponseSchema = exports.UpdateScenarioRequestSchema = exports.CreateScenarioRequestSchema = exports.ActiveScenarioReferenceSchema = exports.ScenarioSchema = exports.ScenarioMetadataSchema = exports.EndpointConfigurationSchema = exports.HTTP_STATUS = exports.CreateEndpointErrorSchema = exports.CreateEndpointSuccessSchema = exports.CreateEndpointRequestSchema = exports.EndpointPathSchema = exports.HttpMethodSchema = void 0; exports.validateCreateEndpointRequest = validateCreateEndpointRequest; exports.formatValidationErrors = formatValidationErrors; exports.validateCreateScenarioRequest = validateCreateScenarioRequest; exports.validateUpdateScenarioRequest = validateUpdateScenarioRequest; exports.validateScenario = validateScenario; exports.formatScenarioValidationErrors = formatScenarioValidationErrors; const zod_1 = __webpack_require__(569); // ============================================================================ // HTTP Method Enum // ============================================================================ /** * Supported HTTP methods for mock endpoints */ exports.HttpMethodSchema = zod_1.z.enum([ "GET", "POST", "PUT", "DELETE", "PATCH", ]); // ============================================================================ // Path Validation // ============================================================================ /** * Endpoint path validation rules: * - Must start with / * - Can contain: letters, numbers, hyphens, slashes, curly braces (for params) * - Cannot contain invalid file system characters: : | < > " * ? * - Cannot start with /_mock/ (reserved for management API) * - Max length: 500 characters */ exports.EndpointPathSchema = zod_1.z .string() .min(1, "Path is required") .max(500, "Path is too long (max 500 characters)") .regex(/^\/[a-z0-9\-\/{}]*$/i, "Path must start with / and can only contain letters, numbers, hyphens, slashes, and {braces} for parameters") .refine((path) => !/[:"|*?<>]/.test(path), "Path contains invalid file system characters. Remove: : | < > \" * ?") .refine((path) => !path.startsWith("/_mock/"), "Cannot create endpoints with reserved /_mock prefix (used for management API)"); // ============================================================================ // Request Schema // ============================================================================ /** * Request body schema for POST /_mock/endpoints * * Example: * { * "path": "/pet/status/{id}", * "method": "GET" * } */ exports.CreateEndpointRequestSchema = zod_1.z.object({ /** * API endpoint path * @example "/users" * @example "/pet/status/{id}" * @example "/api/v1/products/{productId}/reviews" */ path: exports.EndpointPathSchema, /** * HTTP method for the endpoint * @example "GET" * @example "POST" */ method: exports.HttpMethodSchema, }); // ============================================================================ // Success Response Schema // ============================================================================ /** * Success response schema (HTTP 201 Created) * * Example: * { * "success": true, * "message": "Endpoint created successfully", * "endpoint": { * "path": "/pet/status/{id}", * "method": "GET", * "filesCreated": [ * "success-200.json", * "unexpected-error-default.json", * "status.json" * ], * "availableAt": "http://localhost:3000/pet/status/123", * "mockDirectory": "/mock/pet/status/{id}/GET" * } * } */ exports.CreateEndpointSuccessSchema = zod_1.z.object({ /** * Indicates successful endpoint creation */ success: zod_1.z.literal(true), /** * Human-readable success message */ message: zod_1.z.string(), /** * Details about the created endpoint */ endpoint: zod_1.z.object({ /** * The endpoint path that was created */ path: zod_1.z.string(), /** * The HTTP method for the endpoint */ method: exports.HttpMethodSchema, /** * List of files that were generated */ filesCreated: zod_1.z.array(zod_1.z.string()), /** * Example URL where the endpoint is now available * (with path parameters replaced with example values) */ availableAt: zod_1.z.string(), /** * File system directory where mock files are stored */ mockDirectory: zod_1.z.string(), }), }); // ============================================================================ // Error Response Schemas // ============================================================================ /** * Validation error detail */ const ValidationErrorDetailSchema = zod_1.z.object({ /** * Field that failed validation */ field: zod_1.z.string(), /** * Validation error message */ message: zod_1.z.string(), }); /** * Error response schema (HTTP 400, 409, 500) * * Example (400 Validation Error): * { * "error": "Validation failed", * "details": [ * { "field": "path", "message": "Path is required" }, * { "field": "method", "message": "Invalid enum value. Expected 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'" } * ] * } * * Example (409 Duplicate): * { * "error": "Endpoint already exists", * "existingEndpoint": { * "path": "/users", * "method": "GET", * "mockDirectory": "/mock/users/GET" * } * } * * Example (500 Server Error): * { * "error": "Failed to create endpoint files", * "detail": "EACCES: permission denied, mkdir '/mock/users'" * } */ exports.CreateEndpointErrorSchema = zod_1.z.object({ /** * Error message describing what went wrong */ error: zod_1.z.string(), /** * Validation error details (for 400 errors) */ details: zod_1.z.array(ValidationErrorDetailSchema).optional(), /** * Server error detail (for 500 errors) */ detail: zod_1.z.string().optional(), /** * Existing endpoint info (for 409 duplicate errors) */ existingEndpoint: zod_1.z .object({ path: zod_1.z.string(), method: zod_1.z.string(), mockDirectory: zod_1.z.string(), }) .optional(), }); // ============================================================================ // HTTP Status Codes // ============================================================================ /** * Expected HTTP status codes for this endpoint */ exports.HTTP_STATUS = { /** * 201 Created - Endpoint was successfully created */ CREATED: 201, /** * 400 Bad Request - Validation failed (invalid path, method, etc.) */ BAD_REQUEST: 400, /** * 409 Conflict - Endpoint with same path and method already exists */ CONFLICT: 409, /** * 500 Internal Server Error - File system error, permission denied, etc. */ INTERNAL_ERROR: 500, }; // ============================================================================ // Contract Validation Helpers // ============================================================================ /** * Validate a request against the schema */ function validateCreateEndpointRequest(data) { const result = exports.CreateEndpointRequestSchema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } else { return { success: false, errors: result.error }; } } /** * Format Zod errors for API response */ function formatValidationErrors(zodError) { return { error: "Validation failed", details: zodError.errors.map((err) => ({ field: err.path.join("."), message: err.message, })), }; } // ============================================================================ // Scenario Management Validation Schemas // Feature: 004-scenario-management // ============================================================================ /** * Endpoint configuration schema for scenarios * * Defines how a single endpoint should behave within a scenario: * - Which mock response file to use * - Response delay in milliseconds */ exports.EndpointConfigurationSchema = zod_1.z.object({ /** * API endpoint path (must start with /) * @example "/pet/status" * @example "/user/login" */ path: zod_1.z .string() .min(1, "Path is required") .startsWith("/", "Path must start with /") .regex(/^[/a-zA-Z0-9{}\-_]+$/, "Path can only contain letters, numbers, hyphens, underscores, slashes, and {braces} for paramet