Skip to content

Commit 647de5a

Browse files
feat: TT-190 user user-info service to check if user is admin in UI (#658)
* feat: TT-190 add store persistence & switch-group FT to preserve old logic * refactor: TT-189 refactor some names
1 parent 4a40635 commit 647de5a

File tree

14 files changed

+296
-71
lines changed

14 files changed

+296
-71
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"minimist": "^1.2.5",
4343
"moment": "^2.25.3",
4444
"msal": "^1.2.1",
45+
"ngrx-store-localstorage": "^11.0.0",
4546
"ngx-cookie-service": "^11.0.2",
4647
"ngx-mask": "^9.1.2",
4748
"ngx-material-timepicker": "^5.5.3",
@@ -88,7 +89,7 @@
8889
"popper.js": "^1.16.0",
8990
"prettier": "^2.0.2",
9091
"protractor": "^7.0.0",
91-
"semantic-release": "^17.3.0",
92+
"semantic-release": "^17.4.2",
9293
"ts-node": "~8.3.0",
9394
"tslint": "~6.1.0",
9495
"typescript": "4.0.5"

src/app/app-routing.module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AdminGuard } from './guards/admin-guard/admin-guard';
1+
import { AdminGuard } from './guards/admin-guard/admin.guard';
22
import { NgModule } from '@angular/core';
33
import { Routes, RouterModule } from '@angular/router';
44

@@ -26,7 +26,7 @@ const routes: Routes = [
2626
{ path: 'activities-management', component: ActivitiesManagementComponent },
2727
{ path: 'customers-management', canActivate: [AdminGuard], component: CustomerComponent },
2828
{ path: 'users', canActivate: [AdminGuard], component: UsersComponent },
29-
{ path: 'technology-report', canActivate: [AdminGuard, TechnologiesReportGuard], component: TechnologyReportComponent},
29+
{ path: 'technology-report', canActivate: [AdminGuard, TechnologiesReportGuard], component: TechnologyReportComponent },
3030
{ path: '', pathMatch: 'full', redirectTo: 'time-clock' },
3131
],
3232
},
@@ -37,4 +37,4 @@ const routes: Routes = [
3737
imports: [RouterModule.forRoot(routes)],
3838
exports: [RouterModule],
3939
})
40-
export class AppRoutingModule {}
40+
export class AppRoutingModule { }

src/app/guards/admin-guard/admin-guard.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,129 @@
11
import { inject, TestBed } from '@angular/core/testing';
22
import { Router } from '@angular/router';
33
import { RouterTestingModule } from '@angular/router/testing';
4-
4+
import { of } from 'rxjs';
5+
import { skip, take } from 'rxjs/operators';
6+
import { FeatureSwitchGroupService } from 'src/app/modules/shared/feature-toggles/switch-group/feature-switch-group.service';
7+
import { UserInfoService } from 'src/app/modules/user/services/user-info.service';
58
import { AzureAdB2CService } from '../../modules/login/services/azure.ad.b2c.service';
6-
import { AdminGuard } from './admin-guard';
9+
import { AdminGuard } from './admin.guard';
710

811
describe('AdminGuard', () => {
9-
1012
let adminGuard: AdminGuard;
1113
let azureAdB2CService: AzureAdB2CService;
12-
14+
let userInfoService: UserInfoService;
15+
let featureSwitchGroupService: FeatureSwitchGroupService;
1316
const azureAdB2CServiceStub = {
1417
isLogin() {
1518
return true;
1619
},
1720
isAdmin() {
1821
return true;
19-
}
22+
},
23+
};
24+
25+
const userInfoServiceStub = {
26+
isAdmin: () => of(false),
27+
};
28+
29+
const featureSwitchGroupServiceStub = {
30+
isActivated: () => of(false),
2031
};
2132

2233
beforeEach(() => {
2334
TestBed.configureTestingModule({
2435
imports: [RouterTestingModule],
2536
providers: [
26-
{ providers: AzureAdB2CService, useValue: azureAdB2CServiceStub },
27-
]
37+
{ provide: AzureAdB2CService, useValue: azureAdB2CServiceStub },
38+
{ provide: UserInfoService, useValue: userInfoServiceStub },
39+
{ provide: FeatureSwitchGroupService, useValue: featureSwitchGroupServiceStub },
40+
],
2841
});
2942
adminGuard = TestBed.inject(AdminGuard);
3043
azureAdB2CService = TestBed.inject(AzureAdB2CService);
44+
userInfoService = TestBed.inject(UserInfoService);
45+
featureSwitchGroupService = TestBed.inject(FeatureSwitchGroupService);
3146
});
3247

3348
it('should be created', () => {
3449
expect(adminGuard).toBeTruthy();
3550
});
3651

37-
it('can activate the route when user is logged-in', () => {
38-
spyOn(azureAdB2CService, 'isAdmin').and.returnValue(true);
52+
const roleParams = [{ bool: false }, { bool: true }];
53+
roleParams.map((param) => {
54+
it(`isAdminBasedInRole return ${param.bool}`, () => {
55+
spyOn(azureAdB2CService, 'isAdmin').and.returnValue(param.bool);
56+
57+
adminGuard.isAdminBasedInRole().subscribe((enabled) => {
58+
expect(azureAdB2CService.isAdmin).toHaveBeenCalled();
59+
expect(enabled).toBe(param.bool);
60+
});
61+
});
62+
});
63+
64+
const groupParams = [{ bool: false }, { bool: true }];
65+
groupParams.map((param) => {
66+
it(`isAdminBasedInGroup return ${param.bool}`, () => {
67+
spyOn(userInfoService, 'isAdmin').and.returnValue(of(param.bool));
68+
69+
adminGuard.isAdminBasedInGroup().subscribe((enabled) => {
70+
expect(userInfoService.isAdmin).toHaveBeenCalled();
71+
expect(enabled).toBe(param.bool);
72+
});
73+
});
74+
});
75+
76+
const switchToggleParams = [
77+
{ switchGroup: false, chosen: 'isAdminBasedInRole', isAdmin: true },
78+
{ switchGroup: true, chosen: 'isAdminBasedInGroup', isAdmin: false },
79+
];
80+
switchToggleParams.map((param) => {
81+
it(`on switchGroup ${param.switchGroup}, ${param.chosen} should be chosen`, () => {
82+
const switchGroup$ = of(param.switchGroup);
83+
84+
spyOn(featureSwitchGroupService, 'isActivated').and.returnValue(switchGroup$);
85+
86+
const canActivate = adminGuard.canActivate();
3987

40-
const canActivate = adminGuard.canActivate();
88+
featureSwitchGroupService.isActivated().pipe(take(1));
4189

42-
expect(azureAdB2CService.isAdmin).toHaveBeenCalled();
43-
expect(canActivate).toEqual(true);
90+
canActivate.subscribe((enabled) => {
91+
expect(featureSwitchGroupService.isActivated).toHaveBeenCalled();
92+
expect(enabled).toBe(param.isAdmin);
93+
});
94+
});
4495
});
4596

46-
it('can not active the route and is redirected to login if user is not logged-in', inject([Router], (router: Router) => {
47-
spyOn(azureAdB2CService, 'isAdmin').and.returnValue(false);
48-
spyOn(router, 'navigate').and.stub();
97+
const navigateParams = [
98+
{ switchGroup: false, chosen: 'activate the route', isAdmin: true },
99+
{ switchGroup: false, chosen: 'redirect to /login', isAdmin: false },
100+
{ switchGroup: true, chosen: 'activate the route', isAdmin: true },
101+
{ switchGroup: true, chosen: 'redirect to /login', isAdmin: false },
102+
];
103+
navigateParams.map((param) => {
104+
it(`on isAdmin: ${param.isAdmin} with toggleSwitch: ${param.switchGroup}, should ${param.chosen} `, inject(
105+
[Router],
106+
(router: Router) => {
107+
const switchGroup$ = of(param.switchGroup);
108+
const isAdmin$ = of(param.isAdmin);
49109

50-
const canActivate = adminGuard.canActivate();
110+
spyOn(featureSwitchGroupService, 'isActivated').and.returnValue(switchGroup$);
111+
spyOn(adminGuard, 'isAdminBasedInRole').and.returnValue(isAdmin$);
112+
spyOn(adminGuard, 'isAdminBasedInGroup').and.returnValue(isAdmin$);
113+
spyOn(router, 'navigate').and.stub();
51114

52-
expect(azureAdB2CService.isAdmin).toHaveBeenCalled();
53-
expect(canActivate).toEqual(false);
54-
expect(router.navigate).toHaveBeenCalledWith(['login']);
55-
}));
115+
const canActivate = adminGuard.canActivate();
56116

117+
canActivate.subscribe((enabled) => {
118+
expect(featureSwitchGroupService.isActivated).toHaveBeenCalled();
119+
if (!enabled) {
120+
expect(router.navigate).toHaveBeenCalledWith(['login']);
121+
} else {
122+
expect(router.navigate).not.toHaveBeenCalled();
123+
expect(enabled).toBeTrue();
124+
}
125+
});
126+
}
127+
));
128+
});
57129
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Injectable } from '@angular/core';
2+
import { Router, CanActivate } from '@angular/router';
3+
import { Observable, of } from 'rxjs';
4+
import { map, mergeMap } from 'rxjs/operators';
5+
import { FeatureSwitchGroupService } from 'src/app/modules/shared/feature-toggles/switch-group/feature-switch-group.service';
6+
import { UserInfoService } from 'src/app/modules/user/services/user-info.service';
7+
import { AzureAdB2CService } from '../../modules/login/services/azure.ad.b2c.service';
8+
9+
@Injectable({
10+
providedIn: 'root',
11+
})
12+
export class AdminGuard implements CanActivate {
13+
constructor(
14+
private azureAdB2CService: AzureAdB2CService,
15+
private router: Router,
16+
private userInfoService: UserInfoService,
17+
private featureSwitchGroup: FeatureSwitchGroupService
18+
) {}
19+
20+
canActivate(): Observable<boolean> {
21+
return this.featureSwitchGroup.isActivated().pipe(
22+
mergeMap((enabled: boolean) => {
23+
return enabled ? this.isAdminBasedInGroup() : this.isAdminBasedInRole();
24+
}),
25+
map((isAdmin: boolean): boolean => {
26+
if (!isAdmin) {
27+
this.router.navigate(['login']);
28+
}
29+
return isAdmin;
30+
})
31+
);
32+
}
33+
34+
isAdminBasedInRole(): Observable<boolean> {
35+
return of(this.azureAdB2CService.isAdmin());
36+
}
37+
38+
isAdminBasedInGroup(): Observable<boolean> {
39+
return this.userInfoService.isAdmin();
40+
}
41+
}
Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,79 @@
11
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
2-
2+
import { MockStore, provideMockStore } from '@ngrx/store/testing';
3+
import { of } from 'rxjs';
4+
import { AzureAdB2CService } from '../login/services/azure.ad.b2c.service';
5+
import { FeatureSwitchGroupService } from '../shared/feature-toggles/switch-group/feature-switch-group.service';
6+
import { LoadUser } from '../user/store/user.actions';
37
import { HomeComponent } from './home.component';
48

59
describe('HomeComponent', () => {
610
let component: HomeComponent;
11+
let azureAdB2CService: AzureAdB2CService;
12+
let featureSwitchGroupService: FeatureSwitchGroupService;
13+
let store: MockStore;
714
let fixture: ComponentFixture<HomeComponent>;
15+
const initialState = {};
16+
const azureB2CServiceStub = {
17+
getUserId: () => 'user_id',
18+
};
19+
const featureSwitchGroupServiceStub = {
20+
isActivated: () => of(false),
21+
};
822

9-
beforeEach(waitForAsync(() => {
10-
TestBed.configureTestingModule({
11-
declarations: [ HomeComponent ]
23+
beforeEach(
24+
waitForAsync(() => {
25+
TestBed.configureTestingModule({
26+
declarations: [HomeComponent],
27+
providers: [
28+
provideMockStore({ initialState }),
29+
{ provide: AzureAdB2CService, useValue: azureB2CServiceStub },
30+
{ provide: FeatureSwitchGroupService, useValue: featureSwitchGroupServiceStub },
31+
],
32+
}).compileComponents();
1233
})
13-
.compileComponents();
14-
}));
34+
);
1535

1636
beforeEach(() => {
1737
fixture = TestBed.createComponent(HomeComponent);
38+
azureAdB2CService = TestBed.inject(AzureAdB2CService);
39+
featureSwitchGroupService = TestBed.inject(FeatureSwitchGroupService);
40+
store = TestBed.inject(MockStore);
41+
1842
component = fixture.componentInstance;
1943
fixture.detectChanges();
44+
store.setState(initialState);
2045
});
2146

2247
it('should be created', () => {
2348
expect(component).toBeTruthy();
2449
});
50+
51+
it('onInit, if featureSwitchGroup is true LoadUser action is dispatched', () => {
52+
const userId = 'user_id';
53+
spyOn(featureSwitchGroupService, 'isActivated').and.returnValue(of(true));
54+
spyOn(azureAdB2CService, 'getUserId').and.returnValue(userId);
55+
spyOn(store, 'dispatch');
56+
57+
component.ngOnInit();
58+
59+
featureSwitchGroupService.isActivated().subscribe(() => {
60+
expect(featureSwitchGroupService.isActivated).toHaveBeenCalled();
61+
expect(azureAdB2CService.getUserId).toHaveBeenCalled();
62+
expect(store.dispatch).toHaveBeenCalledWith(new LoadUser(userId));
63+
});
64+
});
65+
66+
it('onInit, if featureSwitchGroup is false nothing happens', () => {
67+
spyOn(featureSwitchGroupService, 'isActivated').and.returnValue(of(false));
68+
spyOn(azureAdB2CService, 'getUserId');
69+
spyOn(store, 'dispatch');
70+
71+
component.ngOnInit();
72+
73+
featureSwitchGroupService.isActivated().subscribe(() => {
74+
expect(featureSwitchGroupService.isActivated).toHaveBeenCalled();
75+
expect(azureAdB2CService.getUserId).not.toHaveBeenCalled();
76+
expect(store.dispatch).not.toHaveBeenCalled();
77+
});
78+
});
2579
});
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component, OnDestroy, OnInit } from '@angular/core';
2+
import { Store } from '@ngrx/store';
3+
import { Subscription } from 'rxjs';
4+
import { LoadUser } from 'src/app/modules/user/store/user.actions';
5+
import { AzureAdB2CService } from '../login/services/azure.ad.b2c.service';
6+
import { FeatureSwitchGroupService } from '../shared/feature-toggles/switch-group/feature-switch-group.service';
27

38
@Component({
49
selector: 'app-home',
510
templateUrl: './home.component.html',
6-
styleUrls: ['./home.component.scss']
11+
styleUrls: ['./home.component.scss'],
712
})
8-
export class HomeComponent implements OnInit {
13+
export class HomeComponent implements OnInit, OnDestroy {
14+
FTSwitchGroup$: Subscription;
915

10-
constructor() { }
16+
constructor(
17+
private featureSwitchGroup: FeatureSwitchGroupService,
18+
private azureAdB2CService: AzureAdB2CService,
19+
private store: Store
20+
) {}
1121

1222
ngOnInit(): void {
23+
this.FTSwitchGroup$ = this.featureSwitchGroup.isActivated().subscribe((enabled) => {
24+
if (enabled) {
25+
const userId = this.azureAdB2CService.getUserId();
26+
this.store.dispatch(new LoadUser(userId));
27+
}
28+
});
1329
}
1430

31+
ngOnDestroy() {
32+
this.FTSwitchGroup$.unsubscribe();
33+
}
1534
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ describe('AzureAdB2CService', () => {
4242
expect(UserAgentApplication.prototype.loginPopup).toHaveBeenCalled();
4343
});
4444

45-
it('on logout should call msal logout', () => {
45+
it('on logout should call msal logout and verify if user localStorage is removed', () => {
4646
spyOn(UserAgentApplication.prototype, 'logout').and.returnValue();
47+
spyOn(localStorage, 'removeItem').withArgs('user');
4748
service.logout();
49+
50+
expect(localStorage.removeItem).toHaveBeenCalledWith('user');
4851
expect(UserAgentApplication.prototype.logout).toHaveBeenCalled();
4952
});
5053

@@ -66,7 +69,7 @@ describe('AzureAdB2CService', () => {
6669
});
6770

6871
it('isAdmin when extension_role === time-tracker-admin', async () => {
69-
const adminAccount = {...account};
72+
const adminAccount = { ...account };
7073
adminAccount.idToken.extension_role = 'time-tracker-admin';
7174

7275
spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValue(adminAccount);

0 commit comments

Comments
 (0)