From 60d01697e8e10f94cb6bf58313a5d280eabaed2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:24:58 +0000 Subject: [PATCH 1/5] Initial plan From 504f24ad7db1bc2ab796094c0f318afc1ed2b918 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:27:09 +0000 Subject: [PATCH 2/5] Create comprehensive best-practice README with record/replay documentation Co-authored-by: rawi96 <37732078+rawi96@users.noreply.github.com> --- README.md | 625 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 612 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e9bea26..2659c80 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,625 @@ -# Mockserver +# Smart Mock Server -A simple node.js based web and mail mockserver +[![npm version](https://badge.fury.io/js/@smartive%2Fmockserver.svg)](https://www.npmjs.com/package/@smartive/mockserver) +[![Docker Pulls](https://img.shields.io/docker/pulls/smartive/mockserver)](https://hub.docker.com/r/smartive/mockserver) -## Setup +A lightweight, powerful mock server with API recording and replay capabilities for E2E testing. Perfect for mocking server-side API calls in applications with Server-Side Rendering (SSR). +## Why This Mock Server? + +Modern web frameworks like Next.js, Remix, and SvelteKit fetch data server-side for better SEO and performance. Traditional browser-based mocking tools can't intercept these server-side requests, making E2E testing challenging. + +### The Problem + +**Client-Side Rendering (CSR):** ``` -npm i -npm start +Browser → External API ✅ Easy to mock with browser tools +``` + +**Server-Side Rendering (SSR):** ``` +Browser → App Server → External API ❌ Browser tools can't intercept +``` + +### The Solution -## Usage with Docker Compose +Instead of intercepting requests, let them flow naturally to a mock server: ``` +Browser → App Server → Mock Server → External API (in record mode) +Browser → App Server → Mock Server (in replay mode) +``` + +## Key Features + +- 🎯 **Proxy Mode with Recording**: Record real API responses automatically +- 🔄 **Replay Mode**: Use recorded responses for deterministic testing +- 📝 **Manual Mocking**: Define custom mock responses via HTTP API +- 📧 **SMTP Server**: Built-in mail server for email testing +- 🔍 **Request Inspection**: Track all incoming requests and emails +- 🐳 **Docker Ready**: Available as a lightweight Docker image +- ⚡ **Simple & Fast**: ~350 lines of code, minimal dependencies + +## Quick Start + +### Using Docker (Recommended) + +```bash +docker run -p 1080:1080 smartive/mockserver +``` + +### Using Docker Compose + +```yaml version: "3.3" services: - image: smartive/mockserver - environment: - MOCK_PATH: /mock - MOCK_HOST: 0.0.0.0 - MOCK_HTTP_PORT: 1080 + mockserver: + image: smartive/mockserver + ports: + - "1080:1080" + - "25:25" # Optional: SMTP server + environment: + MOCK_PATH: /mock + MOCK_HOST: 0.0.0.0 + MOCK_HTTP_PORT: 1080 + MOCK_SMTP_PORT: 25 +``` + +### Using npm + +```bash +npm install -g @smartive/mockserver +npx @smartive/mockserver +``` + +### From Source + +```bash +git clone https://github.com/smartive/mockserver.git +cd mockserver +npm install +npm start +``` + +## Usage Patterns + +### Pattern 1: Record & Replay (Recommended) + +This is the most powerful pattern - record real API responses and replay them deterministically. + +#### Step 1: Configure Your App + +Point your app's API base URL to the mock server. The mock server expects the original host as part of the URL path: + +```bash +# .env.test +STORYBLOK_API_BASE_URL="http://localhost:1080/api.storyblok.com" +``` + +This transforms requests from: +``` +https://api.storyblok.com/v2/cdn/stories/home +``` + +to: +``` +http://localhost:1080/api.storyblok.com/v2/cdn/stories/home +``` + +#### Step 2: Record Mode + +Enable recording to capture real API responses: + +```javascript +// Enable recording mode +await fetch("http://localhost:1080/mock/recordings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + active: true, + // Optional: Remove dynamic fields from hash calculation + deleteBodyAttributesForHash: ["timestamp", "requestId"], + deleteHeadersForHash: ["authorization", "x-request-id"], + }), +}); + +// Run your E2E tests - all API calls will be recorded +await page.goto("http://localhost:3000"); +// ... perform your test actions ... +``` + +#### Step 3: Export Recordings + +After recording, export the captured responses: + +```javascript +const response = await fetch("http://localhost:1080/mock/recordings"); +const recordings = await response.json(); + +// Save to your repository +import { writeFileSync } from "fs"; +writeFileSync( + "test/e2e/recordings/home.json", + JSON.stringify(recordings, null, 2) +); +``` + +⚠️ **Important**: Review exported recordings for sensitive data (API keys, tokens, passwords) before committing! + +#### Step 4: Replay Mode + +In your CI or test environment, use the recorded responses: + +```javascript +import recordings from "../recordings/home.json"; + +test("renders homepage with mocked data", async ({ page }) => { + // Load recordings into mock server + await fetch("http://localhost:1080/mock/recordings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + active: false, // Replay mode + recordings, + }), + }); + + await page.goto("http://localhost:3000"); + await expect(page.getByText("Expected Content")).toBeVisible(); +}); +``` + +### Pattern 2: Manual Mocking + +For simpler scenarios or testing edge cases, define mock responses manually: + +```javascript +// Mock a specific endpoint +await fetch("http://localhost:1080/mock/mock", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + request: { + match: "/api.example.com/users/.*", // Regex pattern + bodyMatch: ".*premium.*", // Optional: match request body + }, + response: { + status: 200, + contentType: "application/json", + body: { + id: "123", + name: "Test User", + tier: "premium", + }, + }, + }), +}); +``` + +#### Multiple Responses + +Return different responses for consecutive calls: + +```javascript +await fetch("http://localhost:1080/mock/mock", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + request: { + match: "/api.example.com/status", + }, + response: { + status: 200, + bodies: [ + { status: "processing" }, + { status: "completed" }, + ], + }, + }), +}); +``` + +### Pattern 3: Email Testing + +The built-in SMTP server captures emails for testing: + +```javascript +// Send an email from your app +// ... + +// Wait for and retrieve the email +const emailResponse = await fetch("http://localhost:1080/mock/mails/next"); +const email = await emailResponse.text(); + +// Assert email contents +expect(email).toContain("Welcome to our service"); +``` + +## API Reference + +### Recording Endpoints + +#### `POST /mock/recordings` + +Configure recording/replay mode. + +**Request Body:** +```javascript +{ + active: true, // true = record mode, false = replay mode + recordings: {}, // Recordings to load in replay mode + deleteBodyAttributesForHash: ["timestamp"], // Exclude from hash + deleteHeadersForHash: ["authorization"], // Exclude from hash + forwardHeadersForRoute: [ // Forward headers in proxy mode + { + route: "/api.example.com", + headers: { "X-API-Key": "secret" } + } + ], + failedRequestsResponse: {} // Response when no match found (optional) +} +``` + +**Response:** `204 No Content` + +#### `GET /mock/recordings` + +Retrieve all recorded API responses. + +**Response:** +```javascript +{ + "hash1": [{ + request: { url: "...", method: "GET", headers: {...}, body: {} }, + body: { /* response body */ }, + status: 200, + durationMs: 150 + }], + "hash2": [...] +} +``` + +#### `POST /mock/recordings/rehash` + +Recalculate hashes for all recordings based on current hash configuration. + +**Response:** Updated recordings object + +### Mock Endpoints + +#### `POST /mock/mock` + +Register a manual mock route. + +**Request Body:** +```javascript +{ + request: { + match: "/api\\.example\\.com/users/.*", // URL regex + bodyMatch: ".*premium.*" // Optional: request body regex + }, + response: { + status: 200, + contentType: "application/json", // Optional + body: { /* response data */ }, + // OR for multiple responses: + bodies: [{ /* first */ }, { /* second */ }] + } +} +``` + +**Response:** `204 No Content` + +#### `GET /mock/routes` + +List all registered mock routes. + +**Response:** Array of route configurations + +### Request Inspection + +#### `GET /mock/calls` + +Get all captured requests. + +**Response:** +```javascript +[ + { + method: "GET", + url: "/api.example.com/users/123", + headers: { /* ... */ }, + body: { /* ... */ } + } +] +``` + +#### `GET /mock/calls/next` + +Wait for the next request (long-polling). + +**Response:** Single request object (when available) + +### Email Testing + +#### `GET /mock/mails` + +Get all captured emails. + +**Response:** Array of raw email messages + +#### `GET /mock/mails/next` + +Wait for the next email (long-polling). + +**Response:** Single email message (when available) + +### Utility Endpoints + +#### `POST /mock/reset` + +Reset all state (routes, recordings, calls, emails). + +**Response:** `204 No Content` + +#### `POST /mock/reset/calls` + +Reset only calls and emails (keeps routes and recordings). + +**Response:** `204 No Content` + +## Configuration + +Configure the server using environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `MOCK_HTTP_PORT` or `PORT` | `1080` | HTTP server port | +| `MOCK_HTTPS_PORT` or `HTTPS_PORT` | - | HTTPS server port (optional) | +| `MOCK_SMTP_PORT` or `SMTP_PORT` | `25` | SMTP server port | +| `MOCK_HOST` or `HOST` | `0.0.0.0` | Server host | +| `MOCK_PATH` | `/mock` | Base path for API endpoints | + +### HTTPS Support + +To enable HTTPS, set the `MOCK_HTTPS_PORT` environment variable and provide SSL certificates: + +```bash +# Certificate files must be at: +# - cert/localhost.key +# - cert/localhost.crt + +MOCK_HTTPS_PORT=1443 npm start +``` + +## Complete Example: Playwright Test + +```javascript +import { test, expect } from "@playwright/test"; +import recordings from "./recordings/homepage.json"; + +test.describe("Homepage E2E", () => { + test.beforeEach(async () => { + // Reset mock server + await fetch("http://localhost:1080/mock/reset", { method: "POST" }); + + // Load recordings + await fetch("http://localhost:1080/mock/recordings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + active: false, + recordings, + }), + }); + }); + + test("displays content from mocked API", async ({ page }) => { + await page.goto("http://localhost:3000"); + + // Verify content rendered from mock data + await expect(page.getByText("Mock Server with API Recording")).toBeVisible(); + + // Verify API was called + const calls = await fetch("http://localhost:1080/mock/calls").then(r => r.json()); + expect(calls.length).toBeGreaterThan(0); + }); + + test("handles API errors gracefully", async ({ page }) => { + // Override with error response + await fetch("http://localhost:1080/mock/mock", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + request: { match: "/api.example.com/.*" }, + response: { status: 500, body: { error: "Internal Server Error" } }, + }), + }); + + await page.goto("http://localhost:3000"); + await expect(page.getByText("Something went wrong")).toBeVisible(); + }); +}); +``` + +## Recording Script Example + +Create a script to record new API interactions: + +```javascript +// scripts/record-api.js +import { chromium } from "@playwright/test"; +import { writeFileSync } from "fs"; + +async function record() { + // Start recording + await fetch("http://localhost:1080/mock/recordings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + active: true, + deleteBodyAttributesForHash: ["timestamp", "_v"], + deleteHeadersForHash: ["cookie", "authorization"], + }), + }); + + // Run through your app + const browser = await chromium.launch(); + const page = await browser.newPage(); + + await page.goto("http://localhost:3000"); + await page.click("text=Get Started"); + await page.fill("#email", "test@example.com"); + await page.click("button[type=submit]"); + + await browser.close(); + + // Export recordings + const response = await fetch("http://localhost:1080/mock/recordings"); + const recordings = await response.json(); + + writeFileSync( + "test/recordings/user-flow.json", + JSON.stringify(recordings, null, 2) + ); + + console.log("✅ Recordings saved to test/recordings/user-flow.json"); +} + +record().catch(console.error); +``` + +Run it with: +```bash +node scripts/record-api.js +``` + +## Best Practices + +### 1. Keep Recordings Small and Focused + +Record separate files for different test scenarios: +``` +test/recordings/ + ├── homepage.json + ├── login-flow.json + ├── checkout.json + └── admin-panel.json +``` + +### 2. Sanitize Sensitive Data + +Always exclude sensitive information from recordings: + +```javascript +{ + active: true, + deleteBodyAttributesForHash: [ + "password", + "token", + "apiKey", + "creditCard" + ], + deleteHeadersForHash: [ + "authorization", + "cookie", + "x-api-key" + ] +} +``` + +### 3. Version Your Recordings + +Commit recordings to version control alongside your tests: +```bash +git add test/recordings/ +git commit -m "Update API recordings for new endpoint" +``` + +### 4. Reset Between Tests + +Always reset the mock server to ensure test isolation: + +```javascript +test.beforeEach(async () => { + await fetch("http://localhost:1080/mock/reset", { method: "POST" }); +}); +``` + +### 5. Handle Missing Recordings Gracefully + +Use `failedRequestsResponse` to provide fallbacks: + +```javascript +await fetch("http://localhost:1080/mock/recordings", { + method: "POST", + body: JSON.stringify({ + active: false, + recordings, + failedRequestsResponse: { + error: "Not found in recordings", + data: null + } + }), +}); ``` -## HTTPS +## Troubleshooting + +### Recordings Not Matching + +If recordings aren't being replayed: + +1. Check the hash by examining the error message in server logs +2. Ensure `deleteBodyAttributesForHash` and `deleteHeadersForHash` exclude dynamic fields +3. Use `POST /mock/recordings/rehash` to recalculate hashes after changing configuration + +### Large Recording Files + +If recordings are too large: + +1. Split into multiple smaller files per test/feature +2. Remove unnecessary fields using `deleteBodyAttributesForHash` +3. Consider recording only critical paths, use manual mocks for edge cases + +### HTTPS Certificate Errors + +When using HTTPS mode, you may need to trust self-signed certificates: + +```javascript +// In Playwright +const context = await browser.newContext({ + ignoreHTTPSErrors: true +}); +``` + +### Port Already in Use + +If port 1080 is occupied: + +```bash +# Use a different port +MOCK_HTTP_PORT=8080 npm start + +# Or find and kill the process +lsof -ti:1080 | xargs kill -9 +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +ISC + +## Links + +- [GitHub Repository](https://github.com/smartive/mockserver) +- [npm Package](https://www.npmjs.com/package/@smartive/mockserver) +- [Docker Hub](https://hub.docker.com/r/smartive/mockserver) +- [Issues](https://github.com/smartive/mockserver/issues) + +--- -Set a `MOCK_HTTPS_PORT` env variable to start an https server as well. +Built with ❤️ by [smartive](https://smartive.ch) From 50cfd2de25c049c2c0b92f6d8ebc072ff913c4bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:27:37 +0000 Subject: [PATCH 3/5] Fix prettier formatting for README Co-authored-by: rawi96 <37732078+rawi96@users.noreply.github.com> --- README.md | 220 ++++++++++++++++++++++++---------------------- package-lock.json | 2 + 2 files changed, 115 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 2659c80..32d9f0d 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,13 @@ Modern web frameworks like Next.js, Remix, and SvelteKit fetch data server-side ### The Problem **Client-Side Rendering (CSR):** + ``` Browser → External API ✅ Easy to mock with browser tools ``` **Server-Side Rendering (SSR):** + ``` Browser → App Server → External API ❌ Browser tools can't intercept ``` @@ -51,13 +53,13 @@ docker run -p 1080:1080 smartive/mockserver ### Using Docker Compose ```yaml -version: "3.3" +version: '3.3' services: mockserver: image: smartive/mockserver ports: - - "1080:1080" - - "25:25" # Optional: SMTP server + - '1080:1080' + - '25:25' # Optional: SMTP server environment: MOCK_PATH: /mock MOCK_HOST: 0.0.0.0 @@ -97,11 +99,13 @@ STORYBLOK_API_BASE_URL="http://localhost:1080/api.storyblok.com" ``` This transforms requests from: + ``` https://api.storyblok.com/v2/cdn/stories/home ``` to: + ``` http://localhost:1080/api.storyblok.com/v2/cdn/stories/home ``` @@ -112,19 +116,19 @@ Enable recording to capture real API responses: ```javascript // Enable recording mode -await fetch("http://localhost:1080/mock/recordings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ +await fetch('http://localhost:1080/mock/recordings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ active: true, // Optional: Remove dynamic fields from hash calculation - deleteBodyAttributesForHash: ["timestamp", "requestId"], - deleteHeadersForHash: ["authorization", "x-request-id"], + deleteBodyAttributesForHash: ['timestamp', 'requestId'], + deleteHeadersForHash: ['authorization', 'x-request-id'], }), }); // Run your E2E tests - all API calls will be recorded -await page.goto("http://localhost:3000"); +await page.goto('http://localhost:3000'); // ... perform your test actions ... ``` @@ -133,15 +137,12 @@ await page.goto("http://localhost:3000"); After recording, export the captured responses: ```javascript -const response = await fetch("http://localhost:1080/mock/recordings"); +const response = await fetch('http://localhost:1080/mock/recordings'); const recordings = await response.json(); // Save to your repository -import { writeFileSync } from "fs"; -writeFileSync( - "test/e2e/recordings/home.json", - JSON.stringify(recordings, null, 2) -); +import { writeFileSync } from 'fs'; +writeFileSync('test/e2e/recordings/home.json', JSON.stringify(recordings, null, 2)); ``` ⚠️ **Important**: Review exported recordings for sensitive data (API keys, tokens, passwords) before committing! @@ -151,21 +152,21 @@ writeFileSync( In your CI or test environment, use the recorded responses: ```javascript -import recordings from "../recordings/home.json"; +import recordings from '../recordings/home.json'; -test("renders homepage with mocked data", async ({ page }) => { +test('renders homepage with mocked data', async ({ page }) => { // Load recordings into mock server - await fetch("http://localhost:1080/mock/recordings", { - method: "POST", - headers: { "Content-Type": "application/json" }, + await fetch('http://localhost:1080/mock/recordings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - active: false, // Replay mode + active: false, // Replay mode recordings, }), }); - await page.goto("http://localhost:3000"); - await expect(page.getByText("Expected Content")).toBeVisible(); + await page.goto('http://localhost:3000'); + await expect(page.getByText('Expected Content')).toBeVisible(); }); ``` @@ -175,21 +176,21 @@ For simpler scenarios or testing edge cases, define mock responses manually: ```javascript // Mock a specific endpoint -await fetch("http://localhost:1080/mock/mock", { - method: "POST", - headers: { "Content-Type": "application/json" }, +await fetch('http://localhost:1080/mock/mock', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ request: { - match: "/api.example.com/users/.*", // Regex pattern - bodyMatch: ".*premium.*", // Optional: match request body + match: '/api.example.com/users/.*', // Regex pattern + bodyMatch: '.*premium.*', // Optional: match request body }, response: { status: 200, - contentType: "application/json", + contentType: 'application/json', body: { - id: "123", - name: "Test User", - tier: "premium", + id: '123', + name: 'Test User', + tier: 'premium', }, }, }), @@ -201,19 +202,16 @@ await fetch("http://localhost:1080/mock/mock", { Return different responses for consecutive calls: ```javascript -await fetch("http://localhost:1080/mock/mock", { - method: "POST", - headers: { "Content-Type": "application/json" }, +await fetch('http://localhost:1080/mock/mock', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ request: { - match: "/api.example.com/status", + match: '/api.example.com/status', }, response: { status: 200, - bodies: [ - { status: "processing" }, - { status: "completed" }, - ], + bodies: [{ status: 'processing' }, { status: 'completed' }], }, }), }); @@ -228,11 +226,11 @@ The built-in SMTP server captures emails for testing: // ... // Wait for and retrieve the email -const emailResponse = await fetch("http://localhost:1080/mock/mails/next"); +const emailResponse = await fetch('http://localhost:1080/mock/mails/next'); const email = await emailResponse.text(); // Assert email contents -expect(email).toContain("Welcome to our service"); +expect(email).toContain('Welcome to our service'); ``` ## API Reference @@ -244,6 +242,7 @@ expect(email).toContain("Welcome to our service"); Configure recording/replay mode. **Request Body:** + ```javascript { active: true, // true = record mode, false = replay mode @@ -267,6 +266,7 @@ Configure recording/replay mode. Retrieve all recorded API responses. **Response:** + ```javascript { "hash1": [{ @@ -292,6 +292,7 @@ Recalculate hashes for all recordings based on current hash configuration. Register a manual mock route. **Request Body:** + ```javascript { request: { @@ -323,15 +324,20 @@ List all registered mock routes. Get all captured requests. **Response:** + ```javascript [ { - method: "GET", - url: "/api.example.com/users/123", - headers: { /* ... */ }, - body: { /* ... */ } - } -] + method: 'GET', + url: '/api.example.com/users/123', + headers: { + /* ... */ + }, + body: { + /* ... */ + }, + }, +]; ``` #### `GET /mock/calls/next` @@ -372,13 +378,13 @@ Reset only calls and emails (keeps routes and recordings). Configure the server using environment variables: -| Variable | Default | Description | -|----------|---------|-------------| -| `MOCK_HTTP_PORT` or `PORT` | `1080` | HTTP server port | -| `MOCK_HTTPS_PORT` or `HTTPS_PORT` | - | HTTPS server port (optional) | -| `MOCK_SMTP_PORT` or `SMTP_PORT` | `25` | SMTP server port | -| `MOCK_HOST` or `HOST` | `0.0.0.0` | Server host | -| `MOCK_PATH` | `/mock` | Base path for API endpoints | +| Variable | Default | Description | +| --------------------------------- | --------- | ---------------------------- | +| `MOCK_HTTP_PORT` or `PORT` | `1080` | HTTP server port | +| `MOCK_HTTPS_PORT` or `HTTPS_PORT` | - | HTTPS server port (optional) | +| `MOCK_SMTP_PORT` or `SMTP_PORT` | `25` | SMTP server port | +| `MOCK_HOST` or `HOST` | `0.0.0.0` | Server host | +| `MOCK_PATH` | `/mock` | Base path for API endpoints | ### HTTPS Support @@ -395,18 +401,18 @@ MOCK_HTTPS_PORT=1443 npm start ## Complete Example: Playwright Test ```javascript -import { test, expect } from "@playwright/test"; -import recordings from "./recordings/homepage.json"; +import { test, expect } from '@playwright/test'; +import recordings from './recordings/homepage.json'; -test.describe("Homepage E2E", () => { +test.describe('Homepage E2E', () => { test.beforeEach(async () => { // Reset mock server - await fetch("http://localhost:1080/mock/reset", { method: "POST" }); - + await fetch('http://localhost:1080/mock/reset', { method: 'POST' }); + // Load recordings - await fetch("http://localhost:1080/mock/recordings", { - method: "POST", - headers: { "Content-Type": "application/json" }, + await fetch('http://localhost:1080/mock/recordings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ active: false, recordings, @@ -414,30 +420,30 @@ test.describe("Homepage E2E", () => { }); }); - test("displays content from mocked API", async ({ page }) => { - await page.goto("http://localhost:3000"); - + test('displays content from mocked API', async ({ page }) => { + await page.goto('http://localhost:3000'); + // Verify content rendered from mock data - await expect(page.getByText("Mock Server with API Recording")).toBeVisible(); - + await expect(page.getByText('Mock Server with API Recording')).toBeVisible(); + // Verify API was called - const calls = await fetch("http://localhost:1080/mock/calls").then(r => r.json()); + const calls = await fetch('http://localhost:1080/mock/calls').then((r) => r.json()); expect(calls.length).toBeGreaterThan(0); }); - test("handles API errors gracefully", async ({ page }) => { + test('handles API errors gracefully', async ({ page }) => { // Override with error response - await fetch("http://localhost:1080/mock/mock", { - method: "POST", - headers: { "Content-Type": "application/json" }, + await fetch('http://localhost:1080/mock/mock', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - request: { match: "/api.example.com/.*" }, - response: { status: 500, body: { error: "Internal Server Error" } }, + request: { match: '/api.example.com/.*' }, + response: { status: 500, body: { error: 'Internal Server Error' } }, }), }); - await page.goto("http://localhost:3000"); - await expect(page.getByText("Something went wrong")).toBeVisible(); + await page.goto('http://localhost:3000'); + await expect(page.getByText('Something went wrong')).toBeVisible(); }); }); ``` @@ -448,48 +454,46 @@ Create a script to record new API interactions: ```javascript // scripts/record-api.js -import { chromium } from "@playwright/test"; -import { writeFileSync } from "fs"; +import { chromium } from '@playwright/test'; +import { writeFileSync } from 'fs'; async function record() { // Start recording - await fetch("http://localhost:1080/mock/recordings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ + await fetch('http://localhost:1080/mock/recordings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ active: true, - deleteBodyAttributesForHash: ["timestamp", "_v"], - deleteHeadersForHash: ["cookie", "authorization"], + deleteBodyAttributesForHash: ['timestamp', '_v'], + deleteHeadersForHash: ['cookie', 'authorization'], }), }); // Run through your app const browser = await chromium.launch(); const page = await browser.newPage(); - - await page.goto("http://localhost:3000"); - await page.click("text=Get Started"); - await page.fill("#email", "test@example.com"); - await page.click("button[type=submit]"); - + + await page.goto('http://localhost:3000'); + await page.click('text=Get Started'); + await page.fill('#email', 'test@example.com'); + await page.click('button[type=submit]'); + await browser.close(); // Export recordings - const response = await fetch("http://localhost:1080/mock/recordings"); + const response = await fetch('http://localhost:1080/mock/recordings'); const recordings = await response.json(); - - writeFileSync( - "test/recordings/user-flow.json", - JSON.stringify(recordings, null, 2) - ); - - console.log("✅ Recordings saved to test/recordings/user-flow.json"); + + writeFileSync('test/recordings/user-flow.json', JSON.stringify(recordings, null, 2)); + + console.log('✅ Recordings saved to test/recordings/user-flow.json'); } record().catch(console.error); ``` Run it with: + ```bash node scripts/record-api.js ``` @@ -499,6 +503,7 @@ node scripts/record-api.js ### 1. Keep Recordings Small and Focused Record separate files for different test scenarios: + ``` test/recordings/ ├── homepage.json @@ -531,6 +536,7 @@ Always exclude sensitive information from recordings: ### 3. Version Your Recordings Commit recordings to version control alongside your tests: + ```bash git add test/recordings/ git commit -m "Update API recordings for new endpoint" @@ -542,7 +548,7 @@ Always reset the mock server to ensure test isolation: ```javascript test.beforeEach(async () => { - await fetch("http://localhost:1080/mock/reset", { method: "POST" }); + await fetch('http://localhost:1080/mock/reset', { method: 'POST' }); }); ``` @@ -551,15 +557,15 @@ test.beforeEach(async () => { Use `failedRequestsResponse` to provide fallbacks: ```javascript -await fetch("http://localhost:1080/mock/recordings", { - method: "POST", +await fetch('http://localhost:1080/mock/recordings', { + method: 'POST', body: JSON.stringify({ active: false, recordings, failedRequestsResponse: { - error: "Not found in recordings", - data: null - } + error: 'Not found in recordings', + data: null, + }, }), }); ``` @@ -589,7 +595,7 @@ When using HTTPS mode, you may need to trust self-signed certificates: ```javascript // In Playwright const context = await browser.newContext({ - ignoreHTTPSErrors: true + ignoreHTTPSErrors: true, }); ``` diff --git a/package-lock.json b/package-lock.json index 378d9ed..4398a9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,6 +124,7 @@ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -708,6 +709,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, From 7d899dd5ff3b110c2dd489f2bd2e0f68345395df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:28:26 +0000 Subject: [PATCH 4/5] Address code review feedback: fix title and npm installation instructions Co-authored-by: rawi96 <37732078+rawi96@users.noreply.github.com> --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 32d9f0d..7bc4248 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Smart Mock Server +# Mockserver [![npm version](https://badge.fury.io/js/@smartive%2Fmockserver.svg)](https://www.npmjs.com/package/@smartive/mockserver) [![Docker Pulls](https://img.shields.io/docker/pulls/smartive/mockserver)](https://hub.docker.com/r/smartive/mockserver) @@ -67,11 +67,15 @@ services: MOCK_SMTP_PORT: 25 ``` -### Using npm +### Using npm/npx ```bash -npm install -g @smartive/mockserver +# Run directly without installation npx @smartive/mockserver + +# Or install globally +npm install -g @smartive/mockserver +mockserver ``` ### From Source From b4ff9e6290b2de5a8b3bcd71c361d57080334bc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:56:58 +0000 Subject: [PATCH 5/5] Simplify README to focus on unique features (reduce from 635 to 149 lines) Co-authored-by: rawi96 <37732078+rawi96@users.noreply.github.com> --- README.md | 616 ++++++------------------------------------------------ 1 file changed, 65 insertions(+), 551 deletions(-) diff --git a/README.md b/README.md index 7bc4248..a350b10 100644 --- a/README.md +++ b/README.md @@ -3,207 +3,75 @@ [![npm version](https://badge.fury.io/js/@smartive%2Fmockserver.svg)](https://www.npmjs.com/package/@smartive/mockserver) [![Docker Pulls](https://img.shields.io/docker/pulls/smartive/mockserver)](https://hub.docker.com/r/smartive/mockserver) -A lightweight, powerful mock server with API recording and replay capabilities for E2E testing. Perfect for mocking server-side API calls in applications with Server-Side Rendering (SSR). +Mock server with **API recording and replay** for E2E testing. Works as a proxy that can record real API responses and replay them deterministically. -## Why This Mock Server? +## What makes it special? -Modern web frameworks like Next.js, Remix, and SvelteKit fetch data server-side for better SEO and performance. Traditional browser-based mocking tools can't intercept these server-side requests, making E2E testing challenging. - -### The Problem - -**Client-Side Rendering (CSR):** - -``` -Browser → External API ✅ Easy to mock with browser tools -``` - -**Server-Side Rendering (SSR):** - -``` -Browser → App Server → External API ❌ Browser tools can't intercept -``` - -### The Solution - -Instead of intercepting requests, let them flow naturally to a mock server: - -``` -Browser → App Server → Mock Server → External API (in record mode) -Browser → App Server → Mock Server (in replay mode) -``` - -## Key Features - -- 🎯 **Proxy Mode with Recording**: Record real API responses automatically -- 🔄 **Replay Mode**: Use recorded responses for deterministic testing -- 📝 **Manual Mocking**: Define custom mock responses via HTTP API -- 📧 **SMTP Server**: Built-in mail server for email testing -- 🔍 **Request Inspection**: Track all incoming requests and emails -- 🐳 **Docker Ready**: Available as a lightweight Docker image -- ⚡ **Simple & Fast**: ~350 lines of code, minimal dependencies +- **Record & Replay**: Record real API calls, replay them in tests +- **Proxy mode**: Routes requests like `http://localhost:1080/api.example.com/path` → `https://api.example.com/path` +- **SMTP server**: Built-in mail server for email testing +- **HTTP API**: Configure mocks, recordings, and inspect requests via REST endpoints ## Quick Start -### Using Docker (Recommended) - ```bash -docker run -p 1080:1080 smartive/mockserver -``` - -### Using Docker Compose - -```yaml -version: '3.3' -services: - mockserver: - image: smartive/mockserver - ports: - - '1080:1080' - - '25:25' # Optional: SMTP server - environment: - MOCK_PATH: /mock - MOCK_HOST: 0.0.0.0 - MOCK_HTTP_PORT: 1080 - MOCK_SMTP_PORT: 25 +docker run -p 1080:1080 -p 25:25 smartive/mockserver ``` -### Using npm/npx +Or with npm: ```bash -# Run directly without installation npx @smartive/mockserver - -# Or install globally -npm install -g @smartive/mockserver -mockserver -``` - -### From Source - -```bash -git clone https://github.com/smartive/mockserver.git -cd mockserver -npm install -npm start ``` -## Usage Patterns +## Usage -### Pattern 1: Record & Replay (Recommended) +### Record & Replay -This is the most powerful pattern - record real API responses and replay them deterministically. - -#### Step 1: Configure Your App - -Point your app's API base URL to the mock server. The mock server expects the original host as part of the URL path: +**1. Configure your app** to use the mockserver as proxy: ```bash # .env.test -STORYBLOK_API_BASE_URL="http://localhost:1080/api.storyblok.com" -``` - -This transforms requests from: - -``` -https://api.storyblok.com/v2/cdn/stories/home +API_BASE_URL="http://localhost:1080/api.example.com" ``` -to: - -``` -http://localhost:1080/api.storyblok.com/v2/cdn/stories/home -``` - -#### Step 2: Record Mode - -Enable recording to capture real API responses: +**2. Record** real API responses: ```javascript -// Enable recording mode await fetch('http://localhost:1080/mock/recordings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - active: true, - // Optional: Remove dynamic fields from hash calculation - deleteBodyAttributesForHash: ['timestamp', 'requestId'], - deleteHeadersForHash: ['authorization', 'x-request-id'], - }), + body: JSON.stringify({ active: true }), }); -// Run your E2E tests - all API calls will be recorded -await page.goto('http://localhost:3000'); -// ... perform your test actions ... +// Run your app - all API calls are proxied and recorded ``` -#### Step 3: Export Recordings - -After recording, export the captured responses: +**3. Export** recordings: ```javascript -const response = await fetch('http://localhost:1080/mock/recordings'); -const recordings = await response.json(); - -// Save to your repository -import { writeFileSync } from 'fs'; -writeFileSync('test/e2e/recordings/home.json', JSON.stringify(recordings, null, 2)); +const recordings = await fetch('http://localhost:1080/mock/recordings').then((r) => r.json()); +writeFileSync('recordings.json', JSON.stringify(recordings)); ``` -⚠️ **Important**: Review exported recordings for sensitive data (API keys, tokens, passwords) before committing! - -#### Step 4: Replay Mode - -In your CI or test environment, use the recorded responses: +**4. Replay** in tests: ```javascript -import recordings from '../recordings/home.json'; - -test('renders homepage with mocked data', async ({ page }) => { - // Load recordings into mock server - await fetch('http://localhost:1080/mock/recordings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - active: false, // Replay mode - recordings, - }), - }); - - await page.goto('http://localhost:3000'); - await expect(page.getByText('Expected Content')).toBeVisible(); -}); -``` - -### Pattern 2: Manual Mocking +import recordings from './recordings.json'; -For simpler scenarios or testing edge cases, define mock responses manually: - -```javascript -// Mock a specific endpoint -await fetch('http://localhost:1080/mock/mock', { +await fetch('http://localhost:1080/mock/recordings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - request: { - match: '/api.example.com/users/.*', // Regex pattern - bodyMatch: '.*premium.*', // Optional: match request body - }, - response: { - status: 200, - contentType: 'application/json', - body: { - id: '123', - name: 'Test User', - tier: 'premium', - }, - }, + active: false, + recordings, }), }); -``` -#### Multiple Responses +// All requests now use recorded responses +``` -Return different responses for consecutive calls: +### Manual Mocking ```javascript await fetch('http://localhost:1080/mock/mock', { @@ -211,425 +79,71 @@ await fetch('http://localhost:1080/mock/mock', { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ request: { - match: '/api.example.com/status', + match: '/api.example.com/users/.*', // Regex }, response: { status: 200, - bodies: [{ status: 'processing' }, { status: 'completed' }], + body: { id: '123', name: 'Test User' }, }, }), }); ``` -### Pattern 3: Email Testing - -The built-in SMTP server captures emails for testing: - -```javascript -// Send an email from your app -// ... - -// Wait for and retrieve the email -const emailResponse = await fetch('http://localhost:1080/mock/mails/next'); -const email = await emailResponse.text(); - -// Assert email contents -expect(email).toContain('Welcome to our service'); -``` - -## API Reference - -### Recording Endpoints - -#### `POST /mock/recordings` - -Configure recording/replay mode. - -**Request Body:** - -```javascript -{ - active: true, // true = record mode, false = replay mode - recordings: {}, // Recordings to load in replay mode - deleteBodyAttributesForHash: ["timestamp"], // Exclude from hash - deleteHeadersForHash: ["authorization"], // Exclude from hash - forwardHeadersForRoute: [ // Forward headers in proxy mode - { - route: "/api.example.com", - headers: { "X-API-Key": "secret" } - } - ], - failedRequestsResponse: {} // Response when no match found (optional) -} -``` - -**Response:** `204 No Content` - -#### `GET /mock/recordings` - -Retrieve all recorded API responses. - -**Response:** - -```javascript -{ - "hash1": [{ - request: { url: "...", method: "GET", headers: {...}, body: {} }, - body: { /* response body */ }, - status: 200, - durationMs: 150 - }], - "hash2": [...] -} -``` - -#### `POST /mock/recordings/rehash` - -Recalculate hashes for all recordings based on current hash configuration. - -**Response:** Updated recordings object - -### Mock Endpoints - -#### `POST /mock/mock` - -Register a manual mock route. - -**Request Body:** - -```javascript -{ - request: { - match: "/api\\.example\\.com/users/.*", // URL regex - bodyMatch: ".*premium.*" // Optional: request body regex - }, - response: { - status: 200, - contentType: "application/json", // Optional - body: { /* response data */ }, - // OR for multiple responses: - bodies: [{ /* first */ }, { /* second */ }] - } -} -``` - -**Response:** `204 No Content` - -#### `GET /mock/routes` - -List all registered mock routes. - -**Response:** Array of route configurations - -### Request Inspection - -#### `GET /mock/calls` - -Get all captured requests. - -**Response:** - -```javascript -[ - { - method: 'GET', - url: '/api.example.com/users/123', - headers: { - /* ... */ - }, - body: { - /* ... */ - }, - }, -]; -``` - -#### `GET /mock/calls/next` - -Wait for the next request (long-polling). - -**Response:** Single request object (when available) - ### Email Testing -#### `GET /mock/mails` - -Get all captured emails. - -**Response:** Array of raw email messages - -#### `GET /mock/mails/next` - -Wait for the next email (long-polling). - -**Response:** Single email message (when available) - -### Utility Endpoints - -#### `POST /mock/reset` - -Reset all state (routes, recordings, calls, emails). - -**Response:** `204 No Content` - -#### `POST /mock/reset/calls` - -Reset only calls and emails (keeps routes and recordings). - -**Response:** `204 No Content` - -## Configuration - -Configure the server using environment variables: - -| Variable | Default | Description | -| --------------------------------- | --------- | ---------------------------- | -| `MOCK_HTTP_PORT` or `PORT` | `1080` | HTTP server port | -| `MOCK_HTTPS_PORT` or `HTTPS_PORT` | - | HTTPS server port (optional) | -| `MOCK_SMTP_PORT` or `SMTP_PORT` | `25` | SMTP server port | -| `MOCK_HOST` or `HOST` | `0.0.0.0` | Server host | -| `MOCK_PATH` | `/mock` | Base path for API endpoints | - -### HTTPS Support - -To enable HTTPS, set the `MOCK_HTTPS_PORT` environment variable and provide SSL certificates: - -```bash -# Certificate files must be at: -# - cert/localhost.key -# - cert/localhost.crt - -MOCK_HTTPS_PORT=1443 npm start -``` - -## Complete Example: Playwright Test - -```javascript -import { test, expect } from '@playwright/test'; -import recordings from './recordings/homepage.json'; - -test.describe('Homepage E2E', () => { - test.beforeEach(async () => { - // Reset mock server - await fetch('http://localhost:1080/mock/reset', { method: 'POST' }); - - // Load recordings - await fetch('http://localhost:1080/mock/recordings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - active: false, - recordings, - }), - }); - }); - - test('displays content from mocked API', async ({ page }) => { - await page.goto('http://localhost:3000'); - - // Verify content rendered from mock data - await expect(page.getByText('Mock Server with API Recording')).toBeVisible(); - - // Verify API was called - const calls = await fetch('http://localhost:1080/mock/calls').then((r) => r.json()); - expect(calls.length).toBeGreaterThan(0); - }); - - test('handles API errors gracefully', async ({ page }) => { - // Override with error response - await fetch('http://localhost:1080/mock/mock', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - request: { match: '/api.example.com/.*' }, - response: { status: 500, body: { error: 'Internal Server Error' } }, - }), - }); - - await page.goto('http://localhost:3000'); - await expect(page.getByText('Something went wrong')).toBeVisible(); - }); -}); -``` - -## Recording Script Example - -Create a script to record new API interactions: - -```javascript -// scripts/record-api.js -import { chromium } from '@playwright/test'; -import { writeFileSync } from 'fs'; - -async function record() { - // Start recording - await fetch('http://localhost:1080/mock/recordings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - active: true, - deleteBodyAttributesForHash: ['timestamp', '_v'], - deleteHeadersForHash: ['cookie', 'authorization'], - }), - }); - - // Run through your app - const browser = await chromium.launch(); - const page = await browser.newPage(); - - await page.goto('http://localhost:3000'); - await page.click('text=Get Started'); - await page.fill('#email', 'test@example.com'); - await page.click('button[type=submit]'); - - await browser.close(); - - // Export recordings - const response = await fetch('http://localhost:1080/mock/recordings'); - const recordings = await response.json(); - - writeFileSync('test/recordings/user-flow.json', JSON.stringify(recordings, null, 2)); - - console.log('✅ Recordings saved to test/recordings/user-flow.json'); -} - -record().catch(console.error); -``` - -Run it with: - -```bash -node scripts/record-api.js -``` - -## Best Practices - -### 1. Keep Recordings Small and Focused - -Record separate files for different test scenarios: - -``` -test/recordings/ - ├── homepage.json - ├── login-flow.json - ├── checkout.json - └── admin-panel.json -``` - -### 2. Sanitize Sensitive Data - -Always exclude sensitive information from recordings: - ```javascript -{ - active: true, - deleteBodyAttributesForHash: [ - "password", - "token", - "apiKey", - "creditCard" - ], - deleteHeadersForHash: [ - "authorization", - "cookie", - "x-api-key" - ] -} -``` - -### 3. Version Your Recordings - -Commit recordings to version control alongside your tests: - -```bash -git add test/recordings/ -git commit -m "Update API recordings for new endpoint" +// Wait for next email +const email = await fetch('http://localhost:1080/mock/mails/next').then((r) => r.text()); +expect(email).toContain('Welcome'); ``` -### 4. Reset Between Tests +## API Endpoints -Always reset the mock server to ensure test isolation: +| Endpoint | Method | Description | +| ------------------------- | ------ | ------------------------------------ | +| `/mock/recordings` | POST | Configure recording/replay mode | +| `/mock/recordings` | GET | Get all recordings | +| `/mock/recordings/rehash` | POST | Recalculate recording hashes | +| `/mock/mock` | POST | Register a mock route | +| `/mock/routes` | GET | List all mock routes | +| `/mock/calls` | GET | Get all captured requests | +| `/mock/calls/next` | GET | Wait for next request (long-polling) | +| `/mock/mails` | GET | Get all captured emails | +| `/mock/mails/next` | GET | Wait for next email (long-polling) | +| `/mock/reset` | POST | Reset all state | +| `/mock/reset/calls` | POST | Reset calls and emails | -```javascript -test.beforeEach(async () => { - await fetch('http://localhost:1080/mock/reset', { method: 'POST' }); -}); -``` +## Configuration -### 5. Handle Missing Recordings Gracefully +| Variable | Default | Description | +| ----------------- | --------- | ------------- | +| `MOCK_HTTP_PORT` | `1080` | HTTP port | +| `MOCK_HTTPS_PORT` | - | HTTPS port | +| `MOCK_SMTP_PORT` | `25` | SMTP port | +| `MOCK_HOST` | `0.0.0.0` | Host | +| `MOCK_PATH` | `/mock` | API base path | -Use `failedRequestsResponse` to provide fallbacks: +## Advanced Recording Options ```javascript await fetch('http://localhost:1080/mock/recordings', { method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - active: false, - recordings, - failedRequestsResponse: { - error: 'Not found in recordings', - data: null, - }, + active: true, + deleteBodyAttributesForHash: ['timestamp'], // Ignore these fields in hash + deleteHeadersForHash: ['authorization'], // Ignore these headers in hash + forwardHeadersForRoute: [ + { + route: '/api.example.com', + headers: { 'X-API-Key': 'secret' }, + }, + ], + failedRequestsResponse: { error: 'Not found' }, // Fallback response }), }); ``` -## Troubleshooting - -### Recordings Not Matching - -If recordings aren't being replayed: - -1. Check the hash by examining the error message in server logs -2. Ensure `deleteBodyAttributesForHash` and `deleteHeadersForHash` exclude dynamic fields -3. Use `POST /mock/recordings/rehash` to recalculate hashes after changing configuration - -### Large Recording Files - -If recordings are too large: - -1. Split into multiple smaller files per test/feature -2. Remove unnecessary fields using `deleteBodyAttributesForHash` -3. Consider recording only critical paths, use manual mocks for edge cases - -### HTTPS Certificate Errors - -When using HTTPS mode, you may need to trust self-signed certificates: - -```javascript -// In Playwright -const context = await browser.newContext({ - ignoreHTTPSErrors: true, -}); -``` - -### Port Already in Use - -If port 1080 is occupied: - -```bash -# Use a different port -MOCK_HTTP_PORT=8080 npm start - -# Or find and kill the process -lsof -ti:1080 | xargs kill -9 -``` - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - ## License ISC - -## Links - -- [GitHub Repository](https://github.com/smartive/mockserver) -- [npm Package](https://www.npmjs.com/package/@smartive/mockserver) -- [Docker Hub](https://hub.docker.com/r/smartive/mockserver) -- [Issues](https://github.com/smartive/mockserver/issues) - ---- - -Built with ❤️ by [smartive](https://smartive.ch)