From ba684ff05b00f620635e4685b88efca725fe4910 Mon Sep 17 00:00:00 2001 From: Sara Bee <855595+doeg@users.noreply.github.com> Date: Sat, 30 Jan 2021 11:27:16 -0500 Subject: [PATCH] Add vtadmin-web build flag for configuring fetch credentials Signed-off-by: Sara Bee <855595+doeg@users.noreply.github.com> --- web/vtadmin/src/api/http.test.ts | 93 +++++++++++++++++++++++++++--- web/vtadmin/src/api/http.ts | 18 +++++- web/vtadmin/src/react-app-env.d.ts | 5 ++ 3 files changed, 107 insertions(+), 9 deletions(-) diff --git a/web/vtadmin/src/api/http.test.ts b/web/vtadmin/src/api/http.test.ts index cfd319156a6..cf314605d4b 100644 --- a/web/vtadmin/src/api/http.test.ts +++ b/web/vtadmin/src/api/http.test.ts @@ -39,21 +39,45 @@ import { HTTP_RESPONSE_NOT_OK_ERROR, MALFORMED_HTTP_RESPONSE_ERROR } from './htt // means our fake is more robust than it would be otherwise. Since we are using // the exact same protos in our fake as in our real vtadmin-api server, we're guaranteed // to have type parity. -process.env.REACT_APP_VTADMIN_API_ADDRESS = ''; const server = setupServer(); +// mockServerJson configures an HttpOkResponse containing the given `json` +// for all requests made against the given `endpoint`. const mockServerJson = (endpoint: string, json: object) => { server.use(rest.get(endpoint, (req, res, ctx) => res(ctx.json(json)))); }; -// Enable API mocking before tests. -beforeAll(() => server.listen()); +// Since vtadmin uses process.env variables quite a bit, we need to +// do a bit of a dance to clear them out between test runs. +const ORIGINAL_PROCESS_ENV = process.env; +const TEST_PROCESS_ENV = { + ...process.env, + REACT_APP_VTADMIN_API_ADDRESS: '', +}; + +beforeAll(() => { + process.env = { ...TEST_PROCESS_ENV }; + + // Enable API mocking before tests. + server.listen(); +}); -// Reset any runtime request handlers we may add during the tests. -afterEach(() => server.resetHandlers()); +afterEach(() => { + // Reset the process.env to clear out any changes made in the tests. + process.env = { ...TEST_PROCESS_ENV }; -// Disable API mocking after the tests are done. -afterAll(() => server.close()); + jest.restoreAllMocks(); + + // Reset any runtime request handlers we may add during the tests. + server.resetHandlers(); +}); + +afterAll(() => { + process.env = { ...ORIGINAL_PROCESS_ENV }; + + // Disable API mocking after the tests are done. + server.close(); +}); describe('api/http', () => { describe('vtfetch', () => { @@ -105,6 +129,61 @@ describe('api/http', () => { /* eslint-enable jest/no-conditional-expect */ } }); + + describe('credentials', () => { + it('uses the REACT_APP_FETCH_CREDENTIALS env variable if specified', async () => { + process.env.REACT_APP_FETCH_CREDENTIALS = 'include'; + + jest.spyOn(global, 'fetch'); + + const endpoint = `/api/tablets`; + const response = { ok: true, result: null }; + mockServerJson(endpoint, response); + + await api.vtfetch(endpoint); + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith(endpoint, { credentials: 'include' }); + + jest.restoreAllMocks(); + }); + + it('uses the fetch default `credentials` property by default', async () => { + jest.spyOn(global, 'fetch'); + + const endpoint = `/api/tablets`; + const response = { ok: true, result: null }; + mockServerJson(endpoint, response); + + await api.vtfetch(endpoint); + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith(endpoint, { credentials: undefined }); + + jest.restoreAllMocks(); + }); + + it('throws an error if an invalid value used for `credentials`', async () => { + (process as any).env.REACT_APP_FETCH_CREDENTIALS = 'nope'; + + jest.spyOn(global, 'fetch'); + + const endpoint = `/api/tablets`; + const response = { ok: true, result: null }; + mockServerJson(endpoint, response); + + try { + await api.vtfetch(endpoint); + } catch (e) { + /* eslint-disable jest/no-conditional-expect */ + expect(e.message).toEqual( + 'Invalid fetch credentials property: nope. Must be undefined or one of omit, same-origin, include' + ); + expect(global.fetch).toHaveBeenCalledTimes(0); + /* eslint-enable jest/no-conditional-expect */ + } + + jest.restoreAllMocks(); + }); + }); }); describe('fetchTablets', () => { diff --git a/web/vtadmin/src/api/http.ts b/web/vtadmin/src/api/http.ts index dceb9ae3e98..93ca6a2505c 100644 --- a/web/vtadmin/src/api/http.ts +++ b/web/vtadmin/src/api/http.ts @@ -58,8 +58,12 @@ class HttpResponseNotOkError extends Error { // Note that this only validates the HttpResponse envelope; it does not // do any type checking or validation on the result. export const vtfetch = async (endpoint: string): Promise => { - const url = `${process.env.REACT_APP_VTADMIN_API_ADDRESS}${endpoint}`; - const response = await fetch(url); + const { REACT_APP_VTADMIN_API_ADDRESS } = process.env; + + const url = `${REACT_APP_VTADMIN_API_ADDRESS}${endpoint}`; + const opts = vtfetchOpts(); + + const response = await global.fetch(url, opts); const json = await response.json(); if (!('ok' in json)) throw new MalformedHttpResponseError('invalid http envelope', json); @@ -67,6 +71,16 @@ export const vtfetch = async (endpoint: string): Promise => { return json as HttpResponse; }; +export const vtfetchOpts = (): RequestInit => { + const credentials = process.env.REACT_APP_FETCH_CREDENTIALS; + if (credentials && credentials !== 'omit' && credentials !== 'same-origin' && credentials !== 'include') { + throw Error( + `Invalid fetch credentials property: ${credentials}. Must be undefined or one of omit, same-origin, include` + ); + } + return { credentials }; +}; + export const fetchTablets = async () => { const endpoint = '/api/tablets'; const res = await vtfetch(endpoint); diff --git a/web/vtadmin/src/react-app-env.d.ts b/web/vtadmin/src/react-app-env.d.ts index 56282a3b44f..6ac8d6380de 100644 --- a/web/vtadmin/src/react-app-env.d.ts +++ b/web/vtadmin/src/react-app-env.d.ts @@ -7,6 +7,11 @@ declare namespace NodeJS { // Required. The full address of vtadmin-api's HTTP interface. // Example: "http://127.0.0.1:12345" REACT_APP_VTADMIN_API_ADDRESS: string; + + // Optional. Configures the `credentials` property for fetch requests. + // made against vtadmin-api. If unspecified, uses fetch defaults. + // See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included + REACT_APP_FETCH_CREDENTIALS?: RequestCredentials; } }