@openmrs/esm-api 10.0.1-pre.5602 → 10.0.1-pre.5607
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/openmrs-fetch.d.ts.map +1 -1
- package/dist/openmrs-fetch.js +27 -2
- package/package.json +9 -9
- package/src/openmrs-fetch.rest.test.ts +121 -0
- package/src/openmrs-fetch.test.ts +53 -155
- package/src/openmrs-fetch.ts +29 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openmrs-fetch.d.ts","sourceRoot":"","sources":["../src/openmrs-fetch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7C,mEAAmE;AACnE,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,uEAAuE;AACvE,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAC1C,sDAAsD;AACtD,eAAO,MAAM,eAAe,wBAA2B,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,UASnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,YAAY,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,WAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"openmrs-fetch.d.ts","sourceRoot":"","sources":["../src/openmrs-fetch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7C,mEAAmE;AACnE,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,uEAAuE;AACvE,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAC1C,sDAAsD;AACtD,eAAO,MAAM,eAAe,wBAA2B,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,UASnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,YAAY,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,WAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAkM1G;AAED,qBAAa,iBAAkB,SAAQ,KAAM,YAAW,UAAU;gBACpD,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,EAAE,iBAAiB,EAAE,KAAK;IAQxG,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACxE,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;CAC3B;AAED,KAAK,YAAY,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,UAAU,SAAS;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;CACnC"}
|
package/dist/openmrs-fetch.js
CHANGED
|
@@ -123,8 +123,33 @@ import { defaultRedirectAuthFailureUrl } from "./config-schema.js";
|
|
|
123
123
|
/* This tells the OpenMRS REST API not to return a WWW-Authenticate
|
|
124
124
|
* header. Returning that header is useful when using the API, but
|
|
125
125
|
* not from a UI.
|
|
126
|
-
*/
|
|
127
|
-
|
|
126
|
+
*/ let isRestRequest = false;
|
|
127
|
+
try {
|
|
128
|
+
const requestUrl = new URL(url, window.location.href);
|
|
129
|
+
const restUrl = new URL(makeUrl(restBaseUrl), window.location.href);
|
|
130
|
+
// Compare normalized paths without changing the outgoing URL. Pagination links
|
|
131
|
+
// may omit the extra slash introduced by an openmrsBase with a trailing slash.
|
|
132
|
+
const requestPath = requestUrl.pathname.replace(/\/{2,}/g, '/');
|
|
133
|
+
const restPath = restUrl.pathname.replace(/\/{2,}/g, '/');
|
|
134
|
+
isRestRequest = requestUrl.origin === restUrl.origin && (requestPath === restPath || requestPath.startsWith(`${restPath}/`));
|
|
135
|
+
} catch {
|
|
136
|
+
// Let native fetch reject malformed URLs instead of throwing synchronously here.
|
|
137
|
+
}
|
|
138
|
+
if (isRestRequest) {
|
|
139
|
+
let hasAuthHeader = false;
|
|
140
|
+
for (const [name, value] of Object.entries(fetchInit.headers)){
|
|
141
|
+
if (name.toLowerCase() === 'disable-www-authenticate') {
|
|
142
|
+
if (value === undefined) {
|
|
143
|
+
// Remove every casing so fetch cannot combine "undefined" with the default.
|
|
144
|
+
delete fetchInit.headers[name];
|
|
145
|
+
} else {
|
|
146
|
+
hasAuthHeader = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!hasAuthHeader) {
|
|
151
|
+
fetchInit.headers['Disable-WWW-Authenticate'] = 'true';
|
|
152
|
+
}
|
|
128
153
|
}
|
|
129
154
|
if (path.startsWith(fhirBaseUrl)) {
|
|
130
155
|
const urlUrl = new URL(url, window.location.toString());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-api",
|
|
3
|
-
"version": "10.0.1-pre.
|
|
3
|
+
"version": "10.0.1-pre.5607",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "The javascript module for interacting with the OpenMRS API",
|
|
6
6
|
"type": "module",
|
|
@@ -57,16 +57,16 @@
|
|
|
57
57
|
"lodash-es": "^4.17.21"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@openmrs/esm-config": "^10.0.1-pre.
|
|
61
|
-
"@openmrs/esm-error-handling": "^10.0.1-pre.
|
|
62
|
-
"@openmrs/esm-globals": "^10.0.1-pre.
|
|
63
|
-
"@openmrs/esm-navigation": "^10.0.1-pre.
|
|
60
|
+
"@openmrs/esm-config": "^10.0.1-pre.5607",
|
|
61
|
+
"@openmrs/esm-error-handling": "^10.0.1-pre.5607",
|
|
62
|
+
"@openmrs/esm-globals": "^10.0.1-pre.5607",
|
|
63
|
+
"@openmrs/esm-navigation": "^10.0.1-pre.5607"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@openmrs/esm-config": "10.0.1-pre.
|
|
67
|
-
"@openmrs/esm-error-handling": "10.0.1-pre.
|
|
68
|
-
"@openmrs/esm-globals": "10.0.1-pre.
|
|
69
|
-
"@openmrs/esm-navigation": "10.0.1-pre.
|
|
66
|
+
"@openmrs/esm-config": "10.0.1-pre.5607",
|
|
67
|
+
"@openmrs/esm-error-handling": "10.0.1-pre.5607",
|
|
68
|
+
"@openmrs/esm-globals": "10.0.1-pre.5607",
|
|
69
|
+
"@openmrs/esm-navigation": "10.0.1-pre.5607",
|
|
70
70
|
"@swc/cli": "0.8.1",
|
|
71
71
|
"@swc/core": "1.15.21",
|
|
72
72
|
"@vitest/coverage-v8": "^4.1.2",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getConfig } from '@openmrs/esm-config';
|
|
3
|
+
import { openmrsFetch } from './openmrs-fetch';
|
|
4
|
+
|
|
5
|
+
vi.mock('@openmrs/esm-navigation', () => ({ clearHistory: vi.fn(), navigate: vi.fn() }));
|
|
6
|
+
|
|
7
|
+
const mockFetch = vi.fn<typeof fetch>();
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
window.openmrsBase = '/openmrs';
|
|
11
|
+
vi.stubGlobal('location', new URL('https://frontend.example/openmrs/spa/'));
|
|
12
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
13
|
+
mockFetch.mockResolvedValue(new Response('{}'));
|
|
14
|
+
vi.mocked(getConfig).mockResolvedValue({
|
|
15
|
+
redirectAuthFailure: { enabled: false, url: '', errors: [401], resolvePromise: false },
|
|
16
|
+
followRedirects: true,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
21
|
+
|
|
22
|
+
describe('REST authentication challenge suppression', () => {
|
|
23
|
+
it('leaves malformed URLs to native fetch so callers can catch the rejected promise', async () => {
|
|
24
|
+
const error = new TypeError('Invalid URL');
|
|
25
|
+
mockFetch.mockRejectedValueOnce(error);
|
|
26
|
+
|
|
27
|
+
const request = openmrsFetch('http://[');
|
|
28
|
+
|
|
29
|
+
expect(request).toBeInstanceOf(Promise);
|
|
30
|
+
await expect(request.catch((reason) => reason)).resolves.toBe(error);
|
|
31
|
+
expect(mockFetch).toHaveBeenCalledWith('http://[', expect.any(Object));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it.each([
|
|
35
|
+
['/openmrs', '/ws/rest/v1/patient', '/openmrs/ws/rest/v1/patient'],
|
|
36
|
+
['/openmrs', 'ws/rest/v1/patient', '/openmrs/ws/rest/v1/patient'],
|
|
37
|
+
[
|
|
38
|
+
'/openmrs',
|
|
39
|
+
'https://frontend.example/openmrs/ws/rest/v1/patient?startIndex=10',
|
|
40
|
+
'https://frontend.example/openmrs/ws/rest/v1/patient?startIndex=10',
|
|
41
|
+
],
|
|
42
|
+
['/openmrs', '/ws/rest/v1', '/openmrs/ws/rest/v1'],
|
|
43
|
+
['/openmrs/', '/ws/rest/v1/patient', '/openmrs//ws/rest/v1/patient'],
|
|
44
|
+
[
|
|
45
|
+
'/openmrs/',
|
|
46
|
+
'https://frontend.example/openmrs/ws/rest/v1/patient',
|
|
47
|
+
'https://frontend.example/openmrs/ws/rest/v1/patient',
|
|
48
|
+
],
|
|
49
|
+
['https://api.example/openmrs', '/ws/rest/v1/patient', 'https://api.example/openmrs/ws/rest/v1/patient'],
|
|
50
|
+
[
|
|
51
|
+
'https://api.example/openmrs/',
|
|
52
|
+
'https://api.example/openmrs/ws/rest/v1/patient',
|
|
53
|
+
'https://api.example/openmrs/ws/rest/v1/patient',
|
|
54
|
+
],
|
|
55
|
+
])('adds the header for base %s and path %s without rewriting the request URL', async (base, path, expectedUrl) => {
|
|
56
|
+
window.openmrsBase = base;
|
|
57
|
+
|
|
58
|
+
await openmrsFetch(path);
|
|
59
|
+
|
|
60
|
+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
|
61
|
+
headers: { Accept: 'application/json', 'Disable-WWW-Authenticate': 'true' },
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it.each([
|
|
66
|
+
['/openmrs', 'https://other.example/openmrs/ws/rest/v1/patient'],
|
|
67
|
+
['https://api.example/openmrs', 'https://frontend.example/openmrs/ws/rest/v1/patient'],
|
|
68
|
+
['/openmrs', 'http://frontend.example/openmrs/ws/rest/v1/patient'],
|
|
69
|
+
['/openmrs', 'https://frontend.example:8443/openmrs/ws/rest/v1/patient'],
|
|
70
|
+
['/openmrs', 'https://frontend.example/another/ws/rest/v1/patient'],
|
|
71
|
+
['/openmrs', '/ws/rest/v10/patient'],
|
|
72
|
+
['/openmrs', '/ws/rest/v1relationship'],
|
|
73
|
+
['/openmrs', '/ws/fhir2/R4/Patient'],
|
|
74
|
+
])('does not add the header outside the configured REST API: %s, %s', async (base, path) => {
|
|
75
|
+
window.openmrsBase = base;
|
|
76
|
+
|
|
77
|
+
await openmrsFetch(path);
|
|
78
|
+
|
|
79
|
+
expect(new Headers(mockFetch.mock.calls[0][1]?.headers).has('Disable-WWW-Authenticate')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it.each(['Disable-WWW-Authenticate', 'disable-www-authenticate', 'DISABLE-WWW-AUTHENTICATE'])(
|
|
83
|
+
'preserves an explicit %s override, including null',
|
|
84
|
+
async (name) => {
|
|
85
|
+
for (const value of ['true', 'false', null]) {
|
|
86
|
+
// null retains legacy pass-through behavior, unlike Accept: null which removes the header.
|
|
87
|
+
const headers = { [name]: value };
|
|
88
|
+
await openmrsFetch('/ws/rest/v1/patient', { headers });
|
|
89
|
+
expect(mockFetch.mock.lastCall?.[1]?.headers).toEqual({ [name]: value, Accept: 'application/json' });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
it.each([
|
|
95
|
+
{ 'Disable-WWW-Authenticate': undefined },
|
|
96
|
+
{ 'disable-www-authenticate': undefined },
|
|
97
|
+
{ 'DISABLE-WWW-AUTHENTICATE': undefined },
|
|
98
|
+
{ 'Disable-WWW-Authenticate': undefined, 'disable-www-authenticate': undefined },
|
|
99
|
+
])('defaults undefined overrides without combining header values: %j', async (headers) => {
|
|
100
|
+
// @ts-expect-error JavaScript callers can supply undefined despite the FetchHeaders type.
|
|
101
|
+
await openmrsFetch('/ws/rest/v1/patient', { headers });
|
|
102
|
+
|
|
103
|
+
expect(mockFetch.mock.lastCall?.[1]?.headers).toEqual({
|
|
104
|
+
Accept: 'application/json',
|
|
105
|
+
'Disable-WWW-Authenticate': 'true',
|
|
106
|
+
});
|
|
107
|
+
expect(new Headers(mockFetch.mock.lastCall?.[1]?.headers).get('Disable-WWW-Authenticate')).toBe('true');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('preserves a defined override when another casing has an undefined value', async () => {
|
|
111
|
+
const headers = { 'Disable-WWW-Authenticate': undefined, 'disable-www-authenticate': 'false' };
|
|
112
|
+
// @ts-expect-error JavaScript callers can supply undefined despite the FetchHeaders type.
|
|
113
|
+
await openmrsFetch('/ws/rest/v1/patient', { headers });
|
|
114
|
+
|
|
115
|
+
expect(mockFetch.mock.lastCall?.[1]?.headers).toEqual({
|
|
116
|
+
Accept: 'application/json',
|
|
117
|
+
'disable-www-authenticate': 'false',
|
|
118
|
+
});
|
|
119
|
+
expect(new Headers(mockFetch.mock.lastCall?.[1]?.headers).get('Disable-WWW-Authenticate')).toBe('false');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -10,36 +10,35 @@ vi.mock('@openmrs/esm-navigation', () => ({
|
|
|
10
10
|
|
|
11
11
|
const mockGetConfig = vi.mocked(getConfig);
|
|
12
12
|
const mockNavigate = vi.mocked(navigate);
|
|
13
|
+
const mockFetch = vi.fn<typeof fetch>();
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
writable: true,
|
|
32
|
-
value: { assign: vi.fn() },
|
|
33
|
-
});
|
|
34
|
-
});
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockGetConfig.mockReturnValue(
|
|
17
|
+
Promise.resolve({
|
|
18
|
+
redirectAuthFailure: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
url: '${openmrsSpaBase}/login',
|
|
21
|
+
errors: [401],
|
|
22
|
+
resolvePromise: false,
|
|
23
|
+
},
|
|
24
|
+
followRedirects: true,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
window.openmrsBase = '/openmrs';
|
|
28
|
+
window.getOpenmrsSpaBase = () => '/openmrs/spa/';
|
|
29
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
30
|
+
vi.stubGlobal('location', new URL('https://frontend.example/openmrs/spa/'));
|
|
31
|
+
});
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.unstubAllGlobals();
|
|
35
|
+
// @ts-expect-error Not normally deletable
|
|
36
|
+
delete window.openmrsBase;
|
|
37
|
+
// @ts-expect-error Not normally deletable
|
|
38
|
+
delete window.getOpenmrsSpaBase;
|
|
39
|
+
});
|
|
42
40
|
|
|
41
|
+
describe('openmrsFetch', () => {
|
|
43
42
|
it(`throws an error if you don't pass in a url string`, () => {
|
|
44
43
|
// @ts-expect-error
|
|
45
44
|
expect(() => openmrsFetch()).toThrow(/first argument/);
|
|
@@ -63,8 +62,7 @@ describe('openmrsFetch', () => {
|
|
|
63
62
|
});
|
|
64
63
|
|
|
65
64
|
it('calls window.fetch with the correct arguments for a basic GET request', () => {
|
|
66
|
-
|
|
67
|
-
window.fetch.mockReturnValue(new Promise(() => {}));
|
|
65
|
+
mockFetch.mockReturnValue(new Promise(() => {}));
|
|
68
66
|
openmrsFetch('/ws/rest/v1/session');
|
|
69
67
|
expect(window.fetch).toHaveBeenCalledWith('/openmrs/ws/rest/v1/session', {
|
|
70
68
|
headers: {
|
|
@@ -75,8 +73,7 @@ describe('openmrsFetch', () => {
|
|
|
75
73
|
});
|
|
76
74
|
|
|
77
75
|
it('calls window.fetch correctly for requests that have a request body', () => {
|
|
78
|
-
|
|
79
|
-
window.fetch.mockReturnValue(new Promise(() => {}));
|
|
76
|
+
mockFetch.mockReturnValue(new Promise(() => {}));
|
|
80
77
|
const requestBody = { some: 'json' };
|
|
81
78
|
openmrsFetch('/ws/rest/v1/session', {
|
|
82
79
|
method: 'POST',
|
|
@@ -93,8 +90,7 @@ describe('openmrsFetch', () => {
|
|
|
93
90
|
});
|
|
94
91
|
|
|
95
92
|
it('allows you to specify your own Accept request header', () => {
|
|
96
|
-
|
|
97
|
-
window.fetch.mockReturnValue(new Promise(() => {}));
|
|
93
|
+
mockFetch.mockReturnValue(new Promise(() => {}));
|
|
98
94
|
openmrsFetch('/ws/rest/v1/session', {
|
|
99
95
|
headers: {
|
|
100
96
|
Accept: 'application/xml',
|
|
@@ -109,8 +105,7 @@ describe('openmrsFetch', () => {
|
|
|
109
105
|
});
|
|
110
106
|
|
|
111
107
|
it('allows you to specify no Accept request header to be sent', () => {
|
|
112
|
-
|
|
113
|
-
window.fetch.mockReturnValue(new Promise(() => {}));
|
|
108
|
+
mockFetch.mockReturnValue(new Promise(() => {}));
|
|
114
109
|
openmrsFetch('/ws/rest/v1/session', {
|
|
115
110
|
headers: {
|
|
116
111
|
// specifically null on purpose
|
|
@@ -126,20 +121,7 @@ describe('openmrsFetch', () => {
|
|
|
126
121
|
});
|
|
127
122
|
|
|
128
123
|
it('returns a promise that resolves with a json object when the request succeeds', async () => {
|
|
129
|
-
|
|
130
|
-
window.fetch.mockReturnValue(
|
|
131
|
-
Promise.resolve({
|
|
132
|
-
ok: true,
|
|
133
|
-
status: 200,
|
|
134
|
-
headers: {
|
|
135
|
-
has: () => false,
|
|
136
|
-
get: () => null,
|
|
137
|
-
},
|
|
138
|
-
clone: () => ({
|
|
139
|
-
text: () => Promise.resolve('{ "value": "hi" }'),
|
|
140
|
-
}),
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
124
|
+
mockFetch.mockResolvedValue(new Response('{"value":"hi"}'));
|
|
143
125
|
|
|
144
126
|
const response = await openmrsFetch('/ws/rest/v1/session');
|
|
145
127
|
expect(response.status).toBe(200);
|
|
@@ -147,18 +129,7 @@ describe('openmrsFetch', () => {
|
|
|
147
129
|
});
|
|
148
130
|
|
|
149
131
|
it('returns a promise that resolves with null when the request succeeds with HTTP 204', async () => {
|
|
150
|
-
|
|
151
|
-
window.fetch.mockReturnValue(
|
|
152
|
-
Promise.resolve({
|
|
153
|
-
ok: true,
|
|
154
|
-
status: 204,
|
|
155
|
-
headers: {
|
|
156
|
-
has: () => false,
|
|
157
|
-
get: () => null,
|
|
158
|
-
},
|
|
159
|
-
json: () => Promise.reject(Error("No json for HTTP 204's!!")),
|
|
160
|
-
}),
|
|
161
|
-
);
|
|
132
|
+
mockFetch.mockResolvedValue(new Response(null, { status: 204 }));
|
|
162
133
|
|
|
163
134
|
const response = await openmrsFetch('/ws/rest/v1/session');
|
|
164
135
|
expect(response.status).toBe(204);
|
|
@@ -166,56 +137,30 @@ describe('openmrsFetch', () => {
|
|
|
166
137
|
});
|
|
167
138
|
|
|
168
139
|
it('gives you an amazing error when the server responds with a 500 that has json', async () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
Promise.resolve({
|
|
172
|
-
ok: false,
|
|
140
|
+
mockFetch.mockResolvedValue(
|
|
141
|
+
new Response(JSON.stringify({ error: 'The server is dead' }), {
|
|
173
142
|
status: 500,
|
|
174
143
|
statusText: 'Internal Server Error',
|
|
175
|
-
clone: () => ({
|
|
176
|
-
text: () =>
|
|
177
|
-
Promise.resolve(
|
|
178
|
-
JSON.stringify({
|
|
179
|
-
error: 'The server is dead',
|
|
180
|
-
}),
|
|
181
|
-
),
|
|
182
|
-
}),
|
|
183
144
|
}),
|
|
184
145
|
);
|
|
185
146
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
expect
|
|
192
|
-
|
|
193
|
-
expect(err.response.status).toBe(500);
|
|
194
|
-
}
|
|
147
|
+
await expect(openmrsFetch('/ws/rest/v1/session')).rejects.toMatchObject({
|
|
148
|
+
message: expect.stringContaining(
|
|
149
|
+
'Server responded with 500 (Internal Server Error) for url /openmrs/ws/rest/v1/session',
|
|
150
|
+
),
|
|
151
|
+
responseBody: { error: 'The server is dead' },
|
|
152
|
+
response: expect.objectContaining({ status: 500 }),
|
|
153
|
+
});
|
|
195
154
|
});
|
|
196
155
|
|
|
197
156
|
it("gives you an amazing error when the server responds with a 400 that doesn't have json", async () => {
|
|
198
|
-
|
|
199
|
-
window.fetch.mockReturnValue(
|
|
200
|
-
Promise.resolve({
|
|
201
|
-
ok: false,
|
|
202
|
-
status: 400,
|
|
203
|
-
statusText: 'You goofed up',
|
|
204
|
-
clone: () => ({
|
|
205
|
-
text: () => Promise.resolve('a string response body'),
|
|
206
|
-
}),
|
|
207
|
-
}),
|
|
208
|
-
);
|
|
157
|
+
mockFetch.mockResolvedValue(new Response('a string response body', { status: 400, statusText: 'You goofed up' }));
|
|
209
158
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
expect(err.message).toMatch(/\/ws\/rest\/v1\/session/);
|
|
216
|
-
expect(err.responseBody).toEqual('a string response body');
|
|
217
|
-
expect(err.response.status).toBe(400);
|
|
218
|
-
}
|
|
159
|
+
await expect(openmrsFetch('/ws/rest/v1/session')).rejects.toMatchObject({
|
|
160
|
+
message: expect.stringContaining('Server responded with 400 (You goofed up) for url /openmrs/ws/rest/v1/session'),
|
|
161
|
+
responseBody: 'a string response body',
|
|
162
|
+
response: expect.objectContaining({ status: 400 }),
|
|
163
|
+
});
|
|
219
164
|
});
|
|
220
165
|
|
|
221
166
|
it('redirects to the Location header URL when a 401 response contains a Location header (auth-module challenge)', async () => {
|
|
@@ -228,18 +173,8 @@ describe('openmrsFetch', () => {
|
|
|
228
173
|
},
|
|
229
174
|
});
|
|
230
175
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
Promise.resolve({
|
|
234
|
-
ok: false,
|
|
235
|
-
status: 401,
|
|
236
|
-
statusText: 'Unauthorized',
|
|
237
|
-
headers: {
|
|
238
|
-
has: (name: string) => name.toLowerCase() === 'location',
|
|
239
|
-
get: (name: string) => (name.toLowerCase() === 'location' ? '/module/authentication/login.form' : null),
|
|
240
|
-
},
|
|
241
|
-
text: () => Promise.resolve(''),
|
|
242
|
-
}),
|
|
176
|
+
mockFetch.mockResolvedValue(
|
|
177
|
+
new Response(null, { status: 401, headers: { Location: '/module/authentication/login.form' } }),
|
|
243
178
|
);
|
|
244
179
|
|
|
245
180
|
await openmrsFetch('/ws/rest/v1/session');
|
|
@@ -250,26 +185,9 @@ describe('openmrsFetch', () => {
|
|
|
250
185
|
});
|
|
251
186
|
|
|
252
187
|
it('redirects to the Location header URL when session endpoint called and it contains a Location header', async () => {
|
|
253
|
-
|
|
254
|
-
window.fetch.mockReturnValue(
|
|
255
|
-
Promise.resolve({
|
|
256
|
-
ok: true,
|
|
257
|
-
status: 200,
|
|
258
|
-
statusText: 'OK',
|
|
259
|
-
headers: {
|
|
260
|
-
has: (name: string) => name.toLowerCase() === 'location',
|
|
261
|
-
get: (name: string) => (name.toLowerCase() === 'location' ? '/openmrs/spa/login' : null),
|
|
262
|
-
},
|
|
263
|
-
clone() {
|
|
264
|
-
return this;
|
|
265
|
-
},
|
|
266
|
-
text: () => Promise.resolve(''),
|
|
267
|
-
}),
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const fetchPromise = openmrsFetch('/ws/rest/v1/session');
|
|
188
|
+
mockFetch.mockResolvedValue(new Response(null, { status: 200, headers: { Location: '/openmrs/spa/login' } }));
|
|
271
189
|
|
|
272
|
-
await
|
|
190
|
+
await openmrsFetch('/ws/rest/v1/session');
|
|
273
191
|
|
|
274
192
|
expect(mockNavigate).toHaveBeenCalledWith({
|
|
275
193
|
to: '/openmrs/spa/login',
|
|
@@ -286,19 +204,7 @@ describe('openmrsFetch', () => {
|
|
|
286
204
|
},
|
|
287
205
|
});
|
|
288
206
|
|
|
289
|
-
|
|
290
|
-
window.fetch.mockReturnValue(
|
|
291
|
-
Promise.resolve({
|
|
292
|
-
ok: false,
|
|
293
|
-
status: 401,
|
|
294
|
-
statusText: 'Unauthorized',
|
|
295
|
-
headers: {
|
|
296
|
-
has: (name: string) => false,
|
|
297
|
-
get: (name: string) => null,
|
|
298
|
-
},
|
|
299
|
-
text: () => Promise.resolve(''),
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
207
|
+
mockFetch.mockResolvedValue(new Response(null, { status: 401 }));
|
|
302
208
|
|
|
303
209
|
await openmrsFetch('/ws/rest/v1/session');
|
|
304
210
|
|
|
@@ -317,15 +223,7 @@ describe('openmrsFetch', () => {
|
|
|
317
223
|
},
|
|
318
224
|
});
|
|
319
225
|
|
|
320
|
-
|
|
321
|
-
window.fetch.mockReturnValue(
|
|
322
|
-
Promise.resolve({
|
|
323
|
-
ok: false,
|
|
324
|
-
status: 401,
|
|
325
|
-
statusText: 'You are not authorized',
|
|
326
|
-
text: () => Promise.resolve('a string response body'),
|
|
327
|
-
}),
|
|
328
|
-
);
|
|
226
|
+
mockFetch.mockResolvedValue(new Response('a string response body', { status: 401 }));
|
|
329
227
|
|
|
330
228
|
await openmrsFetch('/ws/rest/v1/session');
|
|
331
229
|
|
package/src/openmrs-fetch.ts
CHANGED
|
@@ -134,8 +134,35 @@ export function openmrsFetch<T = any>(path: string, fetchInit: FetchConfig = {})
|
|
|
134
134
|
* header. Returning that header is useful when using the API, but
|
|
135
135
|
* not from a UI.
|
|
136
136
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
let isRestRequest = false;
|
|
138
|
+
try {
|
|
139
|
+
const requestUrl = new URL(url, window.location.href);
|
|
140
|
+
const restUrl = new URL(makeUrl(restBaseUrl), window.location.href);
|
|
141
|
+
// Compare normalized paths without changing the outgoing URL. Pagination links
|
|
142
|
+
// may omit the extra slash introduced by an openmrsBase with a trailing slash.
|
|
143
|
+
const requestPath = requestUrl.pathname.replace(/\/{2,}/g, '/');
|
|
144
|
+
const restPath = restUrl.pathname.replace(/\/{2,}/g, '/');
|
|
145
|
+
isRestRequest =
|
|
146
|
+
requestUrl.origin === restUrl.origin && (requestPath === restPath || requestPath.startsWith(`${restPath}/`));
|
|
147
|
+
} catch {
|
|
148
|
+
// Let native fetch reject malformed URLs instead of throwing synchronously here.
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (isRestRequest) {
|
|
152
|
+
let hasAuthHeader = false;
|
|
153
|
+
for (const [name, value] of Object.entries(fetchInit.headers)) {
|
|
154
|
+
if (name.toLowerCase() === 'disable-www-authenticate') {
|
|
155
|
+
if (value === undefined) {
|
|
156
|
+
// Remove every casing so fetch cannot combine "undefined" with the default.
|
|
157
|
+
delete fetchInit.headers[name];
|
|
158
|
+
} else {
|
|
159
|
+
hasAuthHeader = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!hasAuthHeader) {
|
|
164
|
+
fetchInit.headers['Disable-WWW-Authenticate'] = 'true';
|
|
165
|
+
}
|
|
139
166
|
}
|
|
140
167
|
|
|
141
168
|
if (path.startsWith(fhirBaseUrl)) {
|