Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
93 changes: 86 additions & 7 deletions web/vtadmin/src/api/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
18 changes: 16 additions & 2 deletions web/vtadmin/src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,29 @@ 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<HttpResponse> => {
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);

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);
Expand Down
5 changes: 5 additions & 0 deletions web/vtadmin/src/react-app-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down