From a67a1deddbde7a4f19982c9b17338272cea742d1 Mon Sep 17 00:00:00 2001 From: Tabrez Date: Mon, 26 Jan 2026 13:35:17 +0530 Subject: [PATCH 1/3] feat(simulation): add error catalog constants for simulation data --- src/constants/simulation.constants.js | 118 ++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/constants/simulation.constants.js diff --git a/src/constants/simulation.constants.js b/src/constants/simulation.constants.js new file mode 100644 index 0000000..bf9250a --- /dev/null +++ b/src/constants/simulation.constants.js @@ -0,0 +1,118 @@ +export const ERROR_TYPES = { + AUTH: 'auth', + DATABASE: 'database', + NETWORK: 'network', + VALIDATION: 'validation', + PAYMENT: 'payment', + SERVER: 'server', +}; + +export const ERROR_CATALOG = [ + // --- AUTH ERRORS --- + { + type: ERROR_TYPES.AUTH, + statusCode: 401, + message: 'Whoops! Authentication Failed.', + hint: 'Missing or invalid token. did you forget to add "Bearer " in the header?', + }, + { + type: ERROR_TYPES.AUTH, + statusCode: 401, + message: 'Token Expired', + hint: 'Your session expired. Hit the refresh endpoint to get a shiny new token.', + }, + { + type: ERROR_TYPES.AUTH, + statusCode: 403, + message: 'Access Denied!', + hint: 'You are trying to touch something above your pay grade (insufficient permissions).', + }, + + // --- DATABASE ERRORS --- + { + type: ERROR_TYPES.DATABASE, + statusCode: 503, + message: 'DB Connection Timeout', + hint: 'Database is taking a nap (timeout). Check your connection string or network.', + }, + { + type: ERROR_TYPES.DATABASE, + statusCode: 409, + message: 'Duplicate Entry', + hint: 'This record already exists. Unique constraint violation.', + }, + { + type: ERROR_TYPES.DATABASE, + statusCode: 422, + message: 'Invalid ID Format', + hint: 'That doesn\'t look like a valid MongoDB ObjectId.', + }, + + // --- NETWORK ERRORS --- + { + type: ERROR_TYPES.NETWORK, + statusCode: 504, + message: 'Gateway Timeout', + hint: 'Upstream server is ghosting us. It took too long to respond.', + }, + { + type: ERROR_TYPES.NETWORK, + statusCode: 502, + message: 'Bad Gateway', + hint: 'Received garbage from the upstream server. Maybe it\'s down?', + }, + { + type: ERROR_TYPES.NETWORK, + statusCode: 503, + message: 'Service Unavailable', + hint: 'Server is overloaded or under maintenance. Try again in a bit.', + }, + + // --- VALIDATION ERRORS --- + { + type: ERROR_TYPES.VALIDATION, + statusCode: 400, + message: 'Validation Error', + hint: 'You missed some required fields. Check the docs!', + }, + { + type: ERROR_TYPES.VALIDATION, + statusCode: 400, + message: 'Invalid Email', + hint: 'That doesn\'t look like a real email address.', + }, + { + type: ERROR_TYPES.VALIDATION, + statusCode: 422, + message: 'Weak Password', + hint: 'Password is too weak. Add some spice (special chars, numbers).', + }, + + // --- PAYMENT ERRORS --- + { + type: ERROR_TYPES.PAYMENT, + statusCode: 402, + message: 'Payment Required', + hint: 'Card declined or insufficient funds. Money makes the world go round.', + }, + { + type: ERROR_TYPES.PAYMENT, + statusCode: 422, + message: 'Invalid Card Info', + hint: 'Check the card number, CVV or expiry. Something is off.', + }, + { + type: ERROR_TYPES.PAYMENT, + statusCode: 409, + message: 'Double Charge Prevented', + hint: 'Transaction already processed. Idempotency saved you from paying twice.', + }, + + // --- SERVER ERRORS --- + { + type: ERROR_TYPES.SERVER, + statusCode: 500, + message: 'Internal Server Error', + hint: 'Something exploded on our end. Check the server logs.', + }, +]; From 80c62f63e4303f04ad7526b914496d31c563ad14 Mon Sep 17 00:00:00 2001 From: Tabrez Date: Mon, 26 Jan 2026 13:42:58 +0530 Subject: [PATCH 2/3] feat(simulation): implement full simulation API engine with chaos, delay, and random generation logic --- src/app.js | 3 + src/constants/simulation.constants.js | 6 +- src/controllers/simulation.controller.js | 95 ++++++++++++++++++ src/routes/simulation.routes.js | 11 ++ src/services/simulation.service.js | 122 +++++++++++++++++++++++ 5 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 src/controllers/simulation.controller.js create mode 100644 src/routes/simulation.routes.js create mode 100644 src/services/simulation.service.js diff --git a/src/app.js b/src/app.js index 40e4a4a..68ff1f5 100644 --- a/src/app.js +++ b/src/app.js @@ -33,12 +33,15 @@ import userRoutes from './routes/user.routes.js'; import productRoutes from './routes/product.routes.js'; import cartRoutes from './routes/cart.routes.js'; import paymentRoutes from './routes/payment.routes.js'; +import simulationRoutes from './routes/simulation.routes.js'; app.use('/api/v1/auth', authRoutes); app.use('/api/v1/users', userRoutes); app.use('/api/v1/store/products', productRoutes); app.use('/api/v1/store/cart', cartRoutes); +app.use('/api/v1/store/cart', cartRoutes); app.use('/api/v1/payments', paymentRoutes); +app.use('/api/v1/simulation', simulationRoutes); // // Simple route for checking server status app.get('/', (req, res) => { diff --git a/src/constants/simulation.constants.js b/src/constants/simulation.constants.js index bf9250a..ce5e86c 100644 --- a/src/constants/simulation.constants.js +++ b/src/constants/simulation.constants.js @@ -45,7 +45,7 @@ export const ERROR_CATALOG = [ type: ERROR_TYPES.DATABASE, statusCode: 422, message: 'Invalid ID Format', - hint: 'That doesn\'t look like a valid MongoDB ObjectId.', + hint: "That doesn't look like a valid MongoDB ObjectId.", }, // --- NETWORK ERRORS --- @@ -59,7 +59,7 @@ export const ERROR_CATALOG = [ type: ERROR_TYPES.NETWORK, statusCode: 502, message: 'Bad Gateway', - hint: 'Received garbage from the upstream server. Maybe it\'s down?', + hint: "Received garbage from the upstream server. Maybe it's down?", }, { type: ERROR_TYPES.NETWORK, @@ -79,7 +79,7 @@ export const ERROR_CATALOG = [ type: ERROR_TYPES.VALIDATION, statusCode: 400, message: 'Invalid Email', - hint: 'That doesn\'t look like a real email address.', + hint: "That doesn't look like a real email address.", }, { type: ERROR_TYPES.VALIDATION, diff --git a/src/controllers/simulation.controller.js b/src/controllers/simulation.controller.js new file mode 100644 index 0000000..daf2229 --- /dev/null +++ b/src/controllers/simulation.controller.js @@ -0,0 +1,95 @@ +import asyncHandler from '../utils/asyncHandler.js'; +import simulationService from '../services/simulation.service.js'; + +class SimulationController { + /** + * Get a random error response. + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ + getRandomError = asyncHandler(async (req, res) => { + // Extract the 'type' query parameter (e.g. /random?type=auth) + const { type } = req.query; + + const error = simulationService.getRandomError(type); + + // We want to simulate the error, so we return the status code from the error object + // But we wrap it in a JSON response so the developer can see the details + res.status(error.statusCode).json({ + error: true, + ...error, + timestamp: new Date().toISOString(), + }); + }); + + /** + * Get a custom error based on status code. + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ + getCustomError = asyncHandler(async (req, res) => { + const { code } = req.query; + + // Default to 400 if no code is provided + if (!code) { + return res.status(400).json({ + error: true, + type: 'validation', + statusCode: 400, + message: 'Missing Status Code', + hint: 'Please provide a "code" query parameter (e.g., ?code=418).', + timestamp: new Date().toISOString(), + }); + } + + const error = simulationService.getCustomError(code); + + res.status(error.statusCode).json({ + ...error, + timestamp: new Date().toISOString(), + }); + }); + + /** + * Delayed response simulation. + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ + getDelayedResponse = asyncHandler(async (req, res) => { + let { ms } = req.query; + + // Parse and set defaults/limits + ms = parseInt(ms, 10); + if (isNaN(ms) || ms < 0) ms = 1000; // Default 1s + if (ms > 10000) ms = 10000; // Cap at 10s to preserve server sanity + + await simulationService.simulateDelay(ms); + + res.status(200).json({ + error: false, + message: `Sorry for the wait! I was napping for ${ms}ms.`, + delay: ms, + timestamp: new Date().toISOString(), + }); + }); + + /** + * Chaos mode endpoint. + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ + getChaosResponse = asyncHandler(async (req, res) => { + // Determine the result from the service + const result = await simulationService.getChaosResponse(); + + // The status code comes from the result (could be 200, 4xx, or 5xx) + res.status(result.statusCode).json({ + // Error flag only true if status is 4xx or 5xx + error: result.statusCode >= 400, + ...result, + timestamp: new Date().toISOString(), + }); + }); +} + +export default new SimulationController(); diff --git a/src/routes/simulation.routes.js b/src/routes/simulation.routes.js new file mode 100644 index 0000000..6c28fa6 --- /dev/null +++ b/src/routes/simulation.routes.js @@ -0,0 +1,11 @@ +import express from 'express'; +import simulationController from '../controllers/simulation.controller.js'; + +const router = express.Router(); + +router.get('/random', simulationController.getRandomError); +router.get('/custom', simulationController.getCustomError); +router.get('/delay', simulationController.getDelayedResponse); +router.get('/chaos', simulationController.getChaosResponse); + +export default router; diff --git a/src/services/simulation.service.js b/src/services/simulation.service.js new file mode 100644 index 0000000..dd79e0f --- /dev/null +++ b/src/services/simulation.service.js @@ -0,0 +1,122 @@ +import { ERROR_CATALOG } from '../constants/simulation.constants.js'; + +class SimulationService { + /** + * Selects a random error from the catalog, optionally filtered by type. + * @param {string} type - (Optional) Error category to filter by (e.g., 'auth', 'database') + * @returns {Object} Random error object + */ + getRandomError(type) { + let pool = ERROR_CATALOG; + + // If a type is provided, we filter the errors + if (type) { + const filtered = ERROR_CATALOG.filter((err) => err.type === type); + + // If the developer asks for a category that doesn't exist or has no errors, + // we should let them know instead of just returning a random one. + if (filtered.length === 0) { + // Find available types for the hint + const availableTypes = [ + ...new Set(ERROR_CATALOG.map((e) => e.type)), + ].join(', '); + + return { + statusCode: 400, + error: true, + type: 'validation', // This corresponds to a validation error on the *simulation API* itself + message: `Invalid error type: '${type}'`, + hint: `Available types are: ${availableTypes}`, + }; + } + + pool = filtered; + } + + const randomIndex = Math.floor(Math.random() * pool.length); + return pool[randomIndex]; + } + + /** + * Generates a custom error response based on the provided status code. + * @param {number|string} code - The HTTP status code to simulate + * @returns {Object} Custom error object + */ + getCustomError(code) { + const statusCode = parseInt(code, 10); + + if (isNaN(statusCode) || statusCode < 100 || statusCode > 599) { + return { + statusCode: 400, + error: true, + type: 'validation', + message: 'Invalid Status Code', + hint: 'Please provide a valid HTTP status code between 100 and 599.', + }; + } + + return { + statusCode, + error: statusCode >= 400, // Only consider 4xx and 5xx as "errors" for the error flag + type: 'custom', + message: `Simulated ${statusCode} Response`, + hint: `You asked for a ${statusCode}, you got a ${statusCode}.`, + }; + } + + /** + * Simulates a network delay. + * @param {number} ms - Milliseconds to delay (default 1000) + * @returns {Promise} Resolves with the delay time + */ + async simulateDelay(ms = 1000) { + return new Promise((resolve) => { + setTimeout(() => resolve(ms), ms); + }); + } + + /** + * Chaos mode! Randomly returns success, error, or a delayed response. + * Distribution: + * - 40% Success + * - 40% Error + * - 20% High Latency (Slow response) + */ + async getChaosResponse() { + const roll = Math.random(); + // 40% Chance: Success + if (roll < 0.4) { + return { + type: 'mode_chaos_success', + statusCode: 200, + message: 'Survived the chaos! Operation successful.', + }; + } + + // 40% Chance: Random Error (re-using existing random logic) + if (roll < 0.8) { + const error = this.getRandomError(); + return { + ...error, + type: 'mode_chaos_error', // Tagging it so distinct from normal random + originalType: error.type, + }; + } + + // 20% Chance: Latency Spike + // Random delay between 1s and 5s + const delay = Math.floor(Math.random() * 4000) + 1000; + + // We wait here to simulate the server actually hanging + await this.simulateDelay(delay); + + return { + type: 'mode_chaos_delay', + statusCode: 200, + message: `Phew! Request lagged for ${delay}ms but finally made it.`, + delay, + }; + } +} + +export default new SimulationService(); From f4ef195b6a1a80d6f0c6f3b94b0b1aa0450b0604 Mon Sep 17 00:00:00 2001 From: Tabrez Date: Mon, 26 Jan 2026 13:44:28 +0530 Subject: [PATCH 3/3] docs(simulation): add comprehensive API usage guide and examples --- docs/SIMULATION_API.md | 195 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 docs/SIMULATION_API.md diff --git a/docs/SIMULATION_API.md b/docs/SIMULATION_API.md new file mode 100644 index 0000000..bb8f935 --- /dev/null +++ b/docs/SIMULATION_API.md @@ -0,0 +1,195 @@ +# Developer Error Simulation API + +This API module is designed to help frontend and mobile developers test their applications against various failure scenarios. Instead of hoping your app handles errors correctly, you can use these endpoints to force specific errors, delays, or chaotic behavior. + +**Base URL**: `/api/v1/simulation` +**Module Location**: `src/services/simulation.service.js` + +> **Note**: This API is for **testing and simulation purposes only**. It should NOT be used in production environments as it intentionally generates errors and delays. + +--- + +## 1. Random Error Generator + +Get a random realistic error to test your global error handling or specific error screens. + +**Endpoint**: `GET /random` + +### Query Parameters + +| Param | Type | Description | +| :----- | :----- | :-------------------------------------------------------------------------------- | +| `type` | string | _(Optional)_ Filter by category. See [Error Categories](#error-categories) below. | + +### Examples + +**Request (Any random error):** +`GET /api/v1/simulation/random` + +**Response:** + +```json +{ + "error": true, + "type": "database", + "statusCode": 503, + "message": "DB Connection Timeout", + "hint": "Database is taking a nap (timeout). Check your connection string or network.", + "timestamp": "2026-01-26T07:15:00.000Z" +} +``` + +**Request (Specific category):** +`GET /api/v1/simulation/random?type=auth` + +**Response:** + +```json +{ + "error": true, + "type": "auth", + "statusCode": 401, + "message": "Token Expired", + "hint": "Your session expired. Hit the refresh endpoint to get a shiny new token.", + "timestamp": "2026-01-26T07:16:00.000Z" +} +``` + +--- + +## 2. Custom Status Code + +Force the API to return a specific HTTP status code. Useful for testing how your UI handles specific edge cases like 429 (Too Many Requests) or 418 (I'm a teapot). + +**Endpoint**: `GET /custom` + +### Query Parameters + +| Param | Type | Description | +| :----- | :----- | :-------------------------------------------------------------------------- | +| `code` | number | **(Required)** The HTTP status code you want returned (e.g. 404, 500, 201). | + +### Example + +**Request:** +`GET /api/v1/simulation/custom?code=429` + +**Response:** + +```json +{ + "statusCode": 429, + "error": true, + "type": "custom", + "message": "Simulated 429 Response", + "hint": "You asked for a 429, you got a 429.", + "timestamp": "2026-01-26T07:20:00.000Z" +} +``` + +--- + +## 3. Latency Simulation (Delay) + +Simulate a slow network connection to test your loading spinners, skeleton screens, and timeout logic. + +**Endpoint**: `GET /delay` + +### Query Parameters + +| Param | Type | Description | +| :---- | :----- | :---------------------------------------------------------------------------- | +| `ms` | number | _(Optional)_ Milliseconds to delay. Defaults to 1000ms. Max limit is 10000ms. | + +### Example + +**Request (Wait for 3 seconds):** +`GET /api/v1/simulation/delay?ms=3000` + +**Response (Returns after 3 seconds):** + +```json +{ + "error": false, + "message": "Sorry for the wait! I was napping for 3000ms.", + "delay": 3000, + "timestamp": "2026-01-26T07:25:00.000Z" +} +``` + +--- + +## 4. Chaos Mode + +This endpoint is for stability testing ("Chaos Engineering"). It randomly decides what to return based on a weighted probability distribution. + +**Endpoint**: `GET /chaos` + +### Behavior Distribution + +- **40% Success**: Returns standard 200 OK. +- **40% Error**: Returns a random error from the catalog. +- **20% High Latency**: Waits between 1s and 5s, then returns success. + +### Example + +**Request:** +`GET /api/v1/simulation/chaos` + +**Possible Response (Chaos Error):** + +```json +{ + "error": true, + "type": "mode_chaos_error", + "originalType": "payment", + "statusCode": 402, + "message": "Payment Required", + "hint": "Card declined or insufficient funds. Money makes the world go round.", + "timestamp": "2026-01-26T07:30:00.000Z" +} +``` + +**Possible Response (Chaos Success):** + +```json +{ + "error": false, + "type": "mode_chaos_success", + "statusCode": 200, + "message": "Survived the chaos! Operation successful.", + "timestamp": "2026-01-26T07:31:00.000Z" +} +``` + +--- + +## Error Categories + +These are the valid values for the `type` parameter in the Random Error endpoint: + +- `auth`: Authentication and authorization issues (401, 403). +- `database`: DB connection timeouts, duplicate entries (503, 409). +- `network`: Gateway timeouts, bad gateways (502, 504). +- `validation`: Bad requests, missing fields (400, 422). +- `payment`: Payment required, card errors (402). +- `server`: Generic internal server errors (500). + +--- + +## Quick Test URLs (Localhost) + +Copy-paste these into your browser or Postman to test immediately: + +- **Random Error**: `http://localhost:3000/api/v1/simulation/random` +- **Auth Error**: `http://localhost:3000/api/v1/simulation/random?type=auth` +- **Database Error**: `http://localhost:3000/api/v1/simulation/random?type=database` +- **Force 418 (Teapot)**: `http://localhost:3000/api/v1/simulation/custom?code=418` +- **Delay (3 Seconds)**: `http://localhost:3000/api/v1/simulation/delay?ms=3000` +- **Chaos Mode**: `http://localhost:3000/api/v1/simulation/chaos` + +--- + +## Author + +Implemented by **[TABREZ RABBANI]** as a contribution to the Open Source community.