diff --git a/packages-exp/auth-exp/karma.conf.js b/packages-exp/auth-exp/karma.conf.js index fe4ace4252f..6e8b47df149 100644 --- a/packages-exp/auth-exp/karma.conf.js +++ b/packages-exp/auth-exp/karma.conf.js @@ -17,7 +17,7 @@ const karmaBase = require('../../config/karma.base'); -const files = ['src/**/*.test.ts']; +const files = ['src/**/*.test.ts', 'test/**/*.test.ts']; module.exports = function(config) { const karmaConfig = Object.assign({}, karmaBase, { diff --git a/packages-exp/auth-exp/package.json b/packages-exp/auth-exp/package.json index ea17f40c598..73fb1a354ed 100644 --- a/packages-exp/auth-exp/package.json +++ b/packages-exp/auth-exp/package.json @@ -19,6 +19,7 @@ "dev": "rollup -c -w", "test": "yarn type-check && run-p lint test:browser test:node", "test:browser": "karma start --single-run", + "test:browser:debug": "karma start", "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha src/**/*.test.* --opts ../../config/mocha.node.opts", "type-check": "tsc -p . --noEmit", "prepare": "yarn build" diff --git a/packages-exp/auth-exp/test/mock_fetch.test.ts b/packages-exp/auth-exp/test/mock_fetch.test.ts new file mode 100644 index 00000000000..db88a57bed3 --- /dev/null +++ b/packages-exp/auth-exp/test/mock_fetch.test.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as mockFetch from './mock_fetch'; + +async function fetchJson(path: string, req?: object): Promise { + const body = req + ? { + body: JSON.stringify(req) + } + : {}; + const request: RequestInit = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + referrerPolicy: 'no-referrer', + ...body + }; + + const response = await fetch(path, request); + return response.json(); +} + +describe('mock fetch utility', () => { + beforeEach(mockFetch.setUp); + afterEach(mockFetch.tearDown); + + describe('matched requests', () => { + it('returns the correct object for multiple routes', async () => { + mockFetch.mock('/a', { a: 1 }); + mockFetch.mock('/b', { b: 2 }); + + expect(await fetchJson('/a')).to.eql({ a: 1 }); + expect(await fetchJson('/b')).to.eql({ b: 2 }); + }); + + it('passes through the status of the mock', async () => { + mockFetch.mock('/not-ok', {}, 500); + expect((await fetch('/not-ok')).status).to.equal(500); + }); + + it('records calls to the mock', async () => { + const someRequest = { + sentence: + 'Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo' + }; + + const mock = mockFetch.mock('/word', {}); + await fetchJson('/word', someRequest); + await fetchJson('/word', { a: 'b' }); + await fetch('/word'); + + expect(mock.calls.length).to.equal(3); + expect(mock.calls[0].request).to.eql(someRequest); + expect(mock.calls[1].request).to.eql({ a: 'b' }); + expect(mock.calls[2].request).to.equal(undefined); + }); + }); + + describe('route rejection', () => { + it('if the route is not in the map', () => { + mockFetch.mock('/test', {}); + expect(() => fetch('/not-test')).to.throw( + 'Unknown route being requested: /not-test' + ); + }); + + it('if call is not a string', () => { + mockFetch.mock('/blah', {}); + expect(() => fetch(new Request({} as any))).to.throw( + 'URL passed to fetch was not a string' + ); + }); + }); +}); + +describe('mock fetch utility (no setUp/tearDown)', () => { + it('errors if mock attempted without setup', () => { + expect(() => mockFetch.mock('/test', {})).to.throw( + 'Mock fetch is not set up' + ); + }); + + it('routes do not carry to next run', async () => { + mockFetch.setUp(); + mockFetch.mock('/test', { first: 'first' }); + expect(await fetchJson('/test')).to.eql({ first: 'first' }); + mockFetch.tearDown(); + mockFetch.setUp(); + expect(() => fetch('/test')).to.throw( + 'Unknown route being requested: /test' + ); + mockFetch.tearDown(); + }); +}); diff --git a/packages-exp/auth-exp/test/mock_fetch.ts b/packages-exp/auth-exp/test/mock_fetch.ts new file mode 100644 index 00000000000..1645e8a2453 --- /dev/null +++ b/packages-exp/auth-exp/test/mock_fetch.ts @@ -0,0 +1,82 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stub, SinonStub } from 'sinon'; + +export interface Call { + request?: object; +} + +export interface Route { + response: object; + status: number; + calls: Call[]; +} + +let fetchStub: SinonStub | null = null; +let routes = new Map(); + +// Using a constant rather than a function to enforce the type matches fetch() +const fakeFetch: typeof fetch = (input: RequestInfo, request?: RequestInit) => { + if (typeof input !== 'string') { + throw new Error('URL passed to fetch was not a string'); + } + + if (!routes.has(input)) { + throw new Error(`Unknown route being requested: ${input}`); + } + + // Bang-assertion is fine since we check for routes.has() above + const { response, status, calls } = routes.get(input)!; + + calls.push({ + request: request?.body ? JSON.parse(request.body as string) : undefined + }); + + const blob = new Blob([JSON.stringify(response)]); + return Promise.resolve( + new Response(blob, { + status + }) + ); +}; + +export function setUp(): void { + fetchStub = stub(self, 'fetch'); + fetchStub.callsFake(fakeFetch); +} + +export function mock(url: string, response: object, status = 200): Route { + if (!fetchStub) { + throw new Error('Mock fetch is not set up. Call setUp() first'); + } + + const route: Route = { + response, + status, + calls: [] + }; + + routes.set(url, route); + return route; +} + +export function tearDown(): void { + fetchStub?.restore(); + fetchStub = null; + routes = new Map(); +}