@openmrs/esm-api
Version:
The javascript module for interacting with the OpenMRS API
748 lines (639 loc) • 20.1 kB
text/typescript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { Observable } from 'rxjs';
import {
userHasAccess,
getCurrentUser,
refetchCurrentUser,
clearCurrentUser,
getLoggedInUser,
setUserLanguage,
setSessionLocation,
setUserProperties,
sessionStore,
} from './current-user';
import type * as openmrsFetchExport from './openmrs-fetch';
import { openmrsFetch } from './openmrs-fetch';
import { reportError } from '@openmrs/esm-error-handling';
import type { LoggedInUser, Privilege, Role, Session } from './types';
// Mock only the function calls, not constants
vi.mock('./openmrs-fetch', async () => {
const actual = await vi.importActual<typeof openmrsFetchExport>('./openmrs-fetch');
return {
...actual,
openmrsFetch: vi.fn(),
};
});
vi.mock('@openmrs/esm-error-handling', () => ({
reportError: vi.fn(),
}));
const mockOpenmrsFetch = vi.mocked(openmrsFetch);
const mockReportError = vi.mocked(reportError);
// Helper to create mock fetch responses
function createMockFetchResponse<T>(data: T, ok = true): any {
return {
data,
ok,
status: ok ? 200 : 400,
statusText: ok ? 'OK' : 'Bad Request',
headers: new Headers(),
redirected: false,
type: 'basic' as const,
url: '',
clone: vi.fn(),
body: null,
bodyUsed: false,
arrayBuffer: vi.fn(),
blob: vi.fn(),
formData: vi.fn(),
json: vi.fn(),
text: vi.fn(),
};
}
describe('userHasAccess', () => {
const createPrivilege = (display: string): Privilege => ({
uuid: `${display}-uuid`,
display,
name: display,
});
const createRole = (display: string): Role => ({
uuid: `${display}-uuid`,
display,
name: display,
});
const mockUser = {
privileges: [
createPrivilege('View Patients'),
createPrivilege('Edit Patients'),
createPrivilege('Delete Patients'),
],
roles: [createRole('Clinician')],
};
const mockSuperUser = {
privileges: [createPrivilege('View Patients')],
roles: [createRole('System Developer')],
};
it('should return true when user has the required privilege', () => {
expect(userHasAccess('View Patients', mockUser)).toBe(true);
});
it('should return false when user lacks the required privilege', () => {
expect(userHasAccess('Manage Users', mockUser)).toBe(false);
});
it('should handle array of privileges (user needs ALL)', () => {
expect(userHasAccess(['View Patients', 'Edit Patients'], mockUser)).toBe(true);
});
it('should return false when user lacks one of multiple required privileges', () => {
expect(userHasAccess(['View Patients', 'Manage Users'], mockUser)).toBe(false);
});
it('should return true when user is undefined and no privilege is required', () => {
// @ts-expect-error Testing with undefined user
expect(userHasAccess(undefined, undefined)).toBe(true);
});
it('should return false when user is undefined and privilege is required', () => {
// @ts-expect-error Testing with undefined user
const result = userHasAccess('View Patients', undefined);
expect(result).toBe(false);
});
it('should return true when no privilege is required and user exists', () => {
expect(userHasAccess('', mockUser)).toBe(true);
});
it('should return true for super users regardless of privileges', () => {
expect(userHasAccess('Manage Users', mockSuperUser)).toBe(true);
});
it('should be case-sensitive for privilege names', () => {
expect(userHasAccess('view patients', mockUser)).toBe(false);
});
it('should handle empty privilege array', () => {
expect(userHasAccess([], mockUser)).toBe(true);
});
it('should return true when single privilege in array matches', () => {
expect(userHasAccess(['View Patients'], mockUser)).toBe(true);
});
it('should return false when user has no privileges', () => {
const userWithNoPrivileges = { privileges: [], roles: [] };
const result = userHasAccess('View Patients', userWithNoPrivileges);
expect(result).toBe(false);
});
});
describe('clearCurrentUser', () => {
it('should clear the session store', () => {
clearCurrentUser();
const state = sessionStore.getState();
expect(state.loaded).toBe(true);
expect(state.session?.authenticated).toBe(false);
expect(state.session?.sessionId).toBe('');
});
});
describe('setUserLanguage', () => {
beforeEach(() => {
document.documentElement.removeAttribute('lang');
});
it('should set document language from session locale', () => {
const session: Session = {
authenticated: true,
sessionId: 'test-session',
locale: 'en-US',
user: {} as LoggedInUser,
};
setUserLanguage(session);
expect(document.documentElement).toHaveAttribute('lang', 'en-US');
});
it('should set document language from user properties defaultLocale', () => {
const session: Session = {
authenticated: true,
sessionId: 'test-session',
user: {
userProperties: {
defaultLocale: 'fr-FR',
},
} as unknown as LoggedInUser,
};
setUserLanguage(session);
expect(document.documentElement).toHaveAttribute('lang', 'fr-FR');
});
it('should prefer session locale over user properties', () => {
const session: Session = {
authenticated: true,
sessionId: 'test-session',
locale: 'es-ES',
user: {
userProperties: {
defaultLocale: 'fr-FR',
},
} as unknown as LoggedInUser,
};
setUserLanguage(session);
expect(document.documentElement).toHaveAttribute('lang', 'es-ES');
});
it('should convert underscores to hyphens in locale', () => {
const session: Session = {
authenticated: true,
sessionId: 'test-session',
locale: 'en_US',
user: {} as LoggedInUser,
};
setUserLanguage(session);
expect(document.documentElement).toHaveAttribute('lang', 'en-US');
});
it('should not set language if locale is invalid', () => {
const session: Session = {
authenticated: true,
sessionId: 'test-session',
locale: 'invalid-locale-xyz',
user: {} as LoggedInUser,
};
setUserLanguage(session);
expect(document.documentElement).not.toHaveAttribute('lang');
});
it('should not set language if locale is undefined', () => {
const session: Session = {
authenticated: true,
sessionId: 'test-session',
user: {} as LoggedInUser,
};
setUserLanguage(session);
expect(document.documentElement).not.toHaveAttribute('lang');
});
it('should not update if language is already set to the same value', () => {
document.documentElement.setAttribute('lang', 'en-US');
const session: Session = {
authenticated: true,
sessionId: 'test-session',
locale: 'en-US',
user: {} as LoggedInUser,
};
const spy = vi.spyOn(document.documentElement, 'setAttribute');
setUserLanguage(session);
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
});
describe('getLoggedInUser', () => {
beforeEach(() => {
// Reset session store
sessionStore.setState({ loaded: false, session: null });
});
it('should return logged in user when session is loaded', async () => {
const mockUser: LoggedInUser = {
uuid: 'user-uuid',
display: 'Test User',
username: 'testuser',
systemId: 'test-sys-id',
userProperties: {},
person: {} as any,
privileges: [],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
};
sessionStore.setState({
loaded: true,
session: {
authenticated: true,
sessionId: 'test-session',
user: mockUser,
},
});
const user = await getLoggedInUser();
expect(user).toEqual(mockUser);
});
it('should wait for session to load if not already loaded', async () => {
const mockUser: LoggedInUser = {
uuid: 'user-uuid',
display: 'Test User',
username: 'testuser',
systemId: 'test-sys-id',
userProperties: {},
person: {} as any,
privileges: [],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
};
const promise = getLoggedInUser();
// Simulate session loading after a delay
setTimeout(() => {
sessionStore.setState({
loaded: true,
session: {
authenticated: true,
sessionId: 'test-session',
user: mockUser,
},
});
}, 10);
const user = await promise;
expect(user).toEqual(mockUser);
});
});
describe('getCurrentUser', () => {
let subscriptions: Array<{ unsubscribe?: () => void }> = [];
beforeEach(() => {
sessionStore.setState({ loaded: false, session: null });
mockOpenmrsFetch.mockClear();
// Mock openmrsFetch to prevent unhandled promise rejections
mockOpenmrsFetch.mockResolvedValue(
createMockFetchResponse({
authenticated: false,
sessionId: '',
}),
);
});
afterEach(() => {
// Clean up any active subscriptions
subscriptions.forEach((sub) => sub.unsubscribe?.());
subscriptions = [];
});
it('should return an Observable', () => {
const result = getCurrentUser();
expect(result).toBeInstanceOf(Observable);
});
it('should emit user when session is loaded and includeAuthStatus is false', () => {
return new Promise<void>((resolve) => {
const mockUser: LoggedInUser = {
uuid: 'user-uuid',
display: 'Test User',
username: 'testuser',
systemId: 'test-sys-id',
userProperties: {},
person: {} as any,
privileges: [],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
};
sessionStore.setState({
loaded: true,
session: {
authenticated: true,
sessionId: 'test-session',
user: mockUser,
},
});
const sub = getCurrentUser({ includeAuthStatus: false }).subscribe((user) => {
expect(user).toEqual(mockUser);
resolve();
});
subscriptions.push(sub);
});
});
it('should emit full session when includeAuthStatus is true', () => {
return new Promise<void>((resolve) => {
const mockUser: LoggedInUser = {
uuid: 'user-uuid',
display: 'Test User',
username: 'testuser',
systemId: 'test-sys-id',
userProperties: {},
person: {} as any,
privileges: [],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
};
const mockSession: Session = {
authenticated: true,
sessionId: 'test-session',
user: mockUser,
};
sessionStore.setState({
loaded: true,
session: mockSession,
});
const sub = getCurrentUser({ includeAuthStatus: true }).subscribe((session) => {
expect(session).toEqual(mockSession);
resolve();
});
subscriptions.push(sub);
});
});
it('should not emit when session is not loaded', () => {
const handler = vi.fn();
const sub = getCurrentUser({ includeAuthStatus: false }).subscribe(handler);
subscriptions.push(sub);
expect(handler).not.toHaveBeenCalled();
});
it('should emit updates when session changes', () => {
return new Promise<void>((resolve) => {
const mockUser1: LoggedInUser = {
uuid: 'user-1',
display: 'User 1',
username: 'user1',
systemId: 'sys-1',
userProperties: {},
person: {} as any,
privileges: [],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
};
const mockUser2: LoggedInUser = {
uuid: 'user-2',
display: 'User 2',
username: 'user2',
systemId: 'sys-2',
userProperties: {},
person: {} as any,
privileges: [],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
};
const emittedUsers: LoggedInUser[] = [];
sessionStore.setState({
loaded: true,
session: {
authenticated: true,
sessionId: 'session-1',
user: mockUser1,
},
});
const sub = getCurrentUser({ includeAuthStatus: false }).subscribe((user) => {
emittedUsers.push(user);
if (emittedUsers.length === 2) {
expect(emittedUsers[0]).toEqual(mockUser1);
expect(emittedUsers[1]).toEqual(mockUser2);
resolve();
}
});
subscriptions.push(sub);
setTimeout(() => {
sessionStore.setState({
loaded: true,
session: {
authenticated: true,
sessionId: 'session-2',
user: mockUser2,
},
});
}, 10);
});
});
it('should allow unsubscribing', () => {
const handler = vi.fn();
const subscription = getCurrentUser({ includeAuthStatus: false }).subscribe(handler);
subscriptions.push(subscription);
sessionStore.setState({
loaded: true,
session: {
authenticated: true,
sessionId: 'test-session',
user: {} as LoggedInUser,
},
});
handler.mockClear();
subscription.unsubscribe();
sessionStore.setState({
loaded: true,
session: {
authenticated: false,
sessionId: 'new-session',
},
});
expect(handler).not.toHaveBeenCalled();
});
});
describe('refetchCurrentUser', () => {
beforeEach(() => {
sessionStore.setState({ loaded: false, session: null });
mockOpenmrsFetch.mockClear();
});
it('should fetch user without credentials', async () => {
const mockSession: Session = {
authenticated: true,
sessionId: 'test-session',
user: {} as LoggedInUser,
};
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
await refetchCurrentUser();
expect(mockOpenmrsFetch).toHaveBeenCalledWith(
expect.stringContaining('/session'),
expect.objectContaining({
headers: {},
}),
);
});
it('should use Basic Auth when username and password are provided', async () => {
const mockSession: Session = {
authenticated: true,
sessionId: 'test-session',
user: {} as LoggedInUser,
};
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
await refetchCurrentUser('testuser', 'testpass');
const expectedAuth = `Basic ${btoa('testuser:testpass')}`;
expect(mockOpenmrsFetch).toHaveBeenCalledWith(
expect.stringContaining('/session'),
expect.objectContaining({
headers: {
Authorization: expectedAuth,
},
}),
);
});
it('should update session store on success', async () => {
const mockUser: LoggedInUser = {
uuid: 'user-uuid',
display: 'Test User',
username: 'testuser',
systemId: 'test-sys-id',
userProperties: {},
person: {} as any,
privileges: [],
roles: [],
retired: false,
locale: 'en',
allowedLocales: ['en'],
};
const mockSession: Session = {
authenticated: true,
sessionId: 'test-session',
user: mockUser,
};
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
await refetchCurrentUser();
const state = sessionStore.getState();
expect(state.loaded).toBe(true);
expect(state.session).toEqual(mockSession);
});
it('should handle fetch failure', async () => {
mockOpenmrsFetch.mockRejectedValue(new Error('Network error'));
await expect(refetchCurrentUser()).rejects.toMatchObject({
loaded: false,
session: null,
});
expect(mockReportError).toHaveBeenCalled();
});
});
describe('setSessionLocation', () => {
beforeEach(() => {
mockOpenmrsFetch.mockClear();
});
it('should set session location with AbortController', async () => {
const locationUuid = 'location-uuid-123';
const abortController = new AbortController();
const mockSession: Session = {
authenticated: true,
sessionId: 'test-session',
sessionLocation: {
uuid: locationUuid,
} as any,
user: {} as LoggedInUser,
};
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
await setSessionLocation(locationUuid, abortController);
expect(mockOpenmrsFetch).toHaveBeenCalledWith(
expect.stringContaining('/session'),
expect.objectContaining({
method: 'POST',
body: { sessionLocation: locationUuid },
headers: {
'Content-Type': 'application/json',
},
signal: abortController.signal,
}),
);
});
it('should update session store with new location', async () => {
const locationUuid = 'location-uuid-123';
const abortController = new AbortController();
const mockSession: Session = {
authenticated: true,
sessionId: 'test-session',
sessionLocation: {
uuid: locationUuid,
display: 'Test Location',
} as any,
user: {} as LoggedInUser,
};
mockOpenmrsFetch.mockResolvedValue(createMockFetchResponse(mockSession));
await setSessionLocation(locationUuid, abortController);
const state = sessionStore.getState();
expect(state.loaded).toBe(true);
expect(state.session?.sessionLocation?.uuid).toBe(locationUuid);
});
});
describe('setUserProperties', () => {
beforeEach(() => {
mockOpenmrsFetch.mockClear();
});
it('should set user properties and refetch session', async () => {
const userUuid = 'user-uuid-123';
const userProperties = {
defaultLocale: 'en-US',
favoriteColor: 'blue',
};
mockOpenmrsFetch
.mockResolvedValueOnce(createMockFetchResponse({})) // First call to update properties
.mockResolvedValueOnce(
createMockFetchResponse({
// Second call to refetch session
authenticated: true,
sessionId: 'test-session',
user: {
uuid: userUuid,
userProperties,
} as unknown as LoggedInUser,
}),
);
await setUserProperties(userUuid, userProperties);
expect(mockOpenmrsFetch).toHaveBeenNthCalledWith(
1,
expect.stringContaining(`/user/${userUuid}`),
expect.objectContaining({
method: 'POST',
body: { userProperties },
headers: {
'Content-Type': 'application/json',
},
}),
);
// Should refetch session after updating
expect(mockOpenmrsFetch).toHaveBeenCalledTimes(2);
});
it('should use provided AbortController', async () => {
const userUuid = 'user-uuid-123';
const userProperties = { defaultLocale: 'fr-FR' };
const abortController = new AbortController();
mockOpenmrsFetch.mockResolvedValueOnce(createMockFetchResponse({})).mockResolvedValueOnce(
createMockFetchResponse({
authenticated: true,
sessionId: 'test-session',
user: {} as LoggedInUser,
}),
);
await setUserProperties(userUuid, userProperties, abortController);
expect(mockOpenmrsFetch).toHaveBeenNthCalledWith(
1,
expect.anything(),
expect.objectContaining({
signal: abortController.signal,
}),
);
});
it('should create AbortController if not provided', async () => {
const userUuid = 'user-uuid-123';
const userProperties = { defaultLocale: 'es-ES' };
mockOpenmrsFetch.mockResolvedValueOnce(createMockFetchResponse({})).mockResolvedValueOnce(
createMockFetchResponse({
authenticated: true,
sessionId: 'test-session',
user: {} as LoggedInUser,
}),
);
await setUserProperties(userUuid, userProperties);
expect(mockOpenmrsFetch).toHaveBeenNthCalledWith(
1,
expect.anything(),
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
);
});
});