From 95d7b4f707b9fad01e16187752d298fe941a7bb8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Lauk" Date: Mon, 16 Aug 2021 09:16:03 +0200 Subject: [PATCH] feat: detect api version closes #21 --- src/api/util.spec.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++ src/api/util.ts | 40 ++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/api/util.spec.ts create mode 100644 src/api/util.ts diff --git a/src/api/util.spec.ts b/src/api/util.spec.ts new file mode 100644 index 0000000..80a55f2 --- /dev/null +++ b/src/api/util.spec.ts @@ -0,0 +1,63 @@ +import { detectVersion } from './util' +import { fetchWithTimeout } from '../http' +jest.mock('../http') +const mockedFetch = fetchWithTimeout as jest.MockedFunction< + typeof fetchWithTimeout +> + +const DEFAULT_URL = 'http://localhost:8080' + +describe('module api/util', () => { + beforeEach(() => { + mockedFetch.mockClear() + }) + + describe('detectVersion', () => { + it('returns version string from version field in response', async () => { + const version = '1.2.3-rc4' + mockedFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve({ version }), + } as Response) + const actualVersion = await detectVersion(DEFAULT_URL) + expect(actualVersion).toBe(version) + }) + + it('returns undefined on HTTP 404', async () => { + mockedFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'not found', + } as Response) + const actualVersion = await detectVersion(DEFAULT_URL) + expect(actualVersion).toBe(undefined) + }) + + it('throws on other HTTP errors', async () => { + const fakeResponses = [ + { ok: false, status: 400, statusText: 'bad request' }, + { ok: false, status: 403, statusText: 'forbidden' }, + { ok: false, status: 405, statusText: 'method not allowed' }, + { ok: false, status: 406, statusText: 'not acceptable' }, + { ok: false, status: 408, statusText: 'request timeout' }, + { ok: false, status: 500, statusText: 'internal server error' }, + { ok: false, status: 501, statusText: 'not implemented' }, + { ok: false, status: 502, statusText: 'bad gateway' }, + { ok: false, status: 503, statusText: 'service unavailable' }, + ] + for (const r of fakeResponses) { + mockedFetch.mockResolvedValueOnce(r as Response) + await expect(detectVersion(DEFAULT_URL)).rejects.toThrow() + } + }) + + it('throws if URL is falsy', async () => { + const urls = [null, undefined, ''] + expect.assertions(urls.length) + for (const u of urls) { + await expect(detectVersion(u as string)).rejects.toThrow() + } + }) + }) +}) diff --git a/src/api/util.ts b/src/api/util.ts new file mode 100644 index 0000000..a5b2423 --- /dev/null +++ b/src/api/util.ts @@ -0,0 +1,40 @@ +import * as decoders from 'decoders' +import { fetchWithTimeout } from '../http' + +type VersionInfo = { + version: string +} +const versionGuard = decoders.guard( + decoders.object({ version: decoders.string }) +) + +/** + * Contact the API provider at base URL `url` and try to read + * version information from endpoint `/version` with a GET request. + * + * @param url base url of the API provider + * @returns A Promise that resolves to either the version string, or undefined if the version could not be read. + */ +export const detectVersion = async ( + url: RequestInfo +): Promise => { + if (!url) throw new Error('need url') + const resp = await fetchWithTimeout(url + '/version', { + method: 'GET', + headers: { Accept: 'application/json' }, + }) + if (resp.status === 404) { + return undefined + } + if (!resp.ok) { + throw new Error( + `could not detect version: ${resp.status} ${resp.statusText}` + ) + } + try { + const versionInfo: VersionInfo = versionGuard(await resp.json()) + return versionInfo.version + } catch (e) { + throw new Error('received malformed version info') + } +}