@openmrs/esm-api 8.0.1-pre.3518 → 8.0.1-pre.3529
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
CHANGED
package/dist/current-user.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export declare function setUserLanguage(data: Session): void;
|
|
|
74
74
|
*/
|
|
75
75
|
export declare function refetchCurrentUser(username?: string, password?: string): Promise<SessionStore>;
|
|
76
76
|
export declare function clearCurrentUser(): void;
|
|
77
|
-
export declare function userHasAccess(requiredPrivilege: string | string
|
|
77
|
+
export declare function userHasAccess(requiredPrivilege: string | Array<string>, user: {
|
|
78
78
|
privileges: Array<Privilege>;
|
|
79
79
|
roles: Array<Role>;
|
|
80
80
|
}): boolean;
|
package/dist/current-user.js
CHANGED
|
@@ -110,7 +110,7 @@ export function clearCurrentUser() {
|
|
|
110
110
|
export function userHasAccess(requiredPrivilege, user) {
|
|
111
111
|
if (user === undefined) {
|
|
112
112
|
// if the user hasn't been loaded, then return false iff there is a required privilege
|
|
113
|
-
return Boolean(requiredPrivilege);
|
|
113
|
+
return !Boolean(requiredPrivilege);
|
|
114
114
|
}
|
|
115
115
|
if (!Boolean(requiredPrivilege)) {
|
|
116
116
|
// if user exists but no requiredPrivilege is defined
|
|
@@ -53,13 +53,15 @@ export interface SessionLocation {
|
|
|
53
53
|
}
|
|
54
54
|
export interface Privilege {
|
|
55
55
|
uuid: string;
|
|
56
|
+
name: string;
|
|
56
57
|
display: string;
|
|
57
58
|
links?: Array<any>;
|
|
58
59
|
}
|
|
59
60
|
export interface Role {
|
|
60
61
|
uuid: string;
|
|
62
|
+
name: string;
|
|
61
63
|
display: string;
|
|
62
|
-
links
|
|
64
|
+
links?: Array<any>;
|
|
63
65
|
}
|
|
64
66
|
export interface User extends OpenmrsResource {
|
|
65
67
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-api",
|
|
3
|
-
"version": "8.0.1-pre.
|
|
3
|
+
"version": "8.0.1-pre.3529",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "The javascript module for interacting with the OpenMRS API",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "cross-env TZ=UTC vitest run --passWithNoTests",
|
|
26
26
|
"test:watch": "cross-env TZ=UTC vitest watch --passWithNoTests",
|
|
27
|
+
"coverage": "cross-env TZ=UTC vitest run --coverage --passWithNoTests",
|
|
27
28
|
"build": "rimraf dist && concurrently \"swc --strip-leading-paths src -d dist\" \"tsc --project tsconfig.build.json\"",
|
|
28
29
|
"build:development": "rimraf dist && concurrently \"swc --strip-leading-paths src -d dist\" \"tsc --project tsconfig.build.json\"",
|
|
29
30
|
"typescript": "tsc --project tsconfig.build.json",
|
|
@@ -61,18 +62,19 @@
|
|
|
61
62
|
"@openmrs/esm-navigation": "6.x"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
|
-
"@openmrs/esm-config": "8.0.1-pre.
|
|
65
|
-
"@openmrs/esm-error-handling": "8.0.1-pre.
|
|
66
|
-
"@openmrs/esm-globals": "8.0.1-pre.
|
|
67
|
-
"@openmrs/esm-navigation": "8.0.1-pre.
|
|
65
|
+
"@openmrs/esm-config": "8.0.1-pre.3529",
|
|
66
|
+
"@openmrs/esm-error-handling": "8.0.1-pre.3529",
|
|
67
|
+
"@openmrs/esm-globals": "8.0.1-pre.3529",
|
|
68
|
+
"@openmrs/esm-navigation": "8.0.1-pre.3529",
|
|
68
69
|
"@swc/cli": "^0.7.7",
|
|
69
70
|
"@swc/core": "^1.11.29",
|
|
71
|
+
"@vitest/coverage-v8": "^4.0.7",
|
|
70
72
|
"concurrently": "^9.1.2",
|
|
71
73
|
"cross-env": "^7.0.3",
|
|
72
74
|
"happy-dom": "^17.4.7",
|
|
73
75
|
"rimraf": "^6.0.1",
|
|
74
76
|
"rxjs": "^6.5.3",
|
|
75
|
-
"vitest": "^
|
|
77
|
+
"vitest": "^4.0.7"
|
|
76
78
|
},
|
|
77
79
|
"stableVersion": "8.0.0"
|
|
78
80
|
}
|
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import '@testing-library/jest-dom/vitest';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
import {
|
|
5
|
+
userHasAccess,
|
|
6
|
+
getCurrentUser,
|
|
7
|
+
refetchCurrentUser,
|
|
8
|
+
clearCurrentUser,
|
|
9
|
+
getLoggedInUser,
|
|
10
|
+
setUserLanguage,
|
|
11
|
+
setSessionLocation,
|
|
12
|
+
setUserProperties,
|
|
13
|
+
sessionStore,
|
|
14
|
+
} from './current-user';
|
|
15
|
+
import type * as openmrsFetchExport from './openmrs-fetch';
|
|
16
|
+
import { openmrsFetch } from './openmrs-fetch';
|
|
17
|
+
import { reportError } from '@openmrs/esm-error-handling';
|
|
18
|
+
import type { LoggedInUser, Privilege, Role, Session } from './types';
|
|
19
|
+
|
|
20
|
+
// Mock only the function calls, not constants
|
|
21
|
+
vi.mock('./openmrs-fetch', async () => {
|
|
22
|
+
const actual = await vi.importActual<typeof openmrsFetchExport>('./openmrs-fetch');
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
openmrsFetch: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
vi.mock('@openmrs/esm-error-handling', () => ({
|
|
30
|
+
reportError: vi.fn(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const mockOpenmrsFetch = vi.mocked(openmrsFetch);
|
|
34
|
+
const mockReportError = vi.mocked(reportError);
|
|
35
|
+
|
|
36
|
+
// Helper to create mock fetch responses
|
|
37
|
+
function createMockFetchResponse<T>(data: T, ok = true): any {
|
|
38
|
+
return {
|
|
39
|
+
data,
|
|
40
|
+
ok,
|
|
41
|
+
status: ok ? 200 : 400,
|
|
42
|
+
statusText: ok ? 'OK' : 'Bad Request',
|
|
43
|
+
headers: new Headers(),
|
|
44
|
+
redirected: false,
|
|
45
|
+
type: 'basic' as const,
|
|
46
|
+
url: '',
|
|
47
|
+
clone: vi.fn(),
|
|
48
|
+
body: null,
|
|
49
|
+
bodyUsed: false,
|
|
50
|
+
arrayBuffer: vi.fn(),
|
|
51
|
+
blob: vi.fn(),
|
|
52
|
+
formData: vi.fn(),
|
|
53
|
+
json: vi.fn(),
|
|
54
|
+
text: vi.fn(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('userHasAccess', () => {
|
|
59
|
+
const createPrivilege = (display: string): Privilege => ({
|
|
60
|
+
uuid: `${display}-uuid`,
|
|
61
|
+
display,
|
|
62
|
+
name: display,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const createRole = (display: string): Role => ({
|
|
66
|
+
uuid: `${display}-uuid`,
|
|
67
|
+
display,
|
|
68
|
+
name: display,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const mockUser = {
|
|
72
|
+
privileges: [
|
|
73
|
+
createPrivilege('View Patients'),
|
|
74
|
+
createPrivilege('Edit Patients'),
|
|
75
|
+
createPrivilege('Delete Patients'),
|
|
76
|
+
],
|
|
77
|
+
roles: [createRole('Clinician')],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const mockSuperUser = {
|
|
81
|
+
privileges: [createPrivilege('View Patients')],
|
|
82
|
+
roles: [createRole('System Developer')],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
it('should return true when user has the required privilege', () => {
|
|
86
|
+
expect(userHasAccess('View Patients', mockUser)).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return false when user lacks the required privilege', () => {
|
|
90
|
+
expect(userHasAccess('Manage Users', mockUser)).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle array of privileges (user needs ALL)', () => {
|
|
94
|
+
expect(userHasAccess(['View Patients', 'Edit Patients'], mockUser)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return false when user lacks one of multiple required privileges', () => {
|
|
98
|
+
expect(userHasAccess(['View Patients', 'Manage Users'], mockUser)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return true when user is undefined and no privilege is required', () => {
|
|
102
|
+
// @ts-expect-error Testing with undefined user
|
|
103
|
+
expect(userHasAccess(undefined, undefined)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return false when user is undefined and privilege is required', () => {
|
|
107
|
+
// @ts-expect-error Testing with undefined user
|
|
108
|
+
const result = userHasAccess('View Patients', undefined);
|
|
109
|
+
expect(result).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return true when no privilege is required and user exists', () => {
|
|
113
|
+
expect(userHasAccess('', mockUser)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return true for super users regardless of privileges', () => {
|
|
117
|
+
expect(userHasAccess('Manage Users', mockSuperUser)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should be case-sensitive for privilege names', () => {
|
|
121
|
+
expect(userHasAccess('view patients', mockUser)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle empty privilege array', () => {
|
|
125
|
+
expect(userHasAccess([], mockUser)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should return true when single privilege in array matches', () => {
|
|
129
|
+
expect(userHasAccess(['View Patients'], mockUser)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return false when user has no privileges', () => {
|
|
133
|
+
const userWithNoPrivileges = { privileges: [], roles: [] };
|
|
134
|
+
const result = userHasAccess('View Patients', userWithNoPrivileges);
|
|
135
|
+
expect(result).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('clearCurrentUser', () => {
|
|
140
|
+
it('should clear the session store', () => {
|
|
141
|
+
clearCurrentUser();
|
|
142
|
+
const state = sessionStore.getState();
|
|
143
|
+
expect(state.loaded).toBe(true);
|
|
144
|
+
expect(state.session?.authenticated).toBe(false);
|
|
145
|
+
expect(state.session?.sessionId).toBe('');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('setUserLanguage', () => {
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
document.documentElement.removeAttribute('lang');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should set document language from session locale', () => {
|
|
155
|
+
const session: Session = {
|
|
156
|
+
authenticated: true,
|
|
157
|
+
sessionId: 'test-session',
|
|
158
|
+
locale: 'en-US',
|
|
159
|
+
user: {} as LoggedInUser,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
setUserLanguage(session);
|
|
163
|
+
expect(document.documentElement).toHaveAttribute('lang', 'en-US');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should set document language from user properties defaultLocale', () => {
|
|
167
|
+
const session: Session = {
|
|
168
|
+
authenticated: true,
|
|
169
|
+
sessionId: 'test-session',
|
|
170
|
+
user: {
|
|
171
|
+
userProperties: {
|
|
172
|
+
defaultLocale: 'fr-FR',
|
|
173
|
+
},
|
|
174
|
+
} as unknown as LoggedInUser,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
setUserLanguage(session);
|
|
178
|
+
expect(document.documentElement).toHaveAttribute('lang', 'fr-FR');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should prefer session locale over user properties', () => {
|
|
182
|
+
const session: Session = {
|
|
183
|
+
authenticated: true,
|
|
184
|
+
sessionId: 'test-session',
|
|
185
|
+
locale: 'es-ES',
|
|
186
|
+
user: {
|
|
187
|
+
userProperties: {
|
|
188
|
+
defaultLocale: 'fr-FR',
|
|
189
|
+
},
|
|
190
|
+
} as unknown as LoggedInUser,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
setUserLanguage(session);
|
|
194
|
+
expect(document.documentElement).toHaveAttribute('lang', 'es-ES');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should convert underscores to hyphens in locale', () => {
|
|
198
|
+
const session: Session = {
|
|
199
|
+
authenticated: true,
|
|
200
|
+
sessionId: 'test-session',
|
|
201
|
+
locale: 'en_US',
|
|
202
|
+
user: {} as LoggedInUser,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
setUserLanguage(session);
|
|
206
|
+
expect(document.documentElement).toHaveAttribute('lang', 'en-US');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should not set language if locale is invalid', () => {
|
|
210
|
+
const session: Session = {
|
|
211
|
+
authenticated: true,
|
|
212
|
+
sessionId: 'test-session',
|
|
213
|
+
locale: 'invalid-locale-xyz',
|
|
214
|
+
user: {} as LoggedInUser,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
setUserLanguage(session);
|
|
218
|
+
expect(document.documentElement).not.toHaveAttribute('lang');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should not set language if locale is undefined', () => {
|
|
222
|
+
const session: Session = {
|
|
223
|
+
authenticated: true,
|
|
224
|
+
sessionId: 'test-session',
|
|
225
|
+
user: {} as LoggedInUser,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
setUserLanguage(session);
|
|
229
|
+
expect(document.documentElement).not.toHaveAttribute('lang');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should not update if language is already set to the same value', () => {
|
|
233
|
+
document.documentElement.setAttribute('lang', 'en-US');
|
|
234
|
+
const session: Session = {
|
|
235
|
+
authenticated: true,
|
|
236
|
+
sessionId: 'test-session',
|
|
237
|
+
locale: 'en-US',
|
|
238
|
+
user: {} as LoggedInUser,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const spy = vi.spyOn(document.documentElement, 'setAttribute');
|
|
242
|
+
setUserLanguage(session);
|
|
243
|
+
expect(spy).not.toHaveBeenCalled();
|
|
244
|
+
spy.mockRestore();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('getLoggedInUser', () => {
|
|
249
|
+
beforeEach(() => {
|
|
250
|
+
// Reset session store
|
|
251
|
+
sessionStore.setState({ loaded: false, session: null });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should return logged in user when session is loaded', async () => {
|
|
255
|
+
const mockUser: LoggedInUser = {
|
|
256
|
+
uuid: 'user-uuid',
|
|
257
|
+
display: 'Test User',
|
|
258
|
+
username: 'testuser',
|
|
259
|
+
systemId: 'test-sys-id',
|
|
260
|
+
userProperties: {},
|
|
261
|
+
person: {} as any,
|
|
262
|
+
privileges: [],
|
|
263
|
+
roles: [],
|
|
264
|
+
retired: false,
|
|
265
|
+
locale: 'en',
|
|
266
|
+
allowedLocales: ['en'],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
sessionStore.setState({
|
|
270
|
+
loaded: true,
|
|
271
|
+
session: {
|
|
272
|
+
authenticated: true,
|
|
273
|
+
sessionId: 'test-session',
|
|
274
|
+
user: mockUser,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const user = await getLoggedInUser();
|
|
279
|
+
expect(user).toEqual(mockUser);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should wait for session to load if not already loaded', async () => {
|
|
283
|
+
const mockUser: LoggedInUser = {
|
|
284
|
+
uuid: 'user-uuid',
|
|
285
|
+
display: 'Test User',
|
|
286
|
+
username: 'testuser',
|
|
287
|
+
systemId: 'test-sys-id',
|
|
288
|
+
userProperties: {},
|
|
289
|
+
person: {} as any,
|
|
290
|
+
privileges: [],
|
|
291
|
+
roles: [],
|
|
292
|
+
retired: false,
|
|
293
|
+
locale: 'en',
|
|
294
|
+
allowedLocales: ['en'],
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const promise = getLoggedInUser();
|
|
298
|
+
|
|
299
|
+
// Simulate session loading after a delay
|
|
300
|
+
setTimeout(() => {
|
|
301
|
+
sessionStore.setState({
|
|
302
|
+
loaded: true,
|
|
303
|
+
session: {
|
|
304
|
+
authenticated: true,
|
|
305
|
+
sessionId: 'test-session',
|
|
306
|
+
user: mockUser,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}, 10);
|
|
310
|
+
|
|
311
|
+
const user = await promise;
|
|
312
|
+
expect(user).toEqual(mockUser);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('getCurrentUser', () => {
|
|
317
|
+
let subscriptions: Array<{ unsubscribe?: () => void }> = [];
|
|
318
|
+
|
|
319
|
+
beforeEach(() => {
|
|
320
|
+
sessionStore.setState({ loaded: false, session: null });
|
|
321
|
+
mockOpenmrsFetch.mockClear();
|
|
322
|
+
// Mock openmrsFetch to prevent unhandled promise rejections
|
|
323
|
+
mockOpenmrsFetch.mockResolvedValue(
|
|
324
|
+
createMockFetchResponse({
|
|
325
|
+
authenticated: false,
|
|
326
|
+
sessionId: '',
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
afterEach(() => {
|
|
332
|
+
// Clean up any active subscriptions
|
|
333
|
+
subscriptions.forEach((sub) => sub.unsubscribe?.());
|
|
334
|
+
subscriptions = [];
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should return an Observable', () => {
|
|
338
|
+
const result = getCurrentUser();
|
|
339
|
+
expect(result).toBeInstanceOf(Observable);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should emit user when session is loaded and includeAuthStatus is false', () => {
|
|
343
|
+
return new Promise<void>((resolve) => {
|
|
344
|
+
const mockUser: LoggedInUser = {
|
|
345
|
+
uuid: 'user-uuid',
|
|
346
|
+
display: 'Test User',
|
|
347
|
+
username: 'testuser',
|
|
348
|
+
systemId: 'test-sys-id',
|
|
349
|
+
userProperties: {},
|
|
350
|
+
person: {} as any,
|
|
351
|
+
privileges: [],
|
|
352
|
+
roles: [],
|
|
353
|
+
retired: false,
|
|
354
|
+
locale: 'en',
|
|
355
|
+
allowedLocales: ['en'],
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
sessionStore.setState({
|
|
359
|
+
loaded: true,
|
|
360
|
+
session: {
|
|
361
|
+
authenticated: true,
|
|
362
|
+
sessionId: 'test-session',
|
|
363
|
+
user: mockUser,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const sub = getCurrentUser({ includeAuthStatus: false }).subscribe((user) => {
|
|
368
|
+
expect(user).toEqual(mockUser);
|
|
369
|
+
resolve();
|
|
370
|
+
});
|
|
371
|
+
subscriptions.push(sub);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should emit full session when includeAuthStatus is true', () => {
|
|
376
|
+
return new Promise<void>((resolve) => {
|
|
377
|
+
const mockUser: LoggedInUser = {
|
|
378
|
+
uuid: 'user-uuid',
|
|
379
|
+
display: 'Test User',
|
|
380
|
+
username: 'testuser',
|
|
381
|
+
systemId: 'test-sys-id',
|
|
382
|
+
userProperties: {},
|
|
383
|
+
person: {} as any,
|
|
384
|
+
privileges: [],
|
|
385
|
+
roles: [],
|
|
386
|
+
retired: false,
|
|
387
|
+
locale: 'en',
|
|
388
|
+
allowedLocales: ['en'],
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const mockSession: Session = {
|
|
392
|
+
authenticated: true,
|
|
393
|
+
sessionId: 'test-session',
|
|
394
|
+
user: mockUser,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
sessionStore.setState({
|
|
398
|
+
loaded: true,
|
|
399
|
+
session: mockSession,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const sub = getCurrentUser({ includeAuthStatus: true }).subscribe((session) => {
|
|
403
|
+
expect(session).toEqual(mockSession);
|
|
404
|
+
resolve();
|
|
405
|
+
});
|
|
406
|
+
subscriptions.push(sub);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should not emit when session is not loaded', () => {
|
|
411
|
+
const handler = vi.fn();
|
|
412
|
+
const sub = getCurrentUser({ includeAuthStatus: false }).subscribe(handler);
|
|
413
|
+
subscriptions.push(sub);
|
|
414
|
+
|
|
415
|
+
expect(handler).not.toHaveBeenCalled();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should emit updates when session changes', () => {
|
|
419
|
+
return new Promise<void>((resolve) => {
|
|
420
|
+
const mockUser1: LoggedInUser = {
|
|
421
|
+
uuid: 'user-1',
|
|
422
|
+
display: 'User 1',
|
|
423
|
+
username: 'user1',
|
|
424
|
+
systemId: 'sys-1',
|
|
425
|
+
userProperties: {},
|
|
426
|
+
person: {} as any,
|
|
427
|
+
privileges: [],
|
|
428
|
+
roles: [],
|
|
429
|
+
retired: false,
|
|
430
|
+
locale: 'en',
|
|
431
|
+
allowedLocales: ['en'],
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const mockUser2: LoggedInUser = {
|
|
435
|
+
uuid: 'user-2',
|
|
436
|
+
display: 'User 2',
|
|
437
|
+
username: 'user2',
|
|
438
|
+
systemId: 'sys-2',
|
|
439
|
+
userProperties: {},
|
|
440
|
+
person: {} as any,
|
|
441
|
+
privileges: [],
|
|
442
|
+
roles: [],
|
|
443
|
+
retired: false,
|
|
444
|
+
locale: 'en',
|
|
445
|
+
allowedLocales: ['en'],
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const emittedUsers: LoggedInUser[] = [];
|
|
449
|
+
|
|
450
|
+
sessionStore.setState({
|
|
451
|
+
loaded: true,
|
|
452
|
+
session: {
|
|
453
|
+
authenticated: true,
|
|
454
|
+
sessionId: 'session-1',
|
|
455
|
+
user: mockUser1,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const sub = getCurrentUser({ includeAuthStatus: false }).subscribe((user) => {
|
|
460
|
+
emittedUsers.push(user);
|
|
461
|
+
if (emittedUsers.length === 2) {
|
|
462
|
+
expect(emittedUsers[0]).toEqual(mockUser1);
|
|
463
|
+
expect(emittedUsers[1]).toEqual(mockUser2);
|
|
464
|
+
resolve();
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
subscriptions.push(sub);
|
|
468
|
+
|
|
469
|
+
setTimeout(() => {
|
|
470
|
+
sessionStore.setState({
|
|
471
|
+
loaded: true,
|
|
472
|
+
session: {
|
|
473
|
+
authenticated: true,
|
|
474
|
+
sessionId: 'session-2',
|
|
475
|
+
user: mockUser2,
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
}, 10);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should allow unsubscribing', () => {
|
|
483
|
+
const handler = vi.fn();
|
|
484
|
+
const subscription = getCurrentUser({ includeAuthStatus: false }).subscribe(handler);
|
|
485
|
+
subscriptions.push(subscription);
|
|
486
|
+
|
|
487
|
+
sessionStore.setState({
|
|
488
|
+
loaded: true,
|
|
489
|
+
session: {
|
|
490
|
+
authenticated: true,
|
|
491
|
+
sessionId: 'test-session',
|
|
492
|
+
user: {} as LoggedInUser,
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
handler.mockClear();
|
|
497
|
+
subscription.unsubscribe();
|
|
498
|
+
|
|
499
|
+
sessionStore.setState({
|
|
500
|
+
loaded: true,
|
|
501
|
+
session: {
|
|
502
|
+
authenticated: false,
|
|
503
|
+
sessionId: 'new-session',
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(handler).not.toHaveBeenCalled();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('refetchCurrentUser', () => {
|
|
512
|
+
beforeEach(() => {
|
|
513
|
+
sessionStore.setState({ loaded: false, session: null });
|
|
514
|
+
mockOpenmrsFetch.mockClear();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should fetch user without credentials', async () => {
|
|
518
|
+
const mockSession: Session = {
|
|
519
|
+
authenticated: true,
|
|
520
|
+
sessionId: 'test-session',
|
|
521
|
+
user: {} as LoggedInUser,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
|
|
525
|
+
|
|
526
|
+
await refetchCurrentUser();
|
|
527
|
+
|
|
528
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledWith(
|
|
529
|
+
expect.stringContaining('/session'),
|
|
530
|
+
expect.objectContaining({
|
|
531
|
+
headers: {},
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should use Basic Auth when username and password are provided', async () => {
|
|
537
|
+
const mockSession: Session = {
|
|
538
|
+
authenticated: true,
|
|
539
|
+
sessionId: 'test-session',
|
|
540
|
+
user: {} as LoggedInUser,
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
|
|
544
|
+
|
|
545
|
+
await refetchCurrentUser('testuser', 'testpass');
|
|
546
|
+
|
|
547
|
+
const expectedAuth = `Basic ${btoa('testuser:testpass')}`;
|
|
548
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledWith(
|
|
549
|
+
expect.stringContaining('/session'),
|
|
550
|
+
expect.objectContaining({
|
|
551
|
+
headers: {
|
|
552
|
+
Authorization: expectedAuth,
|
|
553
|
+
},
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('should update session store on success', async () => {
|
|
559
|
+
const mockUser: LoggedInUser = {
|
|
560
|
+
uuid: 'user-uuid',
|
|
561
|
+
display: 'Test User',
|
|
562
|
+
username: 'testuser',
|
|
563
|
+
systemId: 'test-sys-id',
|
|
564
|
+
userProperties: {},
|
|
565
|
+
person: {} as any,
|
|
566
|
+
privileges: [],
|
|
567
|
+
roles: [],
|
|
568
|
+
retired: false,
|
|
569
|
+
locale: 'en',
|
|
570
|
+
allowedLocales: ['en'],
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const mockSession: Session = {
|
|
574
|
+
authenticated: true,
|
|
575
|
+
sessionId: 'test-session',
|
|
576
|
+
user: mockUser,
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
|
|
580
|
+
|
|
581
|
+
await refetchCurrentUser();
|
|
582
|
+
|
|
583
|
+
const state = sessionStore.getState();
|
|
584
|
+
expect(state.loaded).toBe(true);
|
|
585
|
+
expect(state.session).toEqual(mockSession);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should handle fetch failure', async () => {
|
|
589
|
+
mockOpenmrsFetch.mockRejectedValue(new Error('Network error'));
|
|
590
|
+
|
|
591
|
+
await expect(refetchCurrentUser()).rejects.toMatchObject({
|
|
592
|
+
loaded: false,
|
|
593
|
+
session: null,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
expect(mockReportError).toHaveBeenCalled();
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
describe('setSessionLocation', () => {
|
|
601
|
+
beforeEach(() => {
|
|
602
|
+
mockOpenmrsFetch.mockClear();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should set session location with AbortController', async () => {
|
|
606
|
+
const locationUuid = 'location-uuid-123';
|
|
607
|
+
const abortController = new AbortController();
|
|
608
|
+
const mockSession: Session = {
|
|
609
|
+
authenticated: true,
|
|
610
|
+
sessionId: 'test-session',
|
|
611
|
+
sessionLocation: {
|
|
612
|
+
uuid: locationUuid,
|
|
613
|
+
} as any,
|
|
614
|
+
user: {} as LoggedInUser,
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
|
|
618
|
+
|
|
619
|
+
await setSessionLocation(locationUuid, abortController);
|
|
620
|
+
|
|
621
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledWith(
|
|
622
|
+
expect.stringContaining('/session'),
|
|
623
|
+
expect.objectContaining({
|
|
624
|
+
method: 'POST',
|
|
625
|
+
body: { sessionLocation: locationUuid },
|
|
626
|
+
headers: {
|
|
627
|
+
'Content-Type': 'application/json',
|
|
628
|
+
},
|
|
629
|
+
signal: abortController.signal,
|
|
630
|
+
}),
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should update session store with new location', async () => {
|
|
635
|
+
const locationUuid = 'location-uuid-123';
|
|
636
|
+
const abortController = new AbortController();
|
|
637
|
+
const mockSession: Session = {
|
|
638
|
+
authenticated: true,
|
|
639
|
+
sessionId: 'test-session',
|
|
640
|
+
sessionLocation: {
|
|
641
|
+
uuid: locationUuid,
|
|
642
|
+
display: 'Test Location',
|
|
643
|
+
} as any,
|
|
644
|
+
user: {} as LoggedInUser,
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
|
|
648
|
+
|
|
649
|
+
await setSessionLocation(locationUuid, abortController);
|
|
650
|
+
|
|
651
|
+
const state = sessionStore.getState();
|
|
652
|
+
expect(state.loaded).toBe(true);
|
|
653
|
+
expect(state.session?.sessionLocation?.uuid).toBe(locationUuid);
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
describe('setUserProperties', () => {
|
|
658
|
+
beforeEach(() => {
|
|
659
|
+
mockOpenmrsFetch.mockClear();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('should set user properties and refetch session', async () => {
|
|
663
|
+
const userUuid = 'user-uuid-123';
|
|
664
|
+
const userProperties = {
|
|
665
|
+
defaultLocale: 'en-US',
|
|
666
|
+
favoriteColor: 'blue',
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
mockOpenmrsFetch
|
|
670
|
+
.mockResolvedValueOnce(createMockFetchResponse({})) // First call to update properties
|
|
671
|
+
.mockResolvedValueOnce(
|
|
672
|
+
createMockFetchResponse({
|
|
673
|
+
// Second call to refetch session
|
|
674
|
+
authenticated: true,
|
|
675
|
+
sessionId: 'test-session',
|
|
676
|
+
user: {
|
|
677
|
+
uuid: userUuid,
|
|
678
|
+
userProperties,
|
|
679
|
+
} as unknown as LoggedInUser,
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
await setUserProperties(userUuid, userProperties);
|
|
684
|
+
|
|
685
|
+
expect(mockOpenmrsFetch).toHaveBeenNthCalledWith(
|
|
686
|
+
1,
|
|
687
|
+
expect.stringContaining(`/user/${userUuid}`),
|
|
688
|
+
expect.objectContaining({
|
|
689
|
+
method: 'POST',
|
|
690
|
+
body: { userProperties },
|
|
691
|
+
headers: {
|
|
692
|
+
'Content-Type': 'application/json',
|
|
693
|
+
},
|
|
694
|
+
}),
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// Should refetch session after updating
|
|
698
|
+
expect(mockOpenmrsFetch).toHaveBeenCalledTimes(2);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('should use provided AbortController', async () => {
|
|
702
|
+
const userUuid = 'user-uuid-123';
|
|
703
|
+
const userProperties = { defaultLocale: 'fr-FR' };
|
|
704
|
+
const abortController = new AbortController();
|
|
705
|
+
|
|
706
|
+
mockOpenmrsFetch.mockResolvedValueOnce(createMockFetchResponse({})).mockResolvedValueOnce(
|
|
707
|
+
createMockFetchResponse({
|
|
708
|
+
authenticated: true,
|
|
709
|
+
sessionId: 'test-session',
|
|
710
|
+
user: {} as LoggedInUser,
|
|
711
|
+
}),
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
await setUserProperties(userUuid, userProperties, abortController);
|
|
715
|
+
|
|
716
|
+
expect(mockOpenmrsFetch).toHaveBeenNthCalledWith(
|
|
717
|
+
1,
|
|
718
|
+
expect.anything(),
|
|
719
|
+
expect.objectContaining({
|
|
720
|
+
signal: abortController.signal,
|
|
721
|
+
}),
|
|
722
|
+
);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('should create AbortController if not provided', async () => {
|
|
726
|
+
const userUuid = 'user-uuid-123';
|
|
727
|
+
const userProperties = { defaultLocale: 'es-ES' };
|
|
728
|
+
|
|
729
|
+
mockOpenmrsFetch.mockResolvedValueOnce(createMockFetchResponse({})).mockResolvedValueOnce(
|
|
730
|
+
createMockFetchResponse({
|
|
731
|
+
authenticated: true,
|
|
732
|
+
sessionId: 'test-session',
|
|
733
|
+
user: {} as LoggedInUser,
|
|
734
|
+
}),
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
await setUserProperties(userUuid, userProperties);
|
|
738
|
+
|
|
739
|
+
expect(mockOpenmrsFetch).toHaveBeenNthCalledWith(
|
|
740
|
+
1,
|
|
741
|
+
expect.anything(),
|
|
742
|
+
expect.objectContaining({
|
|
743
|
+
signal: expect.any(AbortSignal),
|
|
744
|
+
}),
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
});
|
package/src/current-user.ts
CHANGED
|
@@ -182,12 +182,12 @@ export function clearCurrentUser() {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
export function userHasAccess(
|
|
185
|
-
requiredPrivilege: string | string
|
|
185
|
+
requiredPrivilege: string | Array<string>,
|
|
186
186
|
user: { privileges: Array<Privilege>; roles: Array<Role> },
|
|
187
187
|
) {
|
|
188
188
|
if (user === undefined) {
|
|
189
189
|
// if the user hasn't been loaded, then return false iff there is a required privilege
|
|
190
|
-
return Boolean(requiredPrivilege);
|
|
190
|
+
return !Boolean(requiredPrivilege);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
if (!Boolean(requiredPrivilege)) {
|
|
@@ -54,14 +54,16 @@ export interface SessionLocation {
|
|
|
54
54
|
|
|
55
55
|
export interface Privilege {
|
|
56
56
|
uuid: string;
|
|
57
|
+
name: string;
|
|
57
58
|
display: string;
|
|
58
59
|
links?: Array<any>;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export interface Role {
|
|
62
63
|
uuid: string;
|
|
64
|
+
name: string;
|
|
63
65
|
display: string;
|
|
64
|
-
links
|
|
66
|
+
links?: Array<any>;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|