diff --git a/angular.json b/angular.json index 5add0a5ba..4988f5368 100644 --- a/angular.json +++ b/angular.json @@ -29,6 +29,7 @@ "./node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.scss", "node_modules/ngx-toastr/toastr.css", + "./node_modules/ngx-ui-switch/ui-switch.component.css", "node_modules/datatables.net-buttons-dt/css/buttons.dataTables.css" ], "scripts": [ diff --git a/package-lock.json b/package-lock.json index 7b895bb5c..5abb2b1c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6077,15 +6077,6 @@ "p-try": "^2.0.0" } }, - "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "ssri": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", @@ -11068,6 +11059,11 @@ "tslib": "^1.10.0" } }, + "ngx-ui-switch": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-10.0.2.tgz", + "integrity": "sha512-T8LhlRnjm36kElVNxkzEmpiXA82FO74FGJHpsW60U6CFXjWqlEiQAXuCA8c/qiWDxHqy36KkaxY8/cutv487RA==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -18536,6 +18532,15 @@ } } }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -20010,9 +20015,9 @@ } }, "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -21245,9 +21250,9 @@ }, "dependencies": { "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { "randombytes": "^2.1.0" diff --git a/package.json b/package.json index 27afea211..f5411847c 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "@angular/platform-browser": "~10.2.2", "@angular/platform-browser-dynamic": "~10.2.2", "@angular/router": "~10.2.2", + "@azure/app-configuration": "^1.1.0", + "@azure/identity": "^1.1.0", "@ngrx/effects": "^10.0.1", "@ngrx/store": "^10.0.1", "@ngrx/store-devtools": "^10.0.1", "@types/datatables.net-buttons": "^1.4.3", "angular-datatables": "^9.0.2", - "@azure/app-configuration": "^1.1.0", - "@azure/identity": "^1.1.0", "bootstrap": "^4.4.1", "datatables.net": "^1.10.21", "datatables.net-buttons": "^1.6.2", @@ -46,6 +46,7 @@ "ngx-material-timepicker": "^5.5.3", "ngx-pagination": "^5.0.0", "ngx-toastr": "^12.0.1", + "ngx-ui-switch": "^10.0.2", "rxjs": "~6.6.3", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1da328b83..505a1d050 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -72,6 +72,7 @@ import { DialogComponent } from './modules/shared/components/dialog/dialog.compo import { LoadingBarComponent } from './modules/shared/components/loading-bar/loading-bar.component'; import { UsersComponent } from './modules/users/pages/users.component'; import { UsersListComponent } from './modules/users/components/users-list/users-list.component'; +import { UiSwitchModule } from 'ngx-ui-switch'; import {NgxMaterialTimepickerModule} from 'ngx-material-timepicker'; // tslint:disable-next-line: max-line-length import { TechnologyReportTableComponent } from './modules/technology-report/components/technology-report-table/technology-report-table.component'; @@ -142,6 +143,7 @@ const maskConfig: Partial = { DataTablesModule, AutocompleteLibModule, NgxMaterialTimepickerModule, + UiSwitchModule, StoreModule.forRoot(reducers, { metaReducers, }), diff --git a/src/app/modules/users/components/users-list/users-list.component.html b/src/app/modules/users/components/users-list/users-list.component.html index 3b4490c97..71aa110da 100644 --- a/src/app/modules/users/components/users-list/users-list.component.html +++ b/src/app/modules/users/components/users-list/users-list.component.html @@ -1,15 +1,37 @@ - +
- - + + + - - + + +
User EmailNamesUser EmailNamesRoles
{{ user.email }}{{ user.name }}{{ user.email }}{{ user.name }} +
+ + admin + + test +
+
diff --git a/src/app/modules/users/components/users-list/users-list.component.spec.ts b/src/app/modules/users/components/users-list/users-list.component.spec.ts index 8cd39d44a..a8e7ead89 100644 --- a/src/app/modules/users/components/users-list/users-list.component.spec.ts +++ b/src/app/modules/users/components/users-list/users-list.component.spec.ts @@ -3,14 +3,17 @@ import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { NgxPaginationModule } from 'ngx-pagination'; import { UsersListComponent } from './users-list.component'; -import { UserActionTypes, UserState, LoadUsers } from '../../store'; +import { UserActionTypes, UserState, LoadUsers, GrantRoleUser, RevokeRoleUser } from '../../store'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; import { ActionsSubject } from '@ngrx/store'; import { DataTablesModule } from 'angular-datatables'; +import { Observable, of } from 'rxjs'; describe('UsersListComponent', () => { let component: UsersListComponent; let fixture: ComponentFixture; let store: MockStore; + let featureManagerService: FeatureManagerService; const actionSub: ActionsSubject = new ActionsSubject(); const state: UserState = { @@ -29,13 +32,16 @@ describe('UsersListComponent', () => { message: '', }; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [NgxPaginationModule, DataTablesModule], - declarations: [UsersListComponent], - providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }], - }).compileComponents(); - })); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NgxPaginationModule, DataTablesModule], + declarations: [UsersListComponent], + providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }], + }).compileComponents(); + featureManagerService = TestBed.inject(FeatureManagerService); + }) + ); beforeEach(() => { fixture = TestBed.createComponent(UsersListComponent); @@ -69,6 +75,76 @@ describe('UsersListComponent', () => { expect(component.users).toEqual(state.data); }); + it('When Component is created, should call the feature toggle method', () => { + spyOn(component, 'isFeatureToggleActivated').and.returnValue(of(true)); + + component.ngOnInit(); + + expect(component.isFeatureToggleActivated).toHaveBeenCalled(); + expect(component.isUserRoleToggleOn).toBe(true); + }); + + const actionsParams = [ + { actionType: UserActionTypes.GRANT_USER_ROLE_SUCCESS }, + { actionType: UserActionTypes.REVOKE_USER_ROLE_SUCCESS }, + ]; + + actionsParams.map((param) => { + it(`When action ${param.actionType} is dispatched should triggered load Users action`, () => { + spyOn(store, 'dispatch'); + + const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject; + const action = { + type: param.actionType, + payload: state.data, + }; + + actionSubject.next(action); + + expect(store.dispatch).toHaveBeenCalledWith(new LoadUsers()); + }); + }); + + const grantRoleTypes = [ + { roleId: 'admin', roleValue: 'time-tracker-admin' }, + { roleId: 'test', roleValue: 'time-tracker-tester' }, + ]; + + grantRoleTypes.map((param) => { + it(`When user switchRole to ${param.roleId} and don't have any role, should grant ${param.roleValue} Role`, () => { + const roleId = param.roleId; + const roleValue = param.roleValue; + const userRoles = []; + const userId = 'userId'; + + spyOn(store, 'dispatch'); + + component.switchRole(userId, userRoles, roleId, roleValue); + + expect(store.dispatch).toHaveBeenCalledWith(new GrantRoleUser(userId, roleId)); + }); + }); + + const revokeRoleTypes = [ + { roleId: 'admin', roleValue: 'time-tracker-admin', userRoles: ['time-tracker-admin'] }, + { roleId: 'test', roleValue: 'time-tracker-tester', userRoles: ['time-tracker-tester'] }, + ]; + + revokeRoleTypes.map((param) => { + it(`When user switchRole to ${param.roleId} and have that rol asigned, should revoke ${param.roleValue} Role`, () => { + const roleId = param.roleId; + const roleValue = param.roleValue; + const userRoles = param.userRoles; + const userId = 'userId'; + + spyOn(store, 'dispatch'); + + component.switchRole(userId, userRoles, roleId, roleValue); + + expect(store.dispatch).toHaveBeenCalledWith(new RevokeRoleUser(userId, roleId)); + }); + }); + it('on success load users, the data of roles should be an array and role null', () => { const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject; const action = { @@ -114,7 +190,23 @@ describe('UsersListComponent', () => { }); }); - it('on success load users, the datatable should be reloaded', async () => { + const toggleValues = [true, false]; + toggleValues.map((toggleValue) => { + it(`when FeatureToggle is ${toggleValue} should return ${toggleValue}`, () => { + spyOn(featureManagerService, 'isToggleEnabledForUser').and.returnValue(of(toggleValue)); + + const isFeatureToggleActivated: Observable = component.isFeatureToggleActivated(); + + expect(featureManagerService.isToggleEnabledForUser).toHaveBeenCalled(); + isFeatureToggleActivated.subscribe((value) => expect(value).toEqual(toggleValue)); + }); + }); + + /* + TODO: block commented on purpose so that when the tests pass and the Feature toggle is removed, + the table will be rendered again with dtInstance and not with dtOptions + + it('on success load users, the datatable should be reloaded', async () => { const actionSubject = TestBed.inject(ActionsSubject); const action = { type: UserActionTypes.LOAD_USERS_SUCCESS, @@ -125,7 +217,7 @@ describe('UsersListComponent', () => { actionSubject.next(action); expect(component.dtElement.dtInstance.then).toHaveBeenCalled(); - }); + });*/ afterEach(() => { component.dtTrigger.unsubscribe(); diff --git a/src/app/modules/users/components/users-list/users-list.component.ts b/src/app/modules/users/components/users-list/users-list.component.ts index 5d119fd47..91cfc2e23 100644 --- a/src/app/modules/users/components/users-list/users-list.component.ts +++ b/src/app/modules/users/components/users-list/users-list.component.ts @@ -2,10 +2,12 @@ import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular import { ActionsSubject, select, Store } from '@ngrx/store'; import { DataTableDirective } from 'angular-datatables'; import { Observable, Subject, Subscription } from 'rxjs'; -import { delay, filter } from 'rxjs/operators'; +import { delay, filter, map } from 'rxjs/operators'; import { User } from '../../models/users'; -import { LoadUsers, UserActionTypes } from '../../store/user.actions'; +import { GrantRoleUser, LoadUsers, RevokeRoleUser, UserActionTypes } from '../../store/user.actions'; import { getIsLoading } from '../../store/user.selectors'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; + @Component({ selector: 'app-users-list', templateUrl: './users-list.component.html', @@ -15,22 +17,45 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit { users: User[] = []; isLoading$: Observable; loadUsersSubscription: Subscription; + switchRoleSubscription: Subscription; dtTrigger: Subject = new Subject(); @ViewChild(DataTableDirective, { static: false }) dtElement: DataTableDirective; + dtOptions: any = {}; + isUserRoleToggleOn; - constructor(private store: Store, private actionsSubject$: ActionsSubject) { + constructor( + private store: Store, + private actionsSubject$: ActionsSubject, + private featureManagerService: FeatureManagerService + ) { this.isLoading$ = store.pipe(delay(0), select(getIsLoading)); } ngOnInit(): void { + this.isFeatureToggleActivated().subscribe((flag) => { + this.isUserRoleToggleOn = flag; + }); + this.store.dispatch(new LoadUsers()); this.loadUsersSubscription = this.actionsSubject$ .pipe(filter((action: any) => action.type === UserActionTypes.LOAD_USERS_SUCCESS)) .subscribe((action) => { this.users = action.payload; this.rerenderDataTable(); }); - this.store.dispatch(new LoadUsers()); + + this.switchRoleSubscription = this.actionsSubject$ + .pipe( + filter( + (action: any) => + action.type === UserActionTypes.GRANT_USER_ROLE_SUCCESS || + action.type === UserActionTypes.REVOKE_USER_ROLE_SUCCESS + ) + ) + .subscribe((action) => { + this.store.dispatch(new LoadUsers()); + this.rerenderDataTable(); + }); } ngAfterViewInit(): void { @@ -52,4 +77,18 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit { this.dtTrigger.next(); } } + + switchRole(userId: string, userRoles: string[], roleId: string, roleValue: string) { + userRoles.includes(roleValue) + ? this.store.dispatch(new RevokeRoleUser(userId, roleId)) + : this.store.dispatch(new GrantRoleUser(userId, roleId)); + } + + isFeatureToggleActivated() { + return this.featureManagerService.isToggleEnabledForUser('ui-list-test-users').pipe( + map((enabled) => { + return enabled === true ? true : false; + }) + ); + } } diff --git a/src/app/modules/users/services/users.service.spec.ts b/src/app/modules/users/services/users.service.spec.ts index 6489f34b9..cba99ef44 100644 --- a/src/app/modules/users/services/users.service.spec.ts +++ b/src/app/modules/users/services/users.service.spec.ts @@ -31,4 +31,24 @@ describe('UsersService', () => { const loadUserRequest = httpMock.expectOne(service.baseUrl); expect(loadUserRequest.request.method).toBe('GET'); }); + + it('grant role to a User', () => { + const userId = 'userId'; + const roleId = 'admin'; + + service.grantRole(userId, roleId).subscribe(); + + const grantRoleRequest = httpMock.expectOne(`${service.baseUrl}/${userId}/roles/${roleId}/grant`); + expect(grantRoleRequest.request.method).toBe('POST'); + }); + + it('revoke role to a User', () => { + const userId = 'userId'; + const roleId = 'admin'; + + service.revokeRole(userId, roleId).subscribe(); + + const grantRoleRequest = httpMock.expectOne(`${service.baseUrl}/${userId}/roles/${roleId}/revoke`); + expect(grantRoleRequest.request.method).toBe('POST'); + }); }); diff --git a/src/app/modules/users/services/users.service.ts b/src/app/modules/users/services/users.service.ts index e41b0401a..80e181f9e 100644 --- a/src/app/modules/users/services/users.service.ts +++ b/src/app/modules/users/services/users.service.ts @@ -13,4 +13,14 @@ export class UsersService { loadUsers(): Observable { return this.http.get(this.baseUrl); } + + grantRole(userId: string, roleId: string): Observable { + const url = `${this.baseUrl}/${userId}/roles/${roleId}/grant`; + return this.http.post(url, null); + } + + revokeRole(userId: string, roleId: string): Observable { + const url = `${this.baseUrl}/${userId}/roles/${roleId}/revoke`; + return this.http.post(url, null); + } } diff --git a/src/app/modules/users/store/user.actions.spec.ts b/src/app/modules/users/store/user.actions.spec.ts index 6edb98176..aa66253be 100644 --- a/src/app/modules/users/store/user.actions.spec.ts +++ b/src/app/modules/users/store/user.actions.spec.ts @@ -1,4 +1,5 @@ import * as actions from './user.actions'; +import { User } from '../models/users'; describe('UserActions', () => { it('LoadUsers type is UserActionTypes.LOAD_USERS', () => { @@ -15,4 +16,40 @@ describe('UserActions', () => { const action = new actions.LoadUsersFail('error'); expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS_FAIL); }); + + it('GrantRoleUser type is UserActionTypes.GRANT_USER_ROLE', () => { + const UserId = 'UserId'; + const RoleId = 'RoleId'; + const action = new actions.GrantRoleUser(UserId, RoleId); + expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE); + }); + + it('GrantRoleUserSuccess type is UserActionTypes.GRANT_USER_ROLE_SUCCESS', () => { + const payload: User = { id: 'id', email: 'email', name: 'name' }; + const action = new actions.GrantRoleUserSuccess(payload); + expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE_SUCCESS); + }); + + it('GrantRoleUserFail type is UserActionTypes.GRANT_USER_ROLE_FAIL', () => { + const action = new actions.GrantRoleUserFail('error'); + expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE_FAIL); + }); + + it('RevokeRoleUser type is UserActionTypes.REVOKE_USER_ROLE', () => { + const UserId = 'UserId'; + const RoleId = 'RoleId'; + const action = new actions.RevokeRoleUser(UserId, RoleId); + expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE); + }); + + it('RevokeRoleUserSuccess type is UserActionTypes.REVOKE_USER_ROLE_SUCCESS', () => { + const payload: User = { id: 'id', email: 'email', name: 'name' }; + const action = new actions.RevokeRoleUserSuccess(payload); + expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE_SUCCESS); + }); + + it('RevokeRoleUserFail type is UserActionTypes.REVOKE_USER_ROLE_FAIL', () => { + const action = new actions.RevokeRoleUserFail('error'); + expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE_FAIL); + }); }); diff --git a/src/app/modules/users/store/user.actions.ts b/src/app/modules/users/store/user.actions.ts index e6419423f..02ee3fe94 100644 --- a/src/app/modules/users/store/user.actions.ts +++ b/src/app/modules/users/store/user.actions.ts @@ -5,6 +5,12 @@ export enum UserActionTypes { LOAD_USERS = '[User] LOAD_USERS', LOAD_USERS_SUCCESS = '[User] LOAD_USERS_SUCCESS', LOAD_USERS_FAIL = '[User] LOAD_USERS_FAIL', + GRANT_USER_ROLE = '[User] GRANT_USER_ROLE', + GRANT_USER_ROLE_SUCCESS = '[User] GRANT_USER_ROLE_SUCCESS', + GRANT_USER_ROLE_FAIL = '[User] GRANT_USER_ROLE_FAIL', + REVOKE_USER_ROLE = '[User] REVOKE_USER_ROLE', + REVOKE_USER_ROLE_SUCCESS = '[User] REVOKE_USER_ROLE_SUCCESS', + REVOKE_USER_ROLE_FAIL = '[User] REVOKE_USER_ROLE_FAIL', DEFAULT_USER = '[USER] DEFAULT_USER', } @@ -22,8 +28,47 @@ export class LoadUsersFail implements Action { constructor(public error: string) {} } +export class GrantRoleUser implements Action { + public readonly type = UserActionTypes.GRANT_USER_ROLE; + constructor(public userId: string, public roleId: string) {} +} + +export class GrantRoleUserSuccess implements Action { + public readonly type = UserActionTypes.GRANT_USER_ROLE_SUCCESS; + constructor(public payload: User) {} +} + +export class GrantRoleUserFail implements Action { + public readonly type = UserActionTypes.GRANT_USER_ROLE_FAIL; + constructor(public error: string) {} +} + +export class RevokeRoleUser implements Action { + public readonly type = UserActionTypes.REVOKE_USER_ROLE; + constructor(public userId: string, public roleId: string) {} +} + +export class RevokeRoleUserSuccess implements Action { + public readonly type = UserActionTypes.REVOKE_USER_ROLE_SUCCESS; + constructor(public payload: User) {} +} + +export class RevokeRoleUserFail implements Action { + public readonly type = UserActionTypes.REVOKE_USER_ROLE_FAIL; + constructor(public error: string) {} +} export class DefaultUser implements Action { public readonly type = UserActionTypes.DEFAULT_USER; } -export type UserActions = LoadUsers | LoadUsersSuccess | LoadUsersFail | DefaultUser; +export type UserActions = + | LoadUsers + | LoadUsersSuccess + | LoadUsersFail + | DefaultUser + | GrantRoleUser + | GrantRoleUserSuccess + | GrantRoleUserFail + | RevokeRoleUser + | RevokeRoleUserSuccess + | RevokeRoleUserFail; diff --git a/src/app/modules/users/store/user.effects.spec.ts b/src/app/modules/users/store/user.effects.spec.ts index 2ac02426a..4438f64ea 100644 --- a/src/app/modules/users/store/user.effects.spec.ts +++ b/src/app/modules/users/store/user.effects.spec.ts @@ -51,4 +51,58 @@ describe('UserEffects', () => { expect(action.type).toEqual(UserActionTypes.LOAD_USERS_FAIL); }); }); + + it('action type is GRANT_USER_ROLE_SUCCESS when service is executed sucessfully', async () => { + const userId = 'userId'; + const roleId = 'roleId'; + actions$ = of({ type: UserActionTypes.GRANT_USER_ROLE, userId, roleId }); + const serviceSpy = spyOn(service, 'grantRole'); + spyOn(toastrService, 'success'); + serviceSpy.and.returnValue(of(user)); + + effects.grantUserRole$.subscribe((action) => { + expect(toastrService.success).toHaveBeenCalledWith('Grant User Role Success'); + expect(action.type).toEqual(UserActionTypes.GRANT_USER_ROLE_SUCCESS); + }); + }); + + it('action type is GRANT_USER_ROLE_FAIL when service is executed and fail', async () => { + const userId = 'userId'; + const roleId = 'roleId'; + actions$ = of({ type: UserActionTypes.GRANT_USER_ROLE, userId, roleId }); + spyOn(service, 'grantRole').and.returnValue(throwError({ error: { message: 'error' } })); + spyOn(toastrService, 'error'); + + effects.grantUserRole$.subscribe((action) => { + expect(toastrService.error).toHaveBeenCalled(); + expect(action.type).toEqual(UserActionTypes.GRANT_USER_ROLE_FAIL); + }); + }); + + it('action type is REVOKE_USER_ROLE_SUCCESS when service is executed sucessfully', async () => { + const userId = 'userId'; + const roleId = 'roleId'; + actions$ = of({ type: UserActionTypes.REVOKE_USER_ROLE, userId, roleId }); + const serviceSpy = spyOn(service, 'revokeRole'); + spyOn(toastrService, 'success'); + serviceSpy.and.returnValue(of(user)); + + effects.revokeUserRole$.subscribe((action) => { + expect(toastrService.success).toHaveBeenCalledWith('Revoke User Role Success'); + expect(action.type).toEqual(UserActionTypes.REVOKE_USER_ROLE_SUCCESS); + }); + }); + + it('action type is REVOKE_USER_ROLE_FAIL when service is executed and fail', async () => { + const userId = 'userId'; + const roleId = 'roleId'; + actions$ = of({ type: UserActionTypes.REVOKE_USER_ROLE, userId, roleId }); + spyOn(service, 'revokeRole').and.returnValue(throwError({ error: { message: 'error' } })); + spyOn(toastrService, 'error'); + + effects.revokeUserRole$.subscribe((action) => { + expect(toastrService.error).toHaveBeenCalled(); + expect(action.type).toEqual(UserActionTypes.REVOKE_USER_ROLE_FAIL); + }); + }); }); diff --git a/src/app/modules/users/store/user.effects.ts b/src/app/modules/users/store/user.effects.ts index 8ed2e1be5..fe715282c 100644 --- a/src/app/modules/users/store/user.effects.ts +++ b/src/app/modules/users/store/user.effects.ts @@ -27,4 +27,40 @@ export class UserEffects { ) ) ); + + @Effect() + grantUserRole$: Observable = this.actions$.pipe( + ofType(actions.UserActionTypes.GRANT_USER_ROLE), + map((action: actions.GrantRoleUser) => action), + mergeMap((action) => + this.userService.grantRole(action.userId, action.roleId).pipe( + map((response) => { + this.toastrService.success('Grant User Role Success'); + return new actions.GrantRoleUserSuccess(response); + }), + catchError((error) => { + this.toastrService.error(error.error.message); + return of(new actions.GrantRoleUserFail(error)); + }) + ) + ) + ); + + @Effect() + revokeUserRole$: Observable = this.actions$.pipe( + ofType(actions.UserActionTypes.REVOKE_USER_ROLE), + map((action: actions.RevokeRoleUser) => action), + mergeMap((action) => + this.userService.revokeRole(action.userId, action.roleId).pipe( + map((response) => { + this.toastrService.success('Revoke User Role Success'); + return new actions.RevokeRoleUserSuccess(response); + }), + catchError((error) => { + this.toastrService.error(error.error.message); + return of(new actions.RevokeRoleUserFail(error)); + }) + ) + ) + ); } diff --git a/src/app/modules/users/store/user.reducer.spec.ts b/src/app/modules/users/store/user.reducer.spec.ts index ccb8b69f8..4f89e5d4e 100644 --- a/src/app/modules/users/store/user.reducer.spec.ts +++ b/src/app/modules/users/store/user.reducer.spec.ts @@ -1,4 +1,5 @@ import { UserState, userReducer } from './user.reducers'; +import { User } from '../models/users'; import * as actions from './user.actions'; describe('userReducer', () => { @@ -28,6 +29,70 @@ describe('userReducer', () => { expect(state.data.length).toBe(0); }); + it('on GrantUserRole, isLoading is true', () => { + const userId = 'userId'; + const roleId = 'roleId'; + const action = new actions.GrantRoleUser(userId, roleId); + const state = userReducer(initialState, action); + + expect(state.isLoading).toEqual(true); + }); + + it('on GrantRoleUserSuccess, user role should change', () => { + const currentState: UserState = { + data: [{ id: 'id', name: 'name', email: 'email', role: null }], + isLoading: false, + message: '', + }; + const userGranted: User = { id: 'id', name: 'name', email: 'email', role: 'admin' }; + const action = new actions.GrantRoleUserSuccess(userGranted); + const state = userReducer(currentState, action); + + expect(state.data).toEqual([userGranted]); + expect(state.isLoading).toEqual(false); + expect(state.message).toEqual('Grant User Role Success'); + }); + + it('on GrantRoleUserFail, should show a message with an error message', () => { + const action = new actions.GrantRoleUserFail('error'); + const state = userReducer(initialState, action); + + expect(state.message).toEqual('Something went wrong granting user role'); + expect(state.isLoading).toEqual(false); + }); + + it('on RevokeUserRole, isLoading is true', () => { + const userId = 'userId'; + const roleId = 'roleId'; + const action = new actions.RevokeRoleUser(userId, roleId); + const state = userReducer(initialState, action); + + expect(state.isLoading).toEqual(true); + }); + + it('on RevokeRoleUserSuccess, user role should change', () => { + const currentState: UserState = { + data: [{ id: 'id', name: 'name', email: 'email', role: 'admin' }], + isLoading: false, + message: '', + }; + const userRevoked: User = { id: 'id', name: 'name', email: 'email', role: null }; + const action = new actions.RevokeRoleUserSuccess(userRevoked); + const state = userReducer(currentState, action); + + expect(state.data).toEqual([userRevoked]); + expect(state.isLoading).toEqual(false); + expect(state.message).toEqual('Revoke User Role Success'); + }); + + it('on RevokeRoleUserFail, should show a message with an error message', () => { + const action = new actions.RevokeRoleUserFail('error'); + const state = userReducer(initialState, action); + + expect(state.message).toEqual('Something went wrong revoking user role'); + expect(state.isLoading).toEqual(false); + }); + it('on Default, ', () => { const action = new actions.DefaultUser(); const state = userReducer(initialState, action); diff --git a/src/app/modules/users/store/user.reducers.ts b/src/app/modules/users/store/user.reducers.ts index 8bd0d10e7..40bd5ca87 100644 --- a/src/app/modules/users/store/user.reducers.ts +++ b/src/app/modules/users/store/user.reducers.ts @@ -14,6 +14,7 @@ export const initialState: UserState = { }; export const userReducer = (state: UserState = initialState, action: UserActions) => { + const userData = [...state.data]; switch (action.type) { case UserActionTypes.LOAD_USERS: { return { @@ -33,6 +34,58 @@ export const userReducer = (state: UserState = initialState, action: UserActions ...state, data: [], isLoading: false, + message: action.error, + }; + } + case UserActionTypes.GRANT_USER_ROLE: { + return { + ...state, + isLoading: true, + }; + } + case UserActionTypes.GRANT_USER_ROLE_SUCCESS: { + const index = userData.findIndex((user) => user.id === action.payload.id); + userData[index] = action.payload; + return { + ...state, + data: userData, + isLoading: false, + message: 'Grant User Role Success', + }; + } + + case UserActionTypes.GRANT_USER_ROLE_FAIL: { + return { + ...state, + data: state.data, + isLoading: false, + message: 'Something went wrong granting user role', + }; + } + + case UserActionTypes.REVOKE_USER_ROLE: { + return { + ...state, + isLoading: true, + }; + } + case UserActionTypes.REVOKE_USER_ROLE_SUCCESS: { + const index = userData.findIndex((user) => user.id === action.payload.id); + userData[index] = action.payload; + return { + ...state, + data: userData, + isLoading: false, + message: 'Revoke User Role Success', + }; + } + + case UserActionTypes.REVOKE_USER_ROLE_FAIL: { + return { + ...state, + data: state.data, + isLoading: false, + message: 'Something went wrong revoking user role', }; } default: