import { Mock, mock } from '@alfa-client/test-utils'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { faker } from '@faker-js/faker'; import KcAdminClient, { NetworkError } from '@keycloak/keycloak-admin-client'; import { TokenProvider } from '@keycloak/keycloak-admin-client/lib/client'; import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; import { Groups } from '@keycloak/keycloak-admin-client/lib/resources/groups'; import { OAuthService } from 'angular-oauth2-oidc'; import { Observable, OperatorFunction, catchError, firstValueFrom, of, throwError } from 'rxjs'; import { createGroupRepresentation, createNetworkError, createOrganisationseinheit, createOrganisationseinheitError, } from '../../../test/user/user'; import { Organisationseinheit, OrganisationseinheitError, OrganisationseinheitErrorType, } from './user.model'; import { UserRepository } from './user.repository.service'; describe('UserRepository', () => { const accessToken: string = faker.random.alphaNumeric(40); const error: OrganisationseinheitError = createOrganisationseinheitError(); const networkError: NetworkError = createNetworkError(400, ''); let repository: UserRepository; let kcAdminClient: Mock<KcAdminClient>; let oAuthService: Mock<OAuthService>; const mockGroupsFunc = (groupsKey: keyof Groups, implementation: jest.Mock) => { kcAdminClient.groups = <any>{ [groupsKey]: implementation, }; }; beforeEach(() => { kcAdminClient = mock(KcAdminClient); oAuthService = mock(OAuthService); TestBed.configureTestingModule({ providers: [ { provide: OAuthService, useValue: oAuthService }, { provide: KcAdminClient, useValue: kcAdminClient }, ], }); oAuthService.getAccessToken.mockReturnValue(accessToken); repository = TestBed.inject(UserRepository); }); it('should be created', () => { expect(repository).toBeTruthy(); }); describe('registerTokenProvider', () => { it('should register token provider from oauth service', async () => { const tokenProvider: TokenProvider = kcAdminClient.registerTokenProvider.mock.calls[0][0]; const token: string = await tokenProvider.getAccessToken(); expect(token).toEqual(accessToken); }); }); describe('map organisationseinheit representation', () => { let expectedOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); it('should map field "id"', () => { const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({ id: expectedOrganisationseinheit.id, }); expect(organisationseinheit.id).toEqual(expectedOrganisationseinheit.id); }); it('should map field "name"', () => { const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({ name: expectedOrganisationseinheit.name, }); expect(organisationseinheit.name).toEqual(expectedOrganisationseinheit.name); }); it('should map field "organisationseinheitIds"', () => { const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({ attributes: { organisationseinheitId: expectedOrganisationseinheit.organisationseinheitIds, }, }); expect(organisationseinheit.organisationseinheitIds).toEqual( expectedOrganisationseinheit.organisationseinheitIds, ); }); it('should map missing organisationseinheitIds to empty list', () => { const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({}); expect(organisationseinheit.organisationseinheitIds).toEqual([]); }); }); describe('find organisationseinheitItems', () => { const organisationseinheitItems: Organisationseinheit[] = [ createOrganisationseinheit(), createOrganisationseinheit(), createOrganisationseinheit(), ]; const groupReps: GroupRepresentation[] = organisationseinheitItems.map(createGroupRepresentation); it('should return mapped organisationseinheit search result', async () => { const findMock: jest.Mock = jest.fn().mockReturnValue(Promise.resolve(groupReps)); mockGroupsFunc('find', findMock); const groupsResult: Organisationseinheit[] = await firstValueFrom( repository.findOrganisationseinheitItems(), ); expect(groupsResult).toEqual(groupsResult); }); it('should call with brief representation', fakeAsync(() => { const findMock: jest.Mock = jest.fn().mockReturnValue(Promise.resolve(groupReps)); mockGroupsFunc('find', findMock); repository.findOrganisationseinheitItems().subscribe(); tick(); expect(findMock).toHaveBeenCalledWith({ briefRepresentation: false }); })); }); describe('save organisationseinheit', () => { const saveGroup: Organisationseinheit = createOrganisationseinheit(); it('should call kcAdminClient.groups.save', async () => { const updateMock: jest.Mock = jest.fn(() => of(null)); mockGroupsFunc('update', updateMock); await firstValueFrom(repository.saveOrganisationseinheit(saveGroup)); expect(updateMock).toHaveBeenCalledWith( { id: saveGroup.id }, { name: saveGroup.name, attributes: { organisationseinheitId: saveGroup.organisationseinheitIds, }, }, ); }); it('should return organisationseinheit save observable', async () => { const updateMock: jest.Mock = jest.fn(() => Promise.resolve(null)); mockGroupsFunc('update', updateMock); const voidResult = await firstValueFrom(repository.saveOrganisationseinheit(saveGroup)); expect(voidResult).toBe(null); }); it('should pipe rethrowMappedGroupsError', (done) => { const updateMock: jest.Mock = jest.fn(() => Promise.reject(networkError)); mockGroupsFunc('update', updateMock); repository.rethrowMappedGroupsError = jest .fn() .mockReturnValue(catchError(() => throwError(() => error))); repository.saveOrganisationseinheit(saveGroup).subscribe({ error: (err) => { expect(err).toBe(error); done(); }, }); }); }); describe('create organisationseinheit', () => { const newOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); it('should call kcAdminClient.groups.create', async () => { const createMock: jest.Mock = jest.fn(() => of({ id: newOrganisationseinheit.id })); mockGroupsFunc('create', createMock); await firstValueFrom( repository.createOrganisationseinheit( newOrganisationseinheit.name, newOrganisationseinheit.organisationseinheitIds, ), ); expect(createMock).toHaveBeenCalledWith({ name: newOrganisationseinheit.name, attributes: { organisationseinheitId: newOrganisationseinheit.organisationseinheitIds, }, }); }); it('should return mapped organisationseinheit result', async () => { const createMock: jest.Mock = jest.fn(() => Promise.resolve({ id: newOrganisationseinheit.id }), ); mockGroupsFunc('create', createMock); const newGroupResult: Organisationseinheit = await firstValueFrom( repository.createOrganisationseinheit( newOrganisationseinheit.name, newOrganisationseinheit.organisationseinheitIds, ), ); expect(newGroupResult).toEqual(newOrganisationseinheit); }); it('should pipe rethrowMappedGroupsError', (done) => { const createMock: jest.Mock = jest.fn(() => Promise.reject(networkError)); mockGroupsFunc('create', createMock); repository.rethrowMappedGroupsError = jest .fn() .mockReturnValue(catchError(() => throwError(() => error))); repository .createOrganisationseinheit( newOrganisationseinheit.name, newOrganisationseinheit.organisationseinheitIds, ) .subscribe({ error: (err) => { expect(err).toBe(error); done(); }, }); }); }); describe('rethrow mapped groups error', () => { let networkErrorObservable: Observable<never>; beforeEach(() => { repository.mapCreateGroupsNetworkError = jest.fn().mockReturnValue(error); networkErrorObservable = throwError(() => networkError); }); it('should throw mapped error', (done) => { const rethrowOperator: OperatorFunction<never, never> = repository.rethrowMappedGroupsError(); networkErrorObservable.pipe(rethrowOperator).subscribe({ error: (err) => { expect(err).toBe(error); done(); }, }); }); it('should call mapCreateGroupsNetworkError', (done) => { const rethrowOperator: OperatorFunction<never, never> = repository.rethrowMappedGroupsError(); networkErrorObservable.pipe(rethrowOperator).subscribe({ error: () => { expect(repository.mapCreateGroupsNetworkError).toHaveBeenCalledWith(networkError); done(); }, }); }); }); describe('map create groups network error', () => { it('should interpret 409 status as name conflict', () => { const keycloakError: OrganisationseinheitError = createOrganisationseinheitError( OrganisationseinheitErrorType.NAME_CONFLICT, ); const networkError: NetworkError = createNetworkError(409, keycloakError.detail); const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError); expect(error).toEqual(keycloakError); }); it('should interpret 400 status as name missing', () => { const keycloakError: OrganisationseinheitError = createOrganisationseinheitError( OrganisationseinheitErrorType.NAME_MISSING, ); const networkError: NetworkError = createNetworkError(400, keycloakError.detail); const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError); expect(error).toEqual(keycloakError); }); it('should map missing errorMessage to empty string', () => { const networkError: NetworkError = createNetworkError(500, undefined); const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError); expect(error.detail).toEqual(''); }); }); describe('delete organisationseinheit', () => { const deleteOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); it('should call kcAdminClient.groups.del', async () => { const delMock: jest.Mock = jest.fn(() => Promise.resolve({ id: deleteOrganisationseinheit.id }), ); mockGroupsFunc('del', delMock); await firstValueFrom(repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id)); expect(delMock).toHaveBeenCalledWith({ id: deleteOrganisationseinheit.id, }); }); it('should return void', async () => { mockGroupsFunc( 'del', jest.fn(() => Promise.resolve(null)), ); const voidResult = await firstValueFrom( repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id), ); expect(voidResult).toBeNull(); }); }); });