diff --git a/package-lock.json b/package-lock.json index 0cd8496..14c6c3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@typescript-eslint/parser": "^4.32.0", "eslint": "^7.32.0", "jest": "^27.0.6", - "jest-fetch-mock": "^2.1.2", + "jest-fetch-mock": "3.0.3", "jest-localstorage-mock": "^2.4.18", "prettier": "^2.5.1", "rollup": "^1.20.3", @@ -2159,13 +2159,12 @@ } }, "node_modules/cross-fetch": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.5.tgz", - "integrity": "sha512-xqYAhQb4NhCJSRym03dwxpP1bYXpK3y7UN83Bo2WFi3x1Zmzn0SL/6xGoPr+gpt4WmNrgCCX3HPysvOwFOW36w==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", "dev": true, "dependencies": { - "node-fetch": "2.6.1", - "whatwg-fetch": "2.0.4" + "node-fetch": "2.6.7" } }, "node_modules/cross-spawn": { @@ -3585,13 +3584,13 @@ } }, "node_modules/jest-fetch-mock": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-2.1.2.tgz", - "integrity": "sha512-tcSR4Lh2bWLe1+0w/IwvNxeDocMI/6yIA2bijZ0fyWxC4kQ18lckQ1n7Yd40NKuisGmcGBRFPandRXrW/ti/Bw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", "dev": true, "dependencies": { - "cross-fetch": "^2.2.2", - "promise-polyfill": "^7.1.1" + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" } }, "node_modules/jest-get-type": { @@ -4392,12 +4391,45 @@ "dev": true }, "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/node-int64": { @@ -4672,9 +4704,9 @@ } }, "node_modules/promise-polyfill": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-7.1.2.tgz", - "integrity": "sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", "dev": true }, "node_modules/prompts": { @@ -5855,12 +5887,6 @@ "iconv-lite": "0.4.24" } }, - "node_modules/whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==", - "dev": true - }, "node_modules/whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", @@ -7710,13 +7736,12 @@ } }, "cross-fetch": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.5.tgz", - "integrity": "sha512-xqYAhQb4NhCJSRym03dwxpP1bYXpK3y7UN83Bo2WFi3x1Zmzn0SL/6xGoPr+gpt4WmNrgCCX3HPysvOwFOW36w==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", "dev": true, "requires": { - "node-fetch": "2.6.1", - "whatwg-fetch": "2.0.4" + "node-fetch": "^2.6.7" } }, "cross-spawn": { @@ -8811,13 +8836,13 @@ } }, "jest-fetch-mock": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-2.1.2.tgz", - "integrity": "sha512-tcSR4Lh2bWLe1+0w/IwvNxeDocMI/6yIA2bijZ0fyWxC4kQ18lckQ1n7Yd40NKuisGmcGBRFPandRXrW/ti/Bw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", "dev": true, "requires": { - "cross-fetch": "^2.2.2", - "promise-polyfill": "^7.1.1" + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" } }, "jest-get-type": { @@ -9433,10 +9458,37 @@ "dev": true }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-int64": { "version": "0.4.0", @@ -9637,9 +9689,9 @@ "dev": true }, "promise-polyfill": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-7.1.2.tgz", - "integrity": "sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", + "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==", "dev": true }, "prompts": { @@ -10523,12 +10575,6 @@ "iconv-lite": "0.4.24" } }, - "whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==", - "dev": true - }, "whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", diff --git a/package.json b/package.json index 8c2384c..4588693 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@typescript-eslint/parser": "^4.32.0", "eslint": "^7.32.0", "jest": "^27.0.6", - "jest-fetch-mock": "^2.1.2", + "jest-fetch-mock": "3.0.3", "jest-localstorage-mock": "^2.4.18", "prettier": "^2.5.1", "rollup": "^1.20.3", diff --git a/src/index.test.ts b/src/index.test.ts index 2deb258..d468913 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -3,6 +3,7 @@ import 'jest-localstorage-mock'; import * as data from '../tests/example-data.json'; import IStorageProvider from './storage-provider'; import { EVENTS, IConfig, IMutableContext, UnleashClient } from './index'; +import { getTypeSafeRequest, getTypeSafeRequestUrl } from '../tests/util'; jest.useFakeTimers(); @@ -594,8 +595,15 @@ test('Should include etag in second request', async () => { jest.advanceTimersByTime(1001); - expect(fetchMock.mock.calls[0][1].headers['If-None-Match']).toEqual(''); - expect(fetchMock.mock.calls[1][1].headers['If-None-Match']).toEqual(etag); + const firstRequest = getTypeSafeRequest(fetchMock, 0); + const secondRequest = getTypeSafeRequest(fetchMock, 1); + + expect(firstRequest.headers).toMatchObject({ + 'If-None-Match': '', + }); + expect(secondRequest.headers).toMatchObject({ + 'If-None-Match': etag, + }); }); test('Should add clientKey as Authorization header', async () => { @@ -613,9 +621,11 @@ test('Should add clientKey as Authorization header', async () => { jest.advanceTimersByTime(1001); - expect(fetchMock.mock.calls[0][1].headers.Authorization).toEqual( - 'some123key' - ); + const request = getTypeSafeRequest(fetchMock); + + expect(request.headers).toMatchObject({ + Authorization: 'some123key', + }); }); test('Should require appName', () => { @@ -712,7 +722,7 @@ test('Should include context fields on request', async () => { jest.advanceTimersByTime(1001); - const url = new URL(fetchMock.mock.calls[0][0]); + const url = new URL(getTypeSafeRequestUrl(fetchMock)); expect(url.searchParams.get('userId')).toEqual('123'); expect(url.searchParams.get('sessionId')).toEqual('456'); @@ -752,13 +762,12 @@ test('Should note include context fields with "null" value', async () => { jest.advanceTimersByTime(1001); - const url = new URL(fetchMock.mock.calls[0][0]); + const url = new URL(getTypeSafeRequestUrl(fetchMock)); expect(url.searchParams.has('userId')).toBe(false); expect(url.searchParams.has('remoteAddress')).toBe(false); expect(url.searchParams.has('sessionId')).toBe(true); expect(url.searchParams.get('sessionId')).toBe('0'); - }); test('Should update context fields on request', async () => { @@ -787,7 +796,7 @@ test('Should update context fields on request', async () => { jest.advanceTimersByTime(1001); - const url = new URL(fetchMock.mock.calls[0][0]); + const url = new URL(getTypeSafeRequestUrl(fetchMock)); expect(url.searchParams.get('userId')).toEqual('123'); expect(url.searchParams.get('sessionId')).toEqual('456'); @@ -818,7 +827,7 @@ test('Should not add property fields when properties is an empty object', async jest.advanceTimersByTime(1001); - const url = new URL(fetchMock.mock.calls[0][0]); + const url = new URL(getTypeSafeRequestUrl(fetchMock)); // console.log(url.toString(), url.searchParams.toString(), url.searchParams.get('properties')); @@ -842,7 +851,7 @@ test('Should use default environment', async () => { jest.advanceTimersByTime(1001); - const url = new URL(fetchMock.mock.calls[0][0]); + const url = new URL(getTypeSafeRequestUrl(fetchMock)); expect(url.searchParams.get('environment')).toEqual('default'); }); @@ -959,8 +968,10 @@ test('Should pass under custom header clientKey', async () => { const client = new UnleashClient(config); client.on(EVENTS.UPDATE, () => { + const request = getTypeSafeRequest(fetchMock, 0); + expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][1].headers).toMatchObject({ + expect(request.headers).toMatchObject({ NotAuthorization: '12', }); client.stop(); @@ -1006,28 +1017,30 @@ test('Should call isEnabled event when impressionData is true', (done) => { }); }); -test('Should pass custom headers', async() => { +test('Should pass custom headers', async () => { fetchMock.mockResponses( [JSON.stringify(data), { status: 200 }], [JSON.stringify(data), { status: 200 }] ); - const config: IConfig = { - url: 'http://localhost/test', - clientKey: 'extrakey', - appName: 'web', - customHeaders: { - 'customheader1': 'header1val', - 'customheader2': 'header2val' - } - }; - const client = new UnleashClient(config); - await client.start(); - - jest.advanceTimersByTime(1001); - - expect(fetchMock.mock.calls[0][1].headers.customheader2).toEqual( - 'header2val' - ); + const config: IConfig = { + url: 'http://localhost/test', + clientKey: 'extrakey', + appName: 'web', + customHeaders: { + customheader1: 'header1val', + customheader2: 'header2val', + }, + }; + const client = new UnleashClient(config); + await client.start(); + + jest.advanceTimersByTime(1001); + + const request = getTypeSafeRequest(fetchMock); + + expect(request.headers).toMatchObject({ + customheader2: 'header2val', + }); }); test('Should call getVariant event when impressionData is true', (done) => { diff --git a/src/metrics.test.ts b/src/metrics.test.ts index 17362b8..dadd723 100644 --- a/src/metrics.test.ts +++ b/src/metrics.test.ts @@ -1,5 +1,6 @@ import { FetchMock } from 'jest-fetch-mock'; import Metrics from './metrics'; +import { getTypeSafeRequest, parseRequestBodyWithType } from '../tests/util'; jest.useFakeTimers(); @@ -47,12 +48,16 @@ test('should send metrics', async () => { await metrics.sendMetrics(); expect(fetchMock.mock.calls.length).toEqual(1); - const content = JSON.parse(fetchMock.mock.calls[0][1].body); - expect(content.bucket.toggles.foo.yes).toEqual(2); - expect(content.bucket.toggles.foo.no).toEqual(1); - expect(content.bucket.toggles.bar.yes).toEqual(0); - expect(content.bucket.toggles.bar.no).toEqual(1); + /** Parse request and get its body with casted type */ + const request = getTypeSafeRequest(fetchMock); + const body = + parseRequestBodyWithType<{ bucket: Metrics['bucket'] }>(request); + + expect(body.bucket.toggles.foo.yes).toEqual(2); + expect(body.bucket.toggles.foo.no).toEqual(1); + expect(body.bucket.toggles.bar.yes).toEqual(0); + expect(body.bucket.toggles.bar.no).toEqual(1); }); test('should send metrics under custom header', async () => { @@ -67,11 +72,14 @@ test('should send metrics under custom header', async () => { }); metrics.count('foo', true); - await metrics.sendMetrics(); + const requestBody = getTypeSafeRequest(fetchMock); + expect(fetchMock.mock.calls.length).toEqual(1); - expect(fetchMock.mock.calls[0][1].headers).toMatchObject({ 'NotAuthorization': '123' }); + expect(requestBody.headers).toMatchObject({ + NotAuthorization: '123', + }); }); test('Should send initial metrics after 2 seconds', () => { diff --git a/tests/util.ts b/tests/util.ts new file mode 100644 index 0000000..08c5170 --- /dev/null +++ b/tests/util.ts @@ -0,0 +1,44 @@ +import type { FetchMock } from 'jest-fetch-mock'; + +/** + * Extract request body from given `FetchMock` object. + * @param fetchedMock - mocked fetch body to get the request body from + * @param callIndex - index of call in given `fetcheMock` + */ +function getTypeSafeRequest( + fetchedMock: FetchMock, + callIndex: number | undefined = 0 +): RequestInit { + const mockedCall = fetchedMock.mock.calls[callIndex]; + const [_, mockedReq] = mockedCall; + const typeSafeRequest = mockedReq ?? {}; + + return typeSafeRequest; +} + +/** + * Extract request url from given `FetchMock` object. + * @param fetchedMock - mocked fetch body to get the request url from + * @param callIndex - index of call in given `fetcheMock` + */ +function getTypeSafeRequestUrl( + fetchedMock: FetchMock, + callIndex: number | undefined = 0 +): string { + const mockedCall = fetchedMock.mock.calls[callIndex]; + const [url] = mockedCall; + + return url as string; +} + +/** + * parses given `RequestInit` with `JSON.parse()` and return + * its body with passed type. + */ +function parseRequestBodyWithType(requestBody: RequestInit): T { + const body = JSON.parse(`${requestBody.body}`) as T; + + return body; +} + +export { getTypeSafeRequest, getTypeSafeRequestUrl, parseRequestBodyWithType };