@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
JavaScript
/******/ (() => { // 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