Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ Commands:
vonage conversations [command] Manage conversations
vonage jwt <command> Manage JWT tokens
vonage members [command] Manage applications
vonage mock <api> Launch a mock server for Vonage APIs using Prism
vonage numbers [command] Manage numbers
vonage users [command] Manage users
```
Expand All @@ -212,6 +213,70 @@ Use the `--help` flag with a command to get more information on how to use:
vonage apps --help
```

## Mock Server

The Vonage CLI includes a built-in mock server feature that allows you to quickly set up a local mock server for Vonage APIs using Prism. This is particularly useful for development and testing.

### Prerequisites

**No additional installation required!** The CLI ships with a bundled version of [Prism](https://stoplight.io/prism) to ensure compatibility and avoid version conflicts with any globally installed versions.

### Usage

To start a mock server for an API:

```shell
vonage mock sms
```

This will:
1. Download the latest OpenAPI specification for the SMS API from the Vonage Developer Portal
2. Start a Prism mock server on `localhost:4010` using the bundled Prism CLI
3. Display available endpoints based on the API specification

### Options

- `--port`: Specify a custom port (default: 4010)
- `--host`: Specify a custom host (default: localhost)
- `--download-only`: Only download the OpenAPI spec without starting the server
- `--latest`: Force re-download of the OpenAPI spec even if it already exists

### Examples

```shell
# Start SMS API mock server on default port 4010
vonage mock sms

# Start SMS API mock server on port 8080
vonage mock sms --port 8080

# Only download the OpenAPI spec without starting the server
vonage mock sms --download-only

# Force re-download the latest SMS API spec and start the server
vonage mock sms --latest

# Re-download the latest spec without starting the server
vonage mock sms --download-only --latest
```

### Available APIs

Currently supported APIs for mocking:
- `sms` - SMS API

More APIs will be added in future releases.

### Downloaded Specifications

OpenAPI specifications are downloaded to `~/.vonage/mock/` (alongside your CLI configuration) and can be reused with other tools or mock servers. This keeps your project directories clean while providing persistent access to the API specifications.

**Smart Caching**: The CLI will automatically cache downloaded specs and reuse them on subsequent runs to improve performance. Use the `--latest` flag to force re-download when you need the most recent API specification.

### Version Isolation

The CLI uses its own bundled version of Prism (`@stoplight/prism-cli@5.14.2`) to ensure consistent behavior and avoid conflicts with any globally installed versions you might have.

## Need Help?

If you encounter any issues or need help, please join our [community Slack channel](/community/slack)
283 changes: 283 additions & 0 deletions __tests__/commands/mock.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
const { handler } = require('../../src/commands/mock');

// Mock dependencies
jest.mock('node-fetch');
jest.mock('fs', () => ({
promises: {
mkdir: jest.fn(),
writeFile: jest.fn(),
},
existsSync: jest.fn(),
}));
jest.mock('child_process');
jest.mock('../../src/ux/spinner', () => ({
spinner: jest.fn(() => ({
stop: jest.fn(),
fail: jest.fn(),
})),
}));
jest.mock('../../src/ux/cursor', () => ({
hideCursor: jest.fn(),
resetCursor: jest.fn(),
}));
jest.mock('../../src/ux/input', () => ({
inputFromTTY: jest.fn(),
}));

const fetch = require('node-fetch');
const fs = require('fs').promises;
const { existsSync } = require('fs');
const { spawn } = require('child_process');

describe('mock command', () => {
beforeEach(() => {
jest.clearAllMocks();
console.info = jest.fn();
console.log = jest.fn();
console.error = jest.fn();
existsSync.mockReturnValue(false); // Default to file not existing
});

describe('download functionality', () => {
it('should download SMS API spec successfully', async () => {
const mockSpec = {
openapi: '3.0.0',
info: { title: 'SMS API', version: '1.0.0' },
paths: {},
};

fetch.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue(mockSpec),
});

fs.mkdir.mockResolvedValue();
fs.writeFile.mockResolvedValue();

const argv = {
api: 'sms',
port: 4010,
host: 'localhost',
downloadOnly: true,
};

await handler(argv);

expect(fetch).toHaveBeenCalledWith(
'https://developer.vonage.com/api/v1/developer/api/file/sms?format=json&vendorId=vonage',
);
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining('sms-spec.json'),
JSON.stringify(mockSpec, null, 2),
);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Downloaded SMS API specification'),
);
});

it('should handle download failure gracefully', async () => {
fetch.mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
});

fs.mkdir.mockResolvedValue();

const argv = {
api: 'sms',
port: 4010,
host: 'localhost',
downloadOnly: true,
};

await expect(handler(argv)).rejects.toThrow('Failed to download API specification');

expect(console.error).toHaveBeenCalledWith(
'Failed to download API specification:',
'Failed to download spec: 404 Not Found',
);
});

it('should handle download failure', async () => {
fetch.mockRejectedValue(new Error('Network error'));

const argv = {
api: 'sms',
port: 4010,
host: 'localhost',
downloadOnly: true,
};

await expect(handler(argv)).rejects.toThrow('Failed to download API specification');

expect(console.error).toHaveBeenCalledWith(
'Failed to download API specification:',
'Network error',
);
});
});

describe('directory creation', () => {
it('should handle directory creation failure', async () => {
fs.mkdir.mockRejectedValue(new Error('Permission denied'));

const argv = {
api: 'sms',
port: 4010,
host: 'localhost',
downloadOnly: true,
};

await expect(handler(argv)).rejects.toThrow('Failed to create mock directory');

expect(console.error).toHaveBeenCalledWith(
'Failed to create mock directory:',
'Permission denied',
);
});
});

describe('caching functionality', () => {
const mockSpec = {
openapi: '3.0.0',
info: { title: 'SMS API', version: '1.0.0' },
paths: {},
};

beforeEach(() => {
fetch.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue(mockSpec),
});
fs.mkdir.mockResolvedValue();
fs.writeFile.mockResolvedValue();
});

it('should use cached spec when file exists and --latest is not used', async () => {
existsSync.mockReturnValue(true); // File exists

const argv = {
api: 'sms',
port: 4010,
host: 'localhost',
downloadOnly: true,
latest: false,
};

await handler(argv);

expect(fetch).not.toHaveBeenCalled();
expect(fs.writeFile).not.toHaveBeenCalled();
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Using cached SMS API specification'),
);
expect(console.log).toHaveBeenCalledWith(
'Spec already exists. Use --latest to re-download the latest version.',
);
});

it('should re-download spec when --latest flag is used', async () => {
existsSync.mockReturnValue(true); // File exists

const argv = {
api: 'sms',
port: 4010,
host: 'localhost',
downloadOnly: true,
latest: true,
};

await handler(argv);

expect(fetch).toHaveBeenCalledWith(
'https://developer.vonage.com/api/v1/developer/api/file/sms?format=json&vendorId=vonage',
);
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining('sms-spec.json'),
JSON.stringify(mockSpec, null, 2),
);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Re-downloaded SMS API specification'),
);
});

it('should download spec when file does not exist', async () => {
existsSync.mockReturnValue(false); // File does not exist

const argv = {
api: 'sms',
port: 4010,
host: 'localhost',
downloadOnly: true,
latest: false,
};

await handler(argv);

expect(fetch).toHaveBeenCalledWith(
'https://developer.vonage.com/api/v1/developer/api/file/sms?format=json&vendorId=vonage',
);
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining('sms-spec.json'),
JSON.stringify(mockSpec, null, 2),
);
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Downloaded SMS API specification'),
);
});
});

describe('Prism server startup', () => {
beforeEach(() => {
// Mock successful download
const mockSpec = { openapi: '3.0.0', info: { title: 'SMS API' } };
fetch.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue(mockSpec),
});
fs.mkdir.mockResolvedValue();
fs.writeFile.mockResolvedValue();

// Mock spawn to return a process-like object
spawn.mockReturnValue({
on: jest.fn(),
kill: jest.fn(),
killed: false,
});
});

it('should start Prism server with bundled CLI', async () => {
const mockProcess = {
on: jest.fn(),
kill: jest.fn(),
killed: false,
};
spawn.mockReturnValue(mockProcess);

// Mock inputFromTTY to simulate immediate quit
const { inputFromTTY } = require('../../src/ux/input');
inputFromTTY.mockRejectedValue('Shutdown');

const argv = {
api: 'sms',
port: 4010,
host: 'localhost',
downloadOnly: false,
};

await handler(argv);

// Check that spawn was called with the correct bundled prism path
expect(spawn).toHaveBeenCalledWith(
expect.stringContaining('node_modules/.bin/prism'),
['mock', expect.stringContaining('sms-spec.json'), '--port', '4010', '--host', 'localhost'],
expect.any(Object),
);

expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Using bundled Prism CLI'),
);
});
});
});
Loading
Loading