Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VIH-9761 use observables in justice users service #1155

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { of } from 'rxjs';
import { TestBed } from '@angular/core/testing';
import { combineLatest, of } from 'rxjs';
import { count, delay, switchMap, take } from 'rxjs/operators';
import {
AddNewJusticeUserRequest,
BHClient,
Expand All @@ -10,10 +11,12 @@ import {
} from './clients/api-client';

import { JusticeUsersService } from './justice-users.service';
import { Logger } from './logger';

describe('JusticeUsersService', () => {
let service: JusticeUsersService;
let clientApiSpy: jasmine.SpyObj<BHClient>;
const loggerSpy = jasmine.createSpyObj<Logger>('Logger', ['error', 'debug', 'warn']);

beforeEach(() => {
clientApiSpy = jasmine.createSpyObj<BHClient>([
Expand All @@ -24,53 +27,115 @@ describe('JusticeUsersService', () => {
'restoreJusticeUser'
]);

TestBed.configureTestingModule({ providers: [{ provide: BHClient, useValue: clientApiSpy }] });
TestBed.configureTestingModule({
providers: [
{ provide: BHClient, useValue: clientApiSpy },
{ provide: Logger, useValue: loggerSpy }
]
});
service = TestBed.inject(JusticeUsersService);
});

describe('retrieveJusticeUserAccounts', () => {
it('should call api when retrieving justice user accounts', (done: DoneFn) => {
describe('`users$` observable', () => {
it('should emit users returned from api', (done: DoneFn) => {
const users: JusticeUserResponse[] = [
new JusticeUserResponse({ id: '123', contact_email: '[email protected]' }),
new JusticeUserResponse({ id: '456', contact_email: '[email protected]' }),
new JusticeUserResponse({ id: '789', contact_email: '[email protected]' })
];
clientApiSpy.getUserList.and.returnValue(of(users));
service.retrieveJusticeUserAccounts().subscribe(result => {
service.allUsers$.subscribe(result => {
expect(result).toEqual(users);
done();
});
});

it('should not call api when retrieving justice user accounts and users have been already been cached', (done: DoneFn) => {
const users: JusticeUserResponse[] = [
new JusticeUserResponse({ id: '123', contact_email: '[email protected]' }),
new JusticeUserResponse({ id: '456', contact_email: '[email protected]' }),
new JusticeUserResponse({ id: '789', contact_email: '[email protected]' })
];
service['cache$'] = of(users);
clientApiSpy.getUserList.and.returnValue(of(users));
service.retrieveJusticeUserAccounts().subscribe(result => {
expect(result).toEqual(users);
expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(0);
it('should not make additional api requests when additional observers subscribe', (done: DoneFn) => {
clientApiSpy.getUserList.and.returnValue(of([]));

combineLatest([service.allUsers$, service.allUsers$])
.pipe(
delay(1000),
switchMap(x => service.allUsers$)
)
.subscribe(() => {
expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(1);
done();
});
});
});

describe('search()', () => {
it('should trigger an emission from $users observable each time it is called', (done: DoneFn) => {
// arrange
clientApiSpy.getUserList.and.returnValue(of([]));

// after calling search() two times, we should see 2 emissions from users$
service.filteredUsers$.pipe(take(2), count()).subscribe(c => {
// assert
expect(c).toBe(2);
done();
});

// act
service.search('');
service.search('');
});

it('should call api and return user list', (done: DoneFn) => {
const users: JusticeUserResponse[] = [new JusticeUserResponse({ id: '123', contact_email: '[email protected]' })];
const term = 'user1';
clientApiSpy.getUserList.and.returnValue(of(users));
service.retrieveJusticeUserAccountsNoCache(term).subscribe(result => {
expect(result).toEqual(users);
expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(1);
it('should apply a filter to the users collection', () => {
// arrange
const users: JusticeUserResponse[] = [
new JusticeUserResponse({
id: '123',
contact_email: '[email protected]',
first_name: 'Test',
lastname: 'Test',
username: 'Test'
}),
new JusticeUserResponse({
id: '456',
contact_email: '[email protected]',
first_name: 'Another',
lastname: 'Another',
username: 'Another'
}),
new JusticeUserResponse({
id: '789',
contact_email: '[email protected]',
first_name: 'Last',
lastname: 'Last',
username: 'Last'
})
];

// act
const filteredUsers = service.applyFilter('Test', users);

// assert
expect(filteredUsers[0].first_name).toBe('Test');
});
});

describe('refresh()', () => {
it('should trigger another emission from $users observable', (done: DoneFn) => {
// arrange
clientApiSpy.getUserList.and.returnValue(of([]));

// users$ will emit initially - after calling refresh() two more times, we should see 3 emissions from users$
service.allUsers$.pipe(take(3), count()).subscribe(c => {
// assert
expect(c).toBe(3);
done();
});

// act
service.refresh();
service.refresh();
});
});

describe('addNewJusticeUser', () => {
it('should call the api to save a new user', fakeAsync(() => {
it('should call the api to save a new user & again to get the users list', (done: DoneFn) => {
const username = '[email protected]';
const firstName = 'john';
const lastName = 'doe';
Expand All @@ -88,24 +153,29 @@ describe('JusticeUsersService', () => {
});

clientApiSpy.addNewJusticeUser.and.returnValue(of(newUser));
let result: JusticeUserResponse;
clientApiSpy.getUserList.and.returnValue(of([]));

service.addNewJusticeUser(username, firstName, lastName, telephone, role).subscribe(data => (result = data));
tick();
const request = new AddNewJusticeUserRequest({
username: username,
first_name: firstName,
last_name: lastName,
contact_telephone: telephone,
role: role
});
expect(result).toEqual(newUser);
expect(clientApiSpy.addNewJusticeUser).toHaveBeenCalledWith(request);
}));

combineLatest([service.allUsers$, service.addNewJusticeUser(username, firstName, lastName, telephone, role)]).subscribe(
([_, userResponse]: [JusticeUserResponse[], JusticeUserResponse]) => {
expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(2);
expect(userResponse).toEqual(newUser);
expect(clientApiSpy.addNewJusticeUser).toHaveBeenCalledWith(request);
done();
}
);
});
});

describe('editJusticeUser', () => {
it('should call the api to edit an existing user', fakeAsync(() => {
it('should call the api to edit an existing user & again to get the users list', (done: DoneFn) => {
const id = '123';
const username = '[email protected]';
const firstName = 'john';
Expand All @@ -123,26 +193,36 @@ describe('JusticeUsersService', () => {
});

clientApiSpy.editJusticeUser.and.returnValue(of(existingUser));
let result: JusticeUserResponse;
clientApiSpy.getUserList.and.returnValue(of([]));

service.editJusticeUser(id, username, role).subscribe(data => (result = data));
tick();
const request = new EditJusticeUserRequest({
id,
username,
role
});
expect(result).toEqual(existingUser);
expect(clientApiSpy.editJusticeUser).toHaveBeenCalledWith(request);
}));

combineLatest([service.allUsers$, service.editJusticeUser(id, username, role)]).subscribe(
([_, result]: [JusticeUserResponse[], JusticeUserResponse]) => {
expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(2);
expect(result).toEqual(existingUser);
expect(clientApiSpy.editJusticeUser).toHaveBeenCalledWith(request);
done();
}
);
});
});

describe('deleteJusticeUser', () => {
it('should call the api to delete the user', () => {
it('should call the api to delete the user & again to get the users list', (done: DoneFn) => {
clientApiSpy.deleteJusticeUser.and.returnValue(of(''));
clientApiSpy.getUserList.and.returnValue(of([]));

const id = '123';
service.deleteJusticeUser(id).subscribe();
expect(clientApiSpy.deleteJusticeUser).toHaveBeenCalledWith(id);
combineLatest([service.allUsers$, service.deleteJusticeUser(id)]).subscribe(() => {
expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(2);
expect(clientApiSpy.deleteJusticeUser).toHaveBeenCalledWith(id);
done();
});
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { BehaviorSubject, throwError } from 'rxjs';
import { catchError, filter, map, mergeMap, shareReplay, skip, switchMap, tap } from 'rxjs/operators';
import {
AddNewJusticeUserRequest,
BHClient,
Expand All @@ -10,28 +10,50 @@ import {
RestoreJusticeUserRequest
} from './clients/api-client';
import { cleanQuery } from '../common/helpers/api-helper';
import { Logger } from './logger';

@Injectable({
providedIn: 'root'
})
export class JusticeUsersService {
private cache$: Observable<JusticeUserResponse[]>;
loggerPrefix = '[JusticeUsersService] -';
private refresh$: BehaviorSubject<void> = new BehaviorSubject(null);
private searchTerm$: BehaviorSubject<string> = new BehaviorSubject(null);

constructor(private apiClient: BHClient) {}
retrieveJusticeUserAccounts() {
if (!this.cache$) {
this.cache$ = this.requestJusticeUsers(null).pipe(shareReplay(1));
}
allUsers$ = this.refresh$.pipe(
mergeMap(() => this.getJusticeUsers(null)),
shareReplay(1)
);

filteredUsers$ = this.allUsers$.pipe(
switchMap(users =>
this.searchTerm$.pipe(
filter(searchTerm => searchTerm !== null),
map(term => this.applyFilter(term, users))
)
)
);

return this.cache$;
constructor(private apiClient: BHClient, private logger: Logger) {}

refresh() {
this.refresh$.next();
}

retrieveJusticeUserAccountsNoCache(term: string) {
return this.requestJusticeUsers(term).pipe(shareReplay(1));
search(searchTerm: string) {
this.searchTerm$.next(searchTerm);
}

private requestJusticeUsers(term: string) {
return this.apiClient.getUserList(cleanQuery(term));
applyFilter(searchTerm: string, users: JusticeUserResponse[]): JusticeUserResponse[] {
if (!searchTerm) {
return users;
}

return users.filter(user =>
[user.first_name, user.lastname, user.contact_email, user.username].some(field =>
field.toLowerCase().includes(searchTerm.toLowerCase())
)
);
}

addNewJusticeUser(username: string, firstName: string, lastName: string, telephone: string, role: JusticeUserRole) {
Expand All @@ -42,7 +64,7 @@ export class JusticeUsersService {
contact_telephone: telephone,
role: role
});
return this.apiClient.addNewJusticeUser(request);
return this.apiClient.addNewJusticeUser(request).pipe(tap(() => this.refresh$.next()));
}

editJusticeUser(id: string, username: string, role: JusticeUserRole) {
Expand All @@ -51,18 +73,27 @@ export class JusticeUsersService {
username,
role
});
return this.apiClient.editJusticeUser(request);
return this.apiClient.editJusticeUser(request).pipe(tap(() => this.refresh$.next()));
}

deleteJusticeUser(id: string) {
return this.apiClient.deleteJusticeUser(id);
return this.apiClient.deleteJusticeUser(id).pipe(tap(() => this.refresh$.next()));
}

restoreJusticeUser(id: string, username: string) {
const request = new RestoreJusticeUserRequest({
username,
id
});
return this.apiClient.restoreJusticeUser(request);
return this.apiClient.restoreJusticeUser(request).pipe(tap(() => this.refresh$.next()));
}

private getJusticeUsers(term: string) {
return this.apiClient.getUserList(cleanQuery(term)).pipe(
catchError(error => {
this.logger.error(`${this.loggerPrefix} There was an unexpected error getting justice users`, new Error(error));
return throwError(error);
})
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
id="user-list"
class="custom"
labelForId="users"
[items]="users"
[items]="users$ | async"
bindLabel="full_name"
bindValue="id"
formControlName="selectedUserIds"
Expand Down
Loading