-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: retrieve roles and permissions for B2B users (#298)
- Loading branch information
Showing
17 changed files
with
444 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export interface AuthorizationData { | ||
userRoles: { | ||
roleDisplayName: string; | ||
permissions: { permissionID: string }[]; | ||
}[]; | ||
} |
125 changes: 125 additions & 0 deletions
125
src/app/core/models/authorization/authorization.mapper.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
|
||
import { AuthorizationData } from './authorization.interface'; | ||
import { AuthorizationMapper } from './authorization.mapper'; | ||
|
||
describe('Authorization Mapper', () => { | ||
let authorizationMapper: AuthorizationMapper; | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({}); | ||
authorizationMapper = TestBed.inject(AuthorizationMapper); | ||
}); | ||
|
||
describe('fromData', () => { | ||
it('should throw when input is falsy', () => { | ||
expect(() => authorizationMapper.fromData(undefined)).toThrow(); | ||
}); | ||
|
||
it('should map empty role response to empty authorization data', () => { | ||
expect(authorizationMapper.fromData({ userRoles: [] })).toMatchInlineSnapshot(` | ||
Object { | ||
"permissionIDs": Array [], | ||
"roleDisplayNames": Array [], | ||
} | ||
`); | ||
}); | ||
|
||
it('should map empty response to empty authorization data', () => { | ||
expect(authorizationMapper.fromData(({} as unknown) as AuthorizationData)).toMatchInlineSnapshot(` | ||
Object { | ||
"permissionIDs": Array [], | ||
"roleDisplayNames": Array [], | ||
} | ||
`); | ||
}); | ||
|
||
it('should map incoming data to model data', () => { | ||
const data = ({ | ||
type: 'UserRoles', | ||
userRoles: [ | ||
{ | ||
type: 'UserRole', | ||
roleID: 'APP_B2B_BUYER', | ||
roleDisplayName: 'Einkäufer', | ||
fixed: true, | ||
permissions: [ | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_ASSIGN_COSTOBJECT_TO_BASKET', | ||
permissionDisplayName: 'Kostenobjekt zu Warenkorb zuordnen', | ||
}, | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_ASSIGN_COSTOBJECT_TO_BASKETLINEITEM', | ||
permissionDisplayName: 'Kostenobjekt zu Warenkorbposition zuordnen', | ||
}, | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_MANAGE_OWN_QUOTES', | ||
permissionDisplayName: 'Preisangebote erstellen', | ||
}, | ||
{ type: 'RolePermission', permissionID: 'APP_B2B_PURCHASE', permissionDisplayName: 'Einkäufe tätigen' }, | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_VIEW_COSTOBJECT', | ||
permissionDisplayName: 'Kostenobjekte anzeigen', | ||
}, | ||
], | ||
}, | ||
{ | ||
type: 'UserRole', | ||
roleID: 'APP_B2B_APPROVER', | ||
roleDisplayName: 'Genehmiger', | ||
fixed: false, | ||
permissions: [ | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_ASSIGN_COSTOBJECT_TO_BASKET', | ||
permissionDisplayName: 'Kostenobjekt zu Warenkorb zuordnen', | ||
}, | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_ASSIGN_COSTOBJECT_TO_BASKETLINEITEM', | ||
permissionDisplayName: 'Kostenobjekt zu Warenkorbposition zuordnen', | ||
}, | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_MANAGE_OWN_QUOTES', | ||
permissionDisplayName: 'Preisangebote erstellen', | ||
}, | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_ORDER_APPROVAL', | ||
permissionDisplayName: 'Offene Bestellungen genehmigen', | ||
}, | ||
{ type: 'RolePermission', permissionID: 'APP_B2B_PURCHASE', permissionDisplayName: 'Einkäufe tätigen' }, | ||
{ | ||
type: 'RolePermission', | ||
permissionID: 'APP_B2B_VIEW_COSTOBJECT', | ||
permissionDisplayName: 'Kostenobjekte anzeigen', | ||
}, | ||
], | ||
}, | ||
], | ||
} as unknown) as AuthorizationData; | ||
const mapped = authorizationMapper.fromData(data); | ||
expect(mapped.roleDisplayNames).toMatchInlineSnapshot(` | ||
Array [ | ||
"Einkäufer", | ||
"Genehmiger", | ||
] | ||
`); | ||
expect(mapped.permissionIDs).toMatchInlineSnapshot(` | ||
Array [ | ||
"APP_B2B_ASSIGN_COSTOBJECT_TO_BASKET", | ||
"APP_B2B_ASSIGN_COSTOBJECT_TO_BASKETLINEITEM", | ||
"APP_B2B_MANAGE_OWN_QUOTES", | ||
"APP_B2B_PURCHASE", | ||
"APP_B2B_VIEW_COSTOBJECT", | ||
"APP_B2B_ORDER_APPROVAL", | ||
] | ||
`); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Injectable } from '@angular/core'; | ||
|
||
import { AuthorizationData } from './authorization.interface'; | ||
import { Authorization } from './authorization.model'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class AuthorizationMapper { | ||
fromData(authorizationData: AuthorizationData): Authorization { | ||
if (authorizationData) { | ||
if (!authorizationData.userRoles?.length) { | ||
return { permissionIDs: [], roleDisplayNames: [] }; | ||
} | ||
return { | ||
roleDisplayNames: authorizationData.userRoles.map(role => role.roleDisplayName), | ||
permissionIDs: authorizationData.userRoles | ||
.map(role => role.permissions.map(p => p.permissionID)) | ||
.reduce((acc, val) => [...acc, ...val], []) | ||
.filter((v, i, a) => a.indexOf(v) === i), | ||
}; | ||
} else { | ||
throw new Error(`authorization data is required`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface Authorization { | ||
roleDisplayNames: string[]; | ||
permissionIDs: string[]; | ||
} |
59 changes: 59 additions & 0 deletions
59
src/app/core/services/authorization/authorization.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
import { of } from 'rxjs'; | ||
import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; | ||
|
||
import { Customer } from 'ish-core/models/customer/customer.model'; | ||
import { User } from 'ish-core/models/user/user.model'; | ||
import { ApiService } from 'ish-core/services/api/api.service'; | ||
|
||
import { AuthorizationService } from './authorization.service'; | ||
|
||
describe('Authorization Service', () => { | ||
let apiService: ApiService; | ||
let authorizationService: AuthorizationService; | ||
|
||
beforeEach(() => { | ||
apiService = mock(ApiService); | ||
when(apiService.get(anything())).thenReturn(of({})); | ||
|
||
TestBed.configureTestingModule({ | ||
providers: [{ provide: ApiService, useFactory: () => instance(apiService) }], | ||
}); | ||
authorizationService = TestBed.inject(AuthorizationService); | ||
}); | ||
|
||
it('should be created', () => { | ||
expect(authorizationService).toBeTruthy(); | ||
}); | ||
|
||
it('should fail when customer input is falsy', done => { | ||
authorizationService.getRolesAndPermissions(undefined, {} as User).subscribe({ | ||
error: err => { | ||
expect(err).toBeTruthy(); | ||
done(); | ||
}, | ||
}); | ||
}); | ||
|
||
it('should fail when user input is falsy', done => { | ||
authorizationService.getRolesAndPermissions({} as Customer, undefined).subscribe({ | ||
error: err => { | ||
expect(err).toBeTruthy(); | ||
done(); | ||
}, | ||
}); | ||
}); | ||
|
||
it('should call roles api when queried', done => { | ||
authorizationService | ||
.getRolesAndPermissions({ customerNo: 'FOOD' } as Customer, { login: 'email' } as User) | ||
.subscribe({ | ||
next: () => { | ||
verify(apiService.get(anything())).once(); | ||
expect(capture(apiService.get).last()[0]).toMatchInlineSnapshot(`"customers/FOOD/users/email/roles"`); | ||
done(); | ||
}, | ||
error: fail, | ||
}); | ||
}); | ||
}); |
27 changes: 27 additions & 0 deletions
27
src/app/core/services/authorization/authorization.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { throwError } from 'rxjs'; | ||
import { map } from 'rxjs/operators'; | ||
|
||
import { AuthorizationData } from 'ish-core/models/authorization/authorization.interface'; | ||
import { AuthorizationMapper } from 'ish-core/models/authorization/authorization.mapper'; | ||
import { Customer } from 'ish-core/models/customer/customer.model'; | ||
import { User } from 'ish-core/models/user/user.model'; | ||
import { ApiService } from 'ish-core/services/api/api.service'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class AuthorizationService { | ||
constructor(private apiService: ApiService, private authorizationMapper: AuthorizationMapper) {} | ||
|
||
getRolesAndPermissions(customer: Customer, user: User) { | ||
if (!customer?.customerNo) { | ||
return throwError('getRolesAndPermissions() called without customer.customerNo'); | ||
} | ||
if (!user?.login) { | ||
return throwError('getRolesAndPermissions() called without user.login'); | ||
} | ||
|
||
return this.apiService | ||
.get<AuthorizationData>(`customers/${customer.customerNo}/users/${user.login}/roles`) | ||
.pipe(map(data => this.authorizationMapper.fromData(data))); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
src/app/core/store/customer/authorization/authorization.actions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { createAction } from '@ngrx/store'; | ||
|
||
import { Authorization } from 'ish-core/models/authorization/authorization.model'; | ||
import { httpError, payload } from 'ish-core/utils/ngrx-creators'; | ||
|
||
export const loadRolesAndPermissionsSuccess = createAction( | ||
'[Authorization API] Load Roles and Permissions Success', | ||
payload<{ authorization: Authorization }>() | ||
); | ||
|
||
export const loadRolesAndPermissionsFail = createAction( | ||
'[Authorization API] Load Roles and Permissions Fail', | ||
httpError() | ||
); |
69 changes: 69 additions & 0 deletions
69
src/app/core/store/customer/authorization/authorization.effects.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
import { provideMockActions } from '@ngrx/effects/testing'; | ||
import { Action } from '@ngrx/store'; | ||
import { MockStore, provideMockStore } from '@ngrx/store/testing'; | ||
import { Observable, of, throwError } from 'rxjs'; | ||
import { anything, instance, mock, verify, when } from 'ts-mockito'; | ||
|
||
import { Authorization } from 'ish-core/models/authorization/authorization.model'; | ||
import { Customer } from 'ish-core/models/customer/customer.model'; | ||
import { User } from 'ish-core/models/user/user.model'; | ||
import { AuthorizationService } from 'ish-core/services/authorization/authorization.service'; | ||
import { getLoggedInCustomer, loadCompanyUserSuccess } from 'ish-core/store/customer/user'; | ||
|
||
import { AuthorizationEffects } from './authorization.effects'; | ||
|
||
describe('Authorization Effects', () => { | ||
let actions$: Observable<Action>; | ||
let effects: AuthorizationEffects; | ||
let store$: MockStore; | ||
let authorizationService: AuthorizationService; | ||
|
||
beforeEach(() => { | ||
authorizationService = mock(AuthorizationService); | ||
when(authorizationService.getRolesAndPermissions(anything(), anything())).thenReturn(of({} as Authorization)); | ||
|
||
TestBed.configureTestingModule({ | ||
providers: [ | ||
AuthorizationEffects, | ||
provideMockActions(() => actions$), | ||
provideMockStore(), | ||
{ provide: AuthorizationService, useFactory: () => instance(authorizationService) }, | ||
], | ||
}); | ||
|
||
effects = TestBed.inject(AuthorizationEffects); | ||
store$ = TestBed.inject(MockStore); | ||
store$.overrideSelector(getLoggedInCustomer, {} as Customer); | ||
}); | ||
|
||
describe('loadRolesAndPermissions$', () => { | ||
it('should call the authorization service when company user was loaded successfully', done => { | ||
actions$ = of(loadCompanyUserSuccess({ user: {} as User })); | ||
|
||
effects.loadRolesAndPermissions$.subscribe(action => { | ||
verify(authorizationService.getRolesAndPermissions(anything(), anything())).once(); | ||
expect(action).toMatchInlineSnapshot(` | ||
[Authorization API] Load Roles and Permissions Success: | ||
authorization: {} | ||
`); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('should map to error action when service call fails', done => { | ||
when(authorizationService.getRolesAndPermissions(anything(), anything())).thenReturn(throwError('ERROR')); | ||
|
||
actions$ = of(loadCompanyUserSuccess({ user: {} as User })); | ||
|
||
effects.loadRolesAndPermissions$.subscribe(action => { | ||
verify(authorizationService.getRolesAndPermissions(anything(), anything())).once(); | ||
expect(action).toMatchInlineSnapshot(` | ||
[Authorization API] Load Roles and Permissions Fail: | ||
error: {} | ||
`); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
29 changes: 29 additions & 0 deletions
29
src/app/core/store/customer/authorization/authorization.effects.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { Actions, createEffect, ofType } from '@ngrx/effects'; | ||
import { Store, select } from '@ngrx/store'; | ||
import { map, switchMap, withLatestFrom } from 'rxjs/operators'; | ||
|
||
import { AuthorizationService } from 'ish-core/services/authorization/authorization.service'; | ||
import { getLoggedInCustomer, loadCompanyUserSuccess } from 'ish-core/store/customer/user'; | ||
import { mapErrorToAction, mapToPayloadProperty } from 'ish-core/utils/operators'; | ||
|
||
import { loadRolesAndPermissionsFail, loadRolesAndPermissionsSuccess } from './authorization.actions'; | ||
|
||
@Injectable() | ||
export class AuthorizationEffects { | ||
constructor(private actions$: Actions, private store: Store, private authorizationService: AuthorizationService) {} | ||
|
||
loadRolesAndPermissions$ = createEffect(() => | ||
this.actions$.pipe( | ||
ofType(loadCompanyUserSuccess), | ||
mapToPayloadProperty('user'), | ||
withLatestFrom(this.store.pipe(select(getLoggedInCustomer))), | ||
switchMap(([user, customer]) => | ||
this.authorizationService.getRolesAndPermissions(customer, user).pipe( | ||
map(authorization => loadRolesAndPermissionsSuccess({ authorization })), | ||
mapErrorToAction(loadRolesAndPermissionsFail) | ||
) | ||
) | ||
) | ||
); | ||
} |
15 changes: 15 additions & 0 deletions
15
src/app/core/store/customer/authorization/authorization.reducer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { createReducer, on } from '@ngrx/store'; | ||
|
||
import { Authorization } from 'ish-core/models/authorization/authorization.model'; | ||
|
||
import { loadRolesAndPermissionsSuccess } from './authorization.actions'; | ||
|
||
const initialState: Authorization = { | ||
roleDisplayNames: [], | ||
permissionIDs: [], | ||
}; | ||
|
||
export const authorizationReducer = createReducer( | ||
initialState, | ||
on(loadRolesAndPermissionsSuccess, (_, action) => action.payload.authorization) | ||
); |
Oops, something went wrong.