Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
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
58 changes: 51 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# data-mocks

# modified by aiden for websocket support.

# will hopefully be deleted soon when merged in to upstream

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also write usage

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you fix this up?

[![GitHub license](https://img.shields.io/github/license/ovotech/data-mocks.svg)](https://github.com/grug/data-mocks)
![npm](https://img.shields.io/npm/dm/data-mocks.svg)

<img src="https://i.imgur.com/gEG3io2.jpg" height="250">

Library (written in TypeScript) to mock REST and GraphQL requests
Library (written in TypeScript) to mock REST, GraphQ, and Websocket requests

<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->

Expand Down Expand Up @@ -93,13 +97,13 @@ import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

async function setupMocks() {
const { injectMocks, extractScenarioFromLocation } = await import(
'data-mocks'
);
// You could just define your mocks inline if you didn't want to import them.
const { getMocks } = await import('./path/to/your/mock/definitions');
const { injectMocks, extractScenarioFromLocation } = await import(
'data-mocks'
);
// You could just define your mocks inline if you didn't want to import them.
const { getMocks } = await import('./path/to/your/mock/definitions');

injectMocks(getMocks(), extractScenarioFromLocation(window.location));
injectMocks(getMocks(), extractScenarioFromLocation(window.location));
}

async function main() {
Expand Down Expand Up @@ -303,6 +307,38 @@ const Component = () => {
};
```

### Basic Websocket server mock injection

To mock a WebSocket server you should provide a function which takes a single `Server` (provided by `mock-socket`) as a paremeter. On this mock server parameter you can set your callbacks as normal. Please not that due to limitations in the underlying websocket mocking library, the url _must_ be supplied as a string, not a regular expression

```ts
const wsMock: WebSocketServerMock = s => {
return s.on('connection', socket => {
socket.on('message', _ => {
socket.send('hello world')
}
});
});
};

const mocks = {
default: [
{
url: 'ws://localhost' //notice this is NOT a regular expression
method: 'WEBSOCKET',
server: wsMock,
}
]
};

injectMocks(mocks, extractScenarioFromLocation(window.location));


const socket = new WebSocket('ws://localhost')
socket.send('hello')

```

## Exported types

### Scenarios
Expand Down Expand Up @@ -331,6 +367,14 @@ const Component = () => {
| method | string | ✅ | Must be 'GRAPHQL' to specify that this is a GraphQL mock. |
| operations | Array\<Operation\> | ✅ | Array of GraphQL operations for this request. |

### WebSocketMock

| Property | Type | Required | Description |
| -------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| url | string | ✅ | Regular expression that matches part of the URL. |
| method | string | ✅ | Must be 'WEBSOCKET' to specify that this is a Websocket mock. |
| server | function | ✅ | a function which takes a server as a parameter. Here you will set up the functionality of the server to mock. |

### Mock

Union type of [`HttpMock`](#HttpMock) and [`GraphQLMock`](#GraphQLMock).
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom'
testEnvironment: 'jsdom',
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"fetch-mock": "^9.4.0",
"mock-socket": "^9.0.3",
"query-string": "^5.1.1",
"xhr-mock": "^2.5.1"
},
Expand Down
125 changes: 119 additions & 6 deletions src/mocks.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,63 @@
import 'isomorphic-fetch';
import axios from 'axios';
import fetchMock from 'fetch-mock';
import 'isomorphic-fetch';
import XHRMock, { proxy } from 'xhr-mock';
import {
injectMocks,
extractScenarioFromLocation,
injectMocks,
reduceAllMocksForScenario,
} from './mocks';
import { HttpMethod, Scenarios, MockConfig } from './types';
import XHRMock, { proxy } from 'xhr-mock';
import fetchMock from 'fetch-mock';
import { HttpMethod, MockConfig, Scenarios } from './types';

describe('data-mocks', () => {
beforeEach(() => {
fetchMock.resetHistory();
});

describe('Websockets', () => {
const testURL = 'ws://localhost/foo';
it('Spawns a working websocket server', async () => {
const onMessage = jest.fn();
const onConnect = jest.fn();
const scenarios: Scenarios = {
default: [
{
url: testURL,
method: 'WEBSOCKET',
server: (s) => {
s.on('connection', (socket) => {
onConnect();
socket.on('message', (req) => {
onMessage();
socket.send(req.toString());
s.close();
});
});
},
},
],
};
injectMocks(scenarios, 'default');

const socket = new WebSocket(testURL);
let res;
socket.addEventListener('message', (data) => {
res = data.data;
socket.close();
});

await awaitSocket(socket, WebSocket.OPEN);
expect(onConnect).toBeCalled();

const message = 'hello world';
socket.send(message);
await awaitSocket(socket, WebSocket.CLOSED);
expect(onMessage).toBeCalled();

expect(res).toEqual(message);
});
});

describe('REST', () => {
describe('HTTP methods', () => {
const httpMethods: HttpMethod[] = [
Expand Down Expand Up @@ -50,7 +94,6 @@ describe('data-mocks', () => {
const xhrSpy = jest.spyOn(XHRMock, httpMethod.toLowerCase() as any);

injectMocks(scenarios, 'default');

expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy.mock.calls[0][0]).toEqual(/foo/);
expect(fetchSpy.mock.calls[1][0]).toEqual(/bar/);
Expand Down Expand Up @@ -282,6 +325,8 @@ describe('data-mocks', () => {
});

describe('Scenarios', () => {
const websocketServerFn = jest.fn();
const anotherServerFn = jest.fn();
const scenarios: Scenarios = {
default: [
{
Expand All @@ -299,6 +344,22 @@ describe('data-mocks', () => {
responseHeaders: { token: 'bar' },
},
{ url: /bar/, method: 'POST', response: {}, responseCode: 200 },
{
url: /graphql/,
method: 'GRAPHQL',
operations: [
{
operationName: 'Query',
type: 'query',
response: { data: { test: 'data' } },
},
],
},
{
url: 'ws://localhost',
method: 'WEBSOCKET',
server: websocketServerFn,
},
],
someScenario: [
{
Expand All @@ -308,6 +369,18 @@ describe('data-mocks', () => {
responseCode: 401,
},
{ url: /baz/, method: 'POST', response: {}, responseCode: 200 },
{
url: /graphql/,
method: 'GRAPHQL',
operations: [
{
operationName: 'Query',
type: 'query',
response: { data: { test: 'different data' } },
},
],
},
{ url: 'ws://localhost', method: 'WEBSOCKET', server: anotherServerFn },
],
};

Expand Down Expand Up @@ -340,6 +413,22 @@ describe('data-mocks', () => {
responseHeaders: { token: 'bar' },
},
{ url: /bar/, method: 'POST', response: {}, responseCode: 200 },
{
url: /graphql/,
method: 'GRAPHQL',
operations: [
{
operationName: 'Query',
type: 'query',
response: { data: { test: 'data' } },
},
],
},
{
url: 'ws://localhost',
method: 'WEBSOCKET',
server: websocketServerFn,
},
]);
});

Expand Down Expand Up @@ -367,6 +456,18 @@ describe('data-mocks', () => {
responseCode: 200,
},
{ url: /baz/, method: 'POST', response: {}, responseCode: 200 },
{
url: /graphql/,
method: 'GRAPHQL',
operations: [
{
operationName: 'Query',
type: 'query',
response: { data: { test: 'different data' } },
},
],
},
{ url: 'ws://localhost', method: 'WEBSOCKET', server: anotherServerFn },
]);
});

Expand Down Expand Up @@ -536,3 +637,15 @@ describe('data-mocks', () => {
});
});
});

const awaitSocket = (socket, state) => {
return new Promise(function (resolve) {
setTimeout(function () {
if (socket.readyState === state) {
resolve(true);
} else {
awaitSocket(socket, state).then(resolve);
}
}, 1000);
});
};
50 changes: 38 additions & 12 deletions src/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import fetchMock from 'fetch-mock';
import XHRMock, { delay as xhrMockDelay, proxy } from 'xhr-mock';
import { parse } from 'query-string';
import { Server as MockServer } from 'mock-socket';
import {
Scenarios,
MockConfig,
Mock,
HttpMock,
GraphQLMock,
Operation,
WebSocketMock,
} from './types';

/**
Expand Down Expand Up @@ -43,14 +45,11 @@ export const injectMocks = (
if (!mocks || mocks.length === 0) {
throw new Error('Unable to instantiate mocks');
}

const restMocks = mocks.filter((m) => m.method !== 'GRAPHQL') as HttpMock[];
const graphQLMocks = mocks.filter(
(m) => m.method === 'GRAPHQL'
) as GraphQLMock[];
const { restMocks, graphQLMocks, webSocketMocks } = getMocksByType(mocks);

restMocks.forEach(handleRestMock);
graphQLMocks.forEach(handleGraphQLMock);
webSocketMocks.forEach(handleWebsocketMock);

if (config?.allowXHRPassthrough) {
XHRMock.use(proxy);
Expand Down Expand Up @@ -78,12 +77,19 @@ export const reduceAllMocksForScenario = (

const mocks = defaultMocks.concat(scenarioMocks);

const initialHttpMocks = mocks.filter(
({ method }) => method !== 'GRAPHQL'
) as HttpMock[];
const initialGraphQlMocks = mocks.filter(
({ method }) => method === 'GRAPHQL'
) as GraphQLMock[];
const {
restMocks: initialHttpMocks,
graphQLMocks: initialGraphQlMocks,
webSocketMocks: initialWebsocketMocks,
} = getMocksByType(mocks);

const websocketMocksByUrl = initialWebsocketMocks.reduce<
Record<string, WebSocketMock>
>((result, mock) => {
const { url } = mock;
result[url] = mock;
return result;
}, {});

const httpMocksByUrlAndMethod = initialHttpMocks.reduce<
Record<string, HttpMock>
Expand Down Expand Up @@ -130,7 +136,9 @@ export const reduceAllMocksForScenario = (
}
) as GraphQLMock[];

return (httpMocks as any).concat(graphQlMocks);
const websocketMocks = Object.values(websocketMocksByUrl);

return [...httpMocks, ...graphQlMocks, ...websocketMocks];
};

/**
Expand Down Expand Up @@ -281,8 +289,26 @@ function handleGraphQLMock({ url, operations }: GraphQLMock) {
});
}

const handleWebsocketMock = ({ url, server }: WebSocketMock) => {
server(new MockServer(url));
};

/**
* Adds delay (in ms) before resolving a promise.
*/
const addDelay = (delay: number) =>
new Promise((res) => setTimeout(res, delay));

const getMocksByType = (mocks: Mock[]) => {
const restMocks = mocks.filter(
(m) => !['GRAPHQL', 'WEBSOCKET'].includes(m.method)
) as HttpMock[];
const graphQLMocks = mocks.filter(
(m) => m.method === 'GRAPHQL'
) as GraphQLMock[];

const webSocketMocks = mocks.filter(
(m) => m.method === 'WEBSOCKET'
) as WebSocketMock[];
return { restMocks, graphQLMocks, webSocketMocks };
};
Loading