Skip to content

Commit

Permalink
Replace fetch-mock with fetch-mock-jest
Browse files Browse the repository at this point in the history
The `.sandbox()` interface we use was split off from fetch-mock into this package.
  • Loading branch information
nicknovitski committed Sep 12, 2024
1 parent cd0a36e commit 8f47c3e
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 28 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"@types/promise-retry": "^1.1.6",
"eslint": "^8.57.0",
"eslint-config-universe": "^12.0.0",
"fetch-mock": "^9.11.0",
"fetch-mock-jest": "^1.5.1",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"ts-jest": "~29.2.5",
Expand Down
2 changes: 1 addition & 1 deletion src/__mocks__/node-fetch.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = require('fetch-mock').sandbox();
module.exports = require('fetch-mock-jest').sandbox();
53 changes: 28 additions & 25 deletions src/__tests__/ExpoClient-test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { afterEach, beforeEach, describe, test, expect } from '@jest/globals';
import sandbox from 'fetch-mock-jest';
import fetch from 'node-fetch';
import assert from 'node:assert';

import ExpoClient, { ExpoPushMessage } from '../ExpoClient';
import { getReceiptsApiUrl, sendApiUrl } from '../ExpoClientValues';

const fetchMock: ReturnType<typeof sandbox> = fetch; // see src/__mocks__/

afterEach(() => {
(fetch as any).reset();
fetchMock.reset();
});

describe('sending push notification messages', () => {
Expand All @@ -15,13 +18,13 @@ describe('sending push notification messages', () => {
{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' },
{ status: 'ok', id: 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY' },
];
(fetch as any).mock(sendApiUrl, { data: mockTickets });
fetchMock.mock(sendApiUrl, { data: mockTickets });

const client = new ExpoClient();
const tickets = await client.sendPushNotificationsAsync([{ to: 'a' }, { to: 'b' }]);
expect(tickets).toEqual(mockTickets);

const [, options] = (fetch as any).lastCall(sendApiUrl);
const [, options] = fetchMock.lastCall(sendApiUrl);
expect(options.headers.get('accept')).toContain('application/json');
expect(options.headers.get('accept-encoding')).toContain('gzip');
expect(options.headers.get('content-type')).toContain('application/json');
Expand All @@ -34,13 +37,13 @@ describe('sending push notification messages', () => {
{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' },
{ status: 'ok', id: 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY' },
];
(fetch as any).mock(sendApiUrl, { data: mockTickets });
fetchMock.mock(sendApiUrl, { data: mockTickets });

const client = new ExpoClient({ accessToken: 'foobar' });
const tickets = await client.sendPushNotificationsAsync([{ to: 'a' }, { to: 'b' }]);
expect(tickets).toEqual(mockTickets);

const [, options] = (fetch as any).lastCall(sendApiUrl);
const [, options] = fetchMock.lastCall(sendApiUrl);
expect(options.headers.get('accept')).toContain('application/json');
expect(options.headers.get('accept-encoding')).toContain('gzip');
expect(options.headers.get('content-type')).toContain('application/json');
Expand All @@ -50,32 +53,32 @@ describe('sending push notification messages', () => {

describe('the useFcmV1 option', () => {
beforeEach(() => {
(fetch as any).any({ data: [{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' }] });
fetchMock.any({ data: [{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' }] });
});

test('sends requests to the Expo API server without the useFcmV1 parameter', async () => {
const client = new ExpoClient();
await client.sendPushNotificationsAsync([{ to: 'a' }]);
expect((fetch as any).called(sendApiUrl)).toBe(true);
expect(fetchMock.called(sendApiUrl)).toBe(true);
});

test('sends requests to the Expo API server with useFcmV1=true', async () => {
const client = new ExpoClient({ useFcmV1: true });
await client.sendPushNotificationsAsync([{ to: 'a' }]);
// Request should omit useFcmV1 if set to true
expect((fetch as any).called(`${sendApiUrl}`)).toBe(true);
expect(fetchMock.called(`${sendApiUrl}`)).toBe(true);
});

test('sends requests to the Expo API server with useFcmV1=false', async () => {
const client = new ExpoClient({ useFcmV1: false });
await client.sendPushNotificationsAsync([{ to: 'a' }]);
expect((fetch as any).called(`${sendApiUrl}?useFcmV1=false`)).toBe(true);
expect(fetchMock.called(`${sendApiUrl}?useFcmV1=false`)).toBe(true);
});
});

test('compresses request bodies over 1 KiB', async () => {
const mockTickets = [{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' }];
(fetch as any).mock(sendApiUrl, { data: mockTickets });
fetchMock.mock(sendApiUrl, { data: mockTickets });

const client = new ExpoClient();

Expand All @@ -85,7 +88,7 @@ describe('sending push notification messages', () => {
expect(tickets).toEqual(mockTickets);

// Ensure the request body was compressed
const [, options] = (fetch as any).lastCall(sendApiUrl);
const [, options] = fetchMock.lastCall(sendApiUrl);
expect(options.body.length).toBeLessThan(JSON.stringify(messages).length);
expect(options.headers.get('content-encoding')).toContain('gzip');
});
Expand All @@ -95,7 +98,7 @@ describe('sending push notification messages', () => {
{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' },
{ status: 'ok', id: 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY' },
];
(fetch as any).mock(sendApiUrl, { data: mockTickets });
fetchMock.mock(sendApiUrl, { data: mockTickets });

const client = new ExpoClient();
await expect(client.sendPushNotificationsAsync([{ to: 'a' }])).rejects.toThrow(
Expand All @@ -108,7 +111,7 @@ describe('sending push notification messages', () => {
});

test('handles 200 HTTP responses with well-formed API errors', async () => {
(fetch as any).mock(sendApiUrl, {
fetchMock.mock(sendApiUrl, {
status: 200,
errors: [{ code: 'TEST_API_ERROR', message: `This is a test error` }],
});
Expand All @@ -120,7 +123,7 @@ describe('sending push notification messages', () => {
});

test('handles 200 HTTP responses with malformed JSON', async () => {
(fetch as any).mock(sendApiUrl, {
fetchMock.mock(sendApiUrl, {
status: 200,
body: '<!DOCTYPE html><body>Not JSON</body>',
});
Expand All @@ -132,7 +135,7 @@ describe('sending push notification messages', () => {
});

test('handles non-200 HTTP responses with well-formed API errors', async () => {
(fetch as any).mock(sendApiUrl, {
fetchMock.mock(sendApiUrl, {
status: 400,
body: {
errors: [{ code: 'TEST_API_ERROR', message: `This is a test error` }],
Expand All @@ -146,7 +149,7 @@ describe('sending push notification messages', () => {
});

test('handles non-200 HTTP responses with arbitrary JSON', async () => {
(fetch as any).mock(sendApiUrl, {
fetchMock.mock(sendApiUrl, {
status: 400,
body: { clowntown: true },
});
Expand All @@ -158,7 +161,7 @@ describe('sending push notification messages', () => {
});

test('handles non-200 HTTP responses with arbitrary text', async () => {
(fetch as any).mock(sendApiUrl, {
fetchMock.mock(sendApiUrl, {
status: 400,
body: '<!DOCTYPE html><body>Not JSON</body>',
});
Expand All @@ -170,7 +173,7 @@ describe('sending push notification messages', () => {
});

test('handles well-formed API responses with multiple errors and extra details', async () => {
(fetch as any).mock(sendApiUrl, {
fetchMock.mock(sendApiUrl, {
status: 400,
body: {
errors: [
Expand Down Expand Up @@ -202,7 +205,7 @@ describe('sending push notification messages', () => {
});

test('handles 429 Too Many Requests by applying exponential backoff', async () => {
(fetch as any).mock(
fetchMock.mock(
sendApiUrl,
{
status: 429,
Expand All @@ -220,15 +223,15 @@ describe('sending push notification messages', () => {
await rejection.toThrow(`Rate limit exceeded`);
await rejection.toMatchObject({ code: 'RATE_LIMIT_ERROR' });

expect((fetch as any).done()).toBeTruthy();
expect(fetchMock.done()).toBeTruthy();
});

test('handles 429 Too Many Requests and succeeds when a retry succeeds', async () => {
const mockTickets = [
{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' },
{ status: 'ok', id: 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY' },
];
(fetch as any)
fetchMock
.mock(
sendApiUrl,
{
Expand All @@ -246,7 +249,7 @@ describe('sending push notification messages', () => {
mockTickets,
);

expect((fetch as any).done()).toBeTruthy();
expect(fetchMock.done()).toBeTruthy();
});
});

Expand All @@ -256,7 +259,7 @@ describe('retrieving push notification receipts', () => {
'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX': { status: 'ok' },
'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY': { status: 'ok' },
};
(fetch as any).mock(getReceiptsApiUrl, { data: mockReceipts });
fetchMock.mock(getReceiptsApiUrl, { data: mockReceipts });

const client = new ExpoClient();
const receipts = await client.getPushNotificationReceiptsAsync([
Expand All @@ -265,15 +268,15 @@ describe('retrieving push notification receipts', () => {
]);
expect(receipts).toEqual(mockReceipts);

const [, options] = (fetch as any).lastCall(getReceiptsApiUrl);
const [, options] = fetchMock.lastCall(getReceiptsApiUrl);
expect(options.headers.get('accept')).toContain('application/json');
expect(options.headers.get('accept-encoding')).toContain('gzip');
expect(options.headers.get('content-type')).toContain('application/json');
});

test('throws an error if the response is not a map', async () => {
const mockReceipts = [{ status: 'ok' }];
(fetch as any).mock(getReceiptsApiUrl, { data: mockReceipts });
fetchMock.mock(getReceiptsApiUrl, { data: mockReceipts });

const client = new ExpoClient();
const rejection = expect(
Expand Down
16 changes: 15 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2743,7 +2743,7 @@ __metadata:
"@types/promise-retry": "npm:^1.1.6"
eslint: "npm:^8.57.0"
eslint-config-universe: "npm:^12.0.0"
fetch-mock: "npm:^9.11.0"
fetch-mock-jest: "npm:^1.5.1"
jest: "npm:^29.7.0"
node-fetch: "npm:^2.6.0"
prettier: "npm:^3.2.5"
Expand Down Expand Up @@ -2820,6 +2820,20 @@ __metadata:
languageName: node
linkType: hard

"fetch-mock-jest@npm:^1.5.1":
version: 1.5.1
resolution: "fetch-mock-jest@npm:1.5.1"
dependencies:
fetch-mock: "npm:^9.11.0"
peerDependencies:
node-fetch: "*"
peerDependenciesMeta:
node-fetch:
optional: true
checksum: 10c0/d1cd2a1e139868567280f2e8145ed4f55acb186b52fb94397ce06d2887ff8f41e9484bada2f842f9efa2e51207b6f6d61b1ec02d68e72758f53e74e3e5351129
languageName: node
linkType: hard

"fetch-mock@npm:^9.11.0":
version: 9.11.0
resolution: "fetch-mock@npm:9.11.0"
Expand Down

0 comments on commit 8f47c3e

Please sign in to comment.