Skip to content

Commit 84e056c

Browse files
thegreatyamoriAngeluz-07thegreatyamori
authored
feat: TT-155 consume user info (#650)
* feat: consume user info * feat: TT-155 register userEfecct in AppModule * feat: TT-155 create user module and model * test: TT-155 Added unit tests for user ngrx flow * feat: TT-155 create a service to verifyGroup in user * test: TT-155 modify test input data * test: TT-155 added unit tests to user-info.service * test: TT-155 deleted two extra lines in sidebar * fix: TT-155 resolve comments & fix user.selectors.spec Co-authored-by: roberto <[email protected]> Co-authored-by: thegreatyamori <[email protected]>
1 parent d8abfba commit 84e056c

17 files changed

+380
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ testem.log
4343
.keys.json
4444
keys.ts
4545
src/environments/keys.ts
46+
debug.log
4647

4748
# System Files
4849
.DS_Store

src/app/app.module.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ import { ProjectTypeListComponent } from './modules/customer-management/componen
5757
// tslint:disable-next-line: max-line-length
5858
import { CreateProjectTypeComponent } from './modules/customer-management/components/projects-type/components/create-project-type/create-project-type.component';
5959
import { CustomerEffects } from './modules/customer-management/store/customer-management.effects';
60-
import { UserEffects } from './modules/users/store/user.effects';
60+
import { UserEffects as UsersEffects } from './modules/users/store/user.effects';
61+
import { UserEffects } from './modules/user/store/user.effects';
6162
import { EntryEffects } from './modules/time-clock/store/entry.effects';
6263
import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.token.interceptor';
6364
import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substract-date.pipe';
@@ -74,7 +75,7 @@ import { LoadingBarComponent } from './modules/shared/components/loading-bar/loa
7475
import { UsersComponent } from './modules/users/pages/users.component';
7576
import { UsersListComponent } from './modules/users/components/users-list/users-list.component';
7677
import { UiSwitchModule } from 'ngx-ui-switch';
77-
import {NgxMaterialTimepickerModule} from 'ngx-material-timepicker';
78+
import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker';
7879
// tslint:disable-next-line: max-line-length
7980
import { TechnologyReportTableComponent } from './modules/technology-report/components/technology-report-table/technology-report-table.component';
8081
import { TechnologyReportComponent } from './modules/technology-report/pages/technology-report.component';
@@ -151,8 +152,8 @@ const maskConfig: Partial<IConfig> = {
151152
}),
152153
!environment.production
153154
? StoreDevtoolsModule.instrument({
154-
maxAge: 15, // Retains last 15 states
155-
})
155+
maxAge: 15, // Retains last 15 states
156+
})
156157
: [],
157158
EffectsModule.forRoot([
158159
ProjectEffects,
@@ -161,9 +162,10 @@ const maskConfig: Partial<IConfig> = {
161162
TechnologyEffects,
162163
ProjectTypeEffects,
163164
EntryEffects,
165+
UsersEffects,
164166
UserEffects,
165167
]),
166-
ToastrModule.forRoot()
168+
ToastrModule.forRoot(),
167169
],
168170
providers: [
169171
{
@@ -176,4 +178,4 @@ const maskConfig: Partial<IConfig> = {
176178
],
177179
bootstrap: [AppComponent],
178180
})
179-
export class AppModule { }
181+
export class AppModule {}

src/app/modules/login/services/azure.ad.b2c.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,8 @@ export class AzureAdB2CService {
8383
getUserGroup(): string {
8484
return this.msal.getAccount().idToken?.extension_role;
8585
}
86+
87+
getUserId(): string{
88+
return this.msal.getAccount().accountIdentifier;
89+
}
8690
}

src/app/modules/user/models/user.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface User {
2+
name: string;
3+
email: string;
4+
roles?: string[];
5+
groups?: string[];
6+
id: string;
7+
tenant_id?: string;
8+
deleted?: string;
9+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { MockStore, provideMockStore } from '@ngrx/store/testing';
3+
import { of } from 'rxjs';
4+
import { getUserGroups } from '../store/user.selectors';
5+
import { UserInfoService } from './user-info.service';
6+
7+
describe('UserInfoService', () => {
8+
let service: UserInfoService;
9+
let store: MockStore;
10+
let mockGetUserGroupsSelector: any;
11+
const initialState = {
12+
name: 'Unknown Name',
13+
14+
roles: [],
15+
groups: ['fake-admin', 'fake-tester'],
16+
id: 'dummy_id_load',
17+
tenant_id: 'dummy_tenant_id_load',
18+
deleted: '',
19+
};
20+
21+
beforeEach(() => {
22+
TestBed.configureTestingModule({
23+
providers: [provideMockStore({ initialState })],
24+
});
25+
service = TestBed.inject(UserInfoService);
26+
store = TestBed.inject(MockStore);
27+
mockGetUserGroupsSelector = store.overrideSelector(getUserGroups, initialState.groups);
28+
});
29+
30+
it('should be created', () => {
31+
expect(service).toBeTruthy();
32+
});
33+
34+
it('should call groups selector', () => {
35+
const expectedGroups = ['fake-admin', 'fake-tester'];
36+
37+
service.groups().subscribe((value) => {
38+
expect(value).toEqual(expectedGroups);
39+
});
40+
});
41+
42+
const params = [
43+
{ groupName: 'fake-admin', expectedValue: true, groups: ['fake-admin', 'fake-tester'] },
44+
{ groupName: 'fake-owner', expectedValue: false, groups: ['fake-admin', 'fake-tester'] },
45+
];
46+
47+
params.map((param) => {
48+
it(`given group ${param.groupName} and groups [${param.groups.toString()}], isMemberOf() should return ${
49+
param.expectedValue
50+
}`, () => {
51+
const groups$ = of(param.groups);
52+
53+
spyOn(service, 'groups').and.returnValue(groups$);
54+
55+
service.isMemberOf(param.groupName).subscribe((value) => {
56+
expect(value).toEqual(param.expectedValue);
57+
});
58+
});
59+
});
60+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Injectable } from '@angular/core';
2+
import { select, Store } from '@ngrx/store';
3+
import { Observable } from 'rxjs';
4+
import { map } from 'rxjs/operators';
5+
import { getUserGroups } from '../store/user.selectors';
6+
import { GROUPS } from '../../../../environments/environment';
7+
8+
@Injectable({
9+
providedIn: 'root',
10+
})
11+
export class UserInfoService {
12+
constructor(private store: Store) {}
13+
14+
groups(): Observable<string[]> {
15+
return this.store.pipe(select(getUserGroups));
16+
}
17+
18+
isMemberOf(groupName: string): Observable<boolean> {
19+
return this.groups().pipe(
20+
map((groups: string[]) => {
21+
return groups.includes(groupName);
22+
})
23+
);
24+
}
25+
26+
isAdmin(): Observable<boolean> {
27+
return this.isMemberOf(GROUPS.ADMIN);
28+
}
29+
30+
isTester(): Observable<boolean> {
31+
return this.isMemberOf(GROUPS.TESTER);
32+
}
33+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Injectable } from '@angular/core';
2+
import { HttpClient } from '@angular/common/http';
3+
import { Observable } from 'rxjs';
4+
import { environment } from 'src/environments/environment';
5+
import { User } from '../models/user';
6+
7+
@Injectable({
8+
providedIn: 'root',
9+
})
10+
export class UserService {
11+
12+
constructor(private http: HttpClient) {
13+
}
14+
15+
baseUrl = `${environment.timeTrackerApiUrl}/users`;
16+
17+
loadUser(userId: string): Observable<User> {
18+
return this.http.get<User>(`${this.baseUrl}/${userId}`);
19+
}
20+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { LoadUserFail, LoadUserSuccess, UserActionTypes } from './user.actions';
2+
3+
import { User } from '../models/user';
4+
5+
describe('Actions for User', () => {
6+
it('LoadUserSuccess type is UserActionTypes.LOAD_USER_SUCCESS', () => {
7+
const user: User = {
8+
name: 'Unknown Name',
9+
10+
roles: [],
11+
groups: [],
12+
id: 'dummy_id_load',
13+
tenant_id: 'dummy_tenant_id_load',
14+
deleted: ''
15+
};
16+
17+
const loadUserSuccess = new LoadUserSuccess(user);
18+
19+
expect(loadUserSuccess.type).toEqual(UserActionTypes.LOAD_USER_SUCCESS);
20+
});
21+
22+
it('LoadUserFail type is UserActionTypes.LOAD_USER_FAIL', () => {
23+
const loadUserFail = new LoadUserFail('error');
24+
25+
expect(loadUserFail.type).toEqual(UserActionTypes.LOAD_USER_FAIL);
26+
});
27+
28+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Action } from '@ngrx/store';
2+
import { User } from '../models/user';
3+
4+
export enum UserActionTypes {
5+
LOAD_USER = '[User] LOAD_USER',
6+
LOAD_USER_SUCCESS = '[User] LOAD_USER_SUCCESS',
7+
LOAD_USER_FAIL = '[User] LOAD_USER_FAIL',
8+
}
9+
10+
export class LoadUser implements Action {
11+
public readonly type = UserActionTypes.LOAD_USER;
12+
constructor(readonly userId: string) {}
13+
}
14+
15+
export class LoadUserSuccess implements Action {
16+
public readonly type = UserActionTypes.LOAD_USER_SUCCESS;
17+
18+
constructor(readonly payload: User) {}
19+
}
20+
21+
export class LoadUserFail implements Action {
22+
public readonly type = UserActionTypes.LOAD_USER_FAIL;
23+
24+
constructor(public error: string) {}
25+
}
26+
27+
export type UserActions = LoadUser | LoadUserSuccess | LoadUserFail;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Observable, of, throwError } from 'rxjs';
2+
import { Action } from '@ngrx/store';
3+
import { User } from '../models/user';
4+
import { UserEffects } from './user.effects';
5+
import { TestBed } from '@angular/core/testing';
6+
import { UserService } from '../services/user.service';
7+
import { provideMockActions } from '@ngrx/effects/testing';
8+
import { HttpClientTestingModule } from '@angular/common/http/testing';
9+
import { LoadUser, UserActionTypes } from './user.actions';
10+
11+
describe('UserEffects', () => {
12+
let actions$: Observable<Action>;
13+
let effects: UserEffects;
14+
let service: UserService;
15+
const userInfo: User = {
16+
name: 'Unknown Name',
17+
18+
roles: [],
19+
groups: [],
20+
id: 'dummy_tenant_id_load',
21+
tenant_id: null,
22+
deleted: null
23+
};
24+
25+
beforeEach(() => {
26+
TestBed.configureTestingModule({
27+
providers: [UserEffects, provideMockActions(() => actions$)],
28+
imports: [HttpClientTestingModule],
29+
});
30+
31+
effects = TestBed.inject(UserEffects);
32+
service = TestBed.inject(UserService);
33+
});
34+
35+
it('should be created', async () => {
36+
expect(effects).toBeTruthy();
37+
});
38+
39+
it('action type is LOAD_USER_SUCCESS when service is executed successfully', async () => {
40+
const userId = 'dummy_id_load';
41+
const serviceSpy = spyOn(service, 'loadUser');
42+
43+
actions$ = of(new LoadUser(userId));
44+
serviceSpy.and.returnValue(of(userInfo));
45+
46+
effects.loadUserInfo$.subscribe((action) => {
47+
expect(action.type).toEqual(UserActionTypes.LOAD_USER_SUCCESS);
48+
});
49+
});
50+
51+
it('action type is LOAD_USER_FAIL when service fail in execution', async () => {
52+
const userId = 'dummy_id_load';
53+
const serviceSpy = spyOn(service, 'loadUser');
54+
55+
actions$ = of(new LoadUser(userId));
56+
serviceSpy.and.returnValue(throwError({ error: { message: 'fail!' } }));
57+
58+
effects.loadUserInfo$.subscribe((action) => {
59+
expect(action.type).toEqual(UserActionTypes.LOAD_USER_FAIL);
60+
});
61+
});
62+
});

0 commit comments

Comments
 (0)