UNPKG

@openmrs/esm-api

Version:

The javascript module for interacting with the OpenMRS API

356 lines (313 loc) • 10.5 kB
import { isObservable } from 'rxjs'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getConfig } from '@openmrs/esm-config'; import { navigate } from '@openmrs/esm-navigation'; import { openmrsFetch, openmrsObservableFetch } from './openmrs-fetch'; vi.mock('@openmrs/esm-navigation', () => ({ clearHistory: vi.fn(), navigate: vi.fn(), })); const mockGetConfig = vi.mocked(getConfig); const mockNavigate = vi.mocked(navigate); describe('openmrsFetch', () => { beforeEach(() => { mockGetConfig.mockReturnValue( Promise.resolve({ redirectAuthFailure: { enabled: true, url: '${openmrsSpaBase}/login', errors: [401], resolvePromise: false, }, }), ); window.openmrsBase = '/openmrs'; window.getOpenmrsSpaBase = () => '/openmrs/spa/'; window.fetch = vi.fn(); Object.defineProperty(window, 'location', { writable: true, value: { assign: vi.fn() }, }); }); afterEach(() => { // @ts-expect-error Not normally deletable delete window.openmrsBase; // @ts-expect-error Not normally deletable delete window.getOpenmrsSpaBase; }); it(`throws an error if you don't pass in a url string`, () => { // @ts-expect-error expect(() => openmrsFetch()).toThrow(/first argument/); // @ts-expect-error expect(() => openmrsFetch({})).toThrow(/first argument/); }); it('throws an error if you pass in an invalid fetchInit object', () => { // @ts-expect-error expect(() => openmrsFetch('/session', 'invalid second arg')).toThrow(/second argument/); // @ts-expect-error expect(() => openmrsFetch('/session', 123)).toThrow(/second argument/); }); it('throws an Error if there is no openmrsBase', () => { // @ts-expect-error delete window.openmrsBase; expect(() => openmrsFetch('/session')).toThrow(/openmrsBase/); }); it('calls window.fetch with the correct arguments for a basic GET request', () => { // @ts-expect-error window.fetch.mockReturnValue(new Promise(() => {})); openmrsFetch('/ws/rest/v1/session'); expect(window.fetch).toHaveBeenCalledWith('/openmrs/ws/rest/v1/session', { headers: { Accept: 'application/json', 'Disable-WWW-Authenticate': 'true', }, }); }); it('calls window.fetch correctly for requests that have a request body', () => { // @ts-expect-error window.fetch.mockReturnValue(new Promise(() => {})); const requestBody = { some: 'json' }; openmrsFetch('/ws/rest/v1/session', { method: 'POST', body: requestBody, }); expect(window.fetch).toHaveBeenCalledWith('/openmrs/ws/rest/v1/session', { headers: { Accept: 'application/json', 'Disable-WWW-Authenticate': 'true', }, body: JSON.stringify(requestBody), method: 'POST', }); }); it('allows you to specify your own Accept request header', () => { // @ts-expect-error mockReturnValue only exists on the mock, not on the raw type window.fetch.mockReturnValue(new Promise(() => {})); openmrsFetch('/ws/rest/v1/session', { headers: { Accept: 'application/xml', }, }); expect(window.fetch).toHaveBeenCalledWith('/openmrs/ws/rest/v1/session', { headers: { Accept: 'application/xml', 'Disable-WWW-Authenticate': 'true', }, }); }); it('allows you to specify no Accept request header to be sent', () => { // @ts-expect-error window.fetch.mockReturnValue(new Promise(() => {})); openmrsFetch('/ws/rest/v1/session', { headers: { // specifically null on purpose Accept: null, }, }); expect(window.fetch).toHaveBeenCalledWith('/openmrs/ws/rest/v1/session', { headers: { 'Disable-WWW-Authenticate': 'true', }, }); }); it('returns a promise that resolves with a json object when the request succeeds', async () => { // @ts-expect-error window.fetch.mockReturnValue( Promise.resolve({ ok: true, status: 200, clone: () => ({ text: () => Promise.resolve('{ "value": "hi" }'), }), }), ); const response = await openmrsFetch('/ws/rest/v1/session'); expect(response.status).toBe(200); expect(response.data).toEqual({ value: 'hi' }); }); it('returns a promise that resolves with null when the request succeeds with HTTP 204', async () => { // @ts-expect-error window.fetch.mockReturnValue( Promise.resolve({ ok: true, status: 204, json: () => Promise.reject(Error("No json for HTTP 204's!!")), }), ); const response = await openmrsFetch('/ws/rest/v1/session'); expect(response.status).toBe(204); expect(response.data).toEqual(null); }); it('gives you an amazing error when the server responds with a 500 that has json', async () => { // @ts-expect-error window.fetch.mockReturnValue( Promise.resolve({ ok: false, status: 500, statusText: 'Internal Server Error', clone: () => ({ text: () => Promise.resolve( JSON.stringify({ error: 'The server is dead', }), ), }), }), ); try { await openmrsFetch('/ws/rest/v1/session'); fail("Promise shouldn't resolve when server responds with 500"); } catch (err) { expect(err.message).toMatch(/Server responded with 500 \(Internal Server Error\)/); expect(err.message).toMatch(/\/ws\/rest\/v1\/session/); expect(err.responseBody).toEqual({ error: 'The server is dead' }); expect(err.response.status).toBe(500); } }); it("gives you an amazing error when the server responds with a 400 that doesn't have json", async () => { // @ts-expect-error window.fetch.mockReturnValue( Promise.resolve({ ok: false, status: 400, statusText: 'You goofed up', clone: () => ({ text: () => Promise.resolve('a string response body'), }), }), ); try { await openmrsFetch('/ws/rest/v1/session'); fail("Promise shouldn't resolve when server responds with 400"); } catch (err) { expect(err.message).toMatch(/Server responded with 400 \(You goofed up\)/); expect(err.message).toMatch(/\/ws\/rest\/v1\/session/); expect(err.responseBody).toEqual('a string response body'); expect(err.response.status).toBe(400); } }); it('redirects to the Location header URL when a 401 response contains a Location header (auth-module challenge)', async () => { mockGetConfig.mockResolvedValueOnce({ redirectAuthFailure: { enabled: true, url: '', errors: [401], resolvePromise: true, }, }); // @ts-expect-error window.fetch.mockReturnValue( Promise.resolve({ ok: false, status: 401, statusText: 'Unauthorized', headers: { has: (name: string) => name.toLowerCase() === 'location', get: (name: string) => (name.toLowerCase() === 'location' ? '/module/authentication/login.form' : null), }, text: () => Promise.resolve(''), }), ); await openmrsFetch('/ws/rest/v1/session'); expect(mockNavigate.mock.calls[0][0]).toStrictEqual({ to: '/module/authentication/login.form', }); }); it('redirects to the default login URL when a 401 response has no Location header (genuine auth failure)', async () => { mockGetConfig.mockResolvedValueOnce({ redirectAuthFailure: { enabled: true, url: '', errors: [401], resolvePromise: true, }, }); // @ts-expect-error window.fetch.mockReturnValue( Promise.resolve({ ok: false, status: 401, statusText: 'Unauthorized', headers: { has: (name: string) => false, get: (name: string) => null, }, text: () => Promise.resolve(''), }), ); await openmrsFetch('/ws/rest/v1/session'); expect(mockNavigate.mock.calls[0][0]).toStrictEqual({ to: '${openmrsSpaBase}/login', }); }); it('navigates to spa login page when the server responds with a 401', async () => { mockGetConfig.mockResolvedValueOnce({ redirectAuthFailure: { enabled: true, url: '/openmrs/spa/login', errors: [401], resolvePromise: true, }, }); // @ts-expect-error window.fetch.mockReturnValue( Promise.resolve({ ok: false, status: 401, statusText: 'You are not authorized', text: () => Promise.resolve('a string response body'), }), ); await openmrsFetch('/ws/rest/v1/session'); expect(mockNavigate.mock.calls[0][0]).toStrictEqual({ to: '/openmrs/spa/login', }); }); }); describe('openmrsObservableFetch', () => { beforeEach(() => { window.openmrsBase = '/openmrs'; window.fetch = vi.fn(); }); it('calls window.fetch with the correct arguments for a basic GET request', async () => { // @ts-ignore window.fetch.mockReturnValue( Promise.resolve({ ok: true, status: 200, clone: () => ({ text: () => Promise.resolve('{"value": "hi"}'), }), }), ); const observable = openmrsObservableFetch('/ws/rest/v1/session'); expect(isObservable(observable)).toBe(true); await new Promise<void>((resolve, reject) => observable.subscribe( (response) => { expect(response.data).toEqual({ value: 'hi' }); resolve(); }, (err) => { reject(err); }, ), ); expect(window.fetch).toHaveBeenCalled(); // @ts-expect-error expect(window.fetch.mock.calls[0][0]).toEqual('/openmrs/ws/rest/v1/session'); // @ts-expect-error expect(window.fetch.mock.calls[0][1].headers.Accept).toEqual('application/json'); }); it('aborts the fetch request when subscription is unsubscribed', () => { // @ts-expect-error window.fetch.mockReturnValue(new Promise(() => {})); const subscription = openmrsObservableFetch('/ws/rest/v1/session').subscribe(); // @ts-expect-error const abortSignal: AbortSignal = window.fetch.mock.calls[0][1].signal; expect(abortSignal.aborted).toBe(false); subscription.unsubscribe(); expect(abortSignal.aborted).toBe(true); }); });