Skip to content

Commit

Permalink
feat: retrieve roles and permissions for B2B users (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi committed Jul 6, 2020
1 parent 9755c0e commit e4ba676
Show file tree
Hide file tree
Showing 17 changed files with 444 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/app/core/models/authorization/authorization.interface.ts
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 src/app/core/models/authorization/authorization.mapper.spec.ts
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",
]
`);
});
});
});
24 changes: 24 additions & 0 deletions src/app/core/models/authorization/authorization.mapper.ts
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`);
}
}
}
4 changes: 4 additions & 0 deletions src/app/core/models/authorization/authorization.model.ts
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 src/app/core/services/authorization/authorization.service.spec.ts
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 src/app/core/services/authorization/authorization.service.ts
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 src/app/core/store/customer/authorization/authorization.actions.ts
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()
);
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 src/app/core/store/customer/authorization/authorization.effects.ts
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 src/app/core/store/customer/authorization/authorization.reducer.ts
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)
);
Loading

0 comments on commit e4ba676

Please sign in to comment.