diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..9e6c1db --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1 @@ +import 'jest-extended'; diff --git a/src/lib/permissions/index.ts b/src/lib/permissions/index.ts index a6d3705..67e7c44 100644 --- a/src/lib/permissions/index.ts +++ b/src/lib/permissions/index.ts @@ -1,6 +1,4 @@ -export * from './models/permission.model'; -export * from './models/permissions-router-data.model'; -export * from './models/role.model'; +export * from './types'; export * from './services/permissions-guard.service'; export * from './services/permissions.service'; export * from './services/roles.service'; diff --git a/src/lib/permissions/models/permission.model.ts b/src/lib/permissions/models/permission.model.ts deleted file mode 100644 index d14a282..0000000 --- a/src/lib/permissions/models/permission.model.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ValidationFn } from './permissions-router-data.model'; - -export interface IqserPermission { - name: string; - validationFn?: ValidationFn; -} - -export interface IqserPermissionsObject { - [name: string]: IqserPermission; -} diff --git a/src/lib/permissions/models/role.model.ts b/src/lib/permissions/models/role.model.ts deleted file mode 100644 index 291c1e2..0000000 --- a/src/lib/permissions/models/role.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidationFn } from './permissions-router-data.model'; - -export interface IqserRole { - name: string; - validationFn: ValidationFn | string[]; -} diff --git a/src/lib/permissions/permissions.directive.spec.ts b/src/lib/permissions/permissions.directive.spec.ts index 0136cc7..62b18e4 100644 --- a/src/lib/permissions/permissions.directive.spec.ts +++ b/src/lib/permissions/permissions.directive.spec.ts @@ -1,246 +1,266 @@ import { Component, Type } from '@angular/core'; import { IqserPermissionsModule } from '.'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, ComponentFixtureAutoDetect, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { IqserPermissionsService } from './services/permissions.service'; import { IqserRolesService } from './services/roles.service'; +class BaseTestComponent { + isAuthorized() {} + + isUnauthorized() {} +} + const ADMIN = 'ADMIN' as const; const GUEST = 'GUEST' as const; -function detectChanges(fixture: ComponentFixture) { - tick(); - fixture.detectChanges(); -} - -let fixture: ComponentFixture; +let fixture: ComponentFixture; let permissionsService: IqserPermissionsService; let rolesService: IqserRolesService; +let isAuthorizedSpy: jest.SpyInstance; +let isUnauthorizedSpy: jest.SpyInstance; + +function configureTestBed(component: Type) { + TestBed.configureTestingModule({ + declarations: [component], + imports: [IqserPermissionsModule.forRoot()], + providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }], + }); -function configureTestBed(component: Type) { - TestBed.configureTestingModule({ declarations: [component], imports: [IqserPermissionsModule.forRoot()] }); fixture = TestBed.createComponent(component); - detectChanges(fixture); - permissionsService = fixture.debugElement.injector.get(IqserPermissionsService); - rolesService = fixture.debugElement.injector.get(IqserRolesService); + isAuthorizedSpy = jest.spyOn(fixture.componentInstance, 'isAuthorized'); + isUnauthorizedSpy = jest.spyOn(fixture.componentInstance, 'isUnauthorized'); + + permissionsService = TestBed.inject(IqserPermissionsService); + rolesService = TestBed.inject(IqserRolesService); } -describe('Permission directive angular', () => { +function getFixtureContent(): HTMLElement { + return fixture.debugElement.nativeElement.querySelector('div'); +} + +describe('Permission directive', () => { @Component({ - template: ` + template: `
123
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should show the component', fakeAsync(() => { + it('should show the component and emit permissionAuthorized', fakeAsync(() => { permissionsService.load([ADMIN, GUEST]); - detectChanges(fixture); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); + + expect(isAuthorizedSpy).toHaveBeenCalledTimes(1); + expect(isUnauthorizedSpy).toHaveBeenCalledTimes(1); + expect(isUnauthorizedSpy).toHaveBeenCalledBefore(isAuthorizedSpy); })); - it('Should not show the component', fakeAsync(() => { + it('should not show the component', fakeAsync(() => { permissionsService.load([GUEST]); - detectChanges(fixture); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + expect(getFixtureContent()).toEqual(null); + expect(isAuthorizedSpy).toHaveBeenCalledTimes(0); + expect(isUnauthorizedSpy).toHaveBeenCalledTimes(1); })); - it('Should show component when permission added', fakeAsync(() => { + it('should show component when permission added', fakeAsync(() => { permissionsService.load([GUEST]); - detectChanges(fixture); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + expect(getFixtureContent()).toEqual(null); permissionsService.add(ADMIN); - detectChanges(fixture); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('123'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('123'); + + expect(isUnauthorizedSpy).toHaveBeenCalledTimes(1); + expect(isAuthorizedSpy).toHaveBeenCalledTimes(1); + expect(isUnauthorizedSpy).toHaveBeenCalledBefore(isAuthorizedSpy); })); - it('Should hide component when permission removed', fakeAsync(() => { + it('should hide component when permission removed', fakeAsync(() => { permissionsService.load([ADMIN, GUEST]); - detectChanges(fixture); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('123'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('123'); + expect(isUnauthorizedSpy).toHaveBeenCalledTimes(1); + expect(isAuthorizedSpy).toHaveBeenCalledTimes(1); + expect(isUnauthorizedSpy).toHaveBeenCalledBefore(isAuthorizedSpy); permissionsService.remove(ADMIN); - detectChanges(fixture); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + expect(getFixtureContent()).toEqual(null); + expect(isUnauthorizedSpy).toHaveBeenCalledTimes(2); + expect(isAuthorizedSpy).toHaveBeenCalledTimes(1); })); }); -describe('Permission directive angular roles', () => { +describe('Permission directive with roles', () => { @Component({ template: `
123
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} const awesomePermissions = 'AWESOME'; - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should show the component when key of role is the same', fakeAsync(() => { - rolesService.add('ADMIN', [awesomePermissions]); + it('should show the component when key of role is the same', fakeAsync(() => { + rolesService.add({ ADMIN: [awesomePermissions] }); permissionsService.add(awesomePermissions); - detectChanges(fixture); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); })); - it('should show the component when permissions array is the same ', fakeAsync(() => { - rolesService.add('ADMIN', [awesomePermissions]); + it('should show the component when permissions array is the same', fakeAsync(() => { + rolesService.add({ ADMIN: [awesomePermissions] }); permissionsService.add(awesomePermissions); - detectChanges(fixture); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); })); it('should hide the component when user deletes all roles', fakeAsync(() => { permissionsService.add(awesomePermissions); - rolesService.add('ADMIN', [awesomePermissions]); - detectChanges(fixture); + rolesService.add({ ADMIN: [awesomePermissions] }); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); rolesService.clear(); - detectChanges(fixture); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + tick(); + + expect(getFixtureContent()).toEqual(null); })); it('should hide the component when user deletes one role', fakeAsync(() => { permissionsService.add(awesomePermissions); - rolesService.add('ADMIN', [awesomePermissions]); - detectChanges(fixture); + rolesService.add({ ADMIN: [awesomePermissions] }); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); - rolesService.remove('ADMIN'); - detectChanges(fixture); + rolesService.remove(ADMIN); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); }); -describe('Permission directive angular roles array', () => { +describe('Permission directive with roles array', () => { @Component({ template: `
123
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} const awesomePermission = 'AWESOME'; - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should show the component when key of role is the same', fakeAsync(() => { + it('should show the component when key of role is the same', fakeAsync(() => { permissionsService.add(awesomePermission); - rolesService.add('ADMIN', [awesomePermission]); - detectChanges(fixture); + rolesService.add({ ADMIN: [awesomePermission] }); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); })); - it('should show the component when there is permission ', fakeAsync(() => { + it('should show the component when there is permission', fakeAsync(() => { permissionsService.add(awesomePermission); - rolesService.add('ADMIN', ['AWESOME']); - detectChanges(fixture); + rolesService.add({ ADMIN: ['AWESOME'] }); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); })); it('should hide the component when user deletes all roles', fakeAsync(() => { permissionsService.add(awesomePermission); - rolesService.add('ADMIN', [awesomePermission]); - detectChanges(fixture); + rolesService.add({ ADMIN: [awesomePermission] }); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); rolesService.clear(); - detectChanges(fixture); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); it('should hide the component when user deletes one roles', fakeAsync(() => { permissionsService.add(awesomePermission); - rolesService.add('ADMIN', [awesomePermission]); - detectChanges(fixture); + rolesService.add({ ADMIN: [awesomePermission] }); + tick(); - const content = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); expect(content).toBeTruthy(); expect(content.innerHTML).toEqual('123'); - rolesService.remove('ADMIN'); - detectChanges(fixture); + rolesService.remove(ADMIN); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); }); -describe('Permission directive angular testing different selectors *allow', () => { +describe('Permission directive testing different selectors *allow', () => { @Component({ template: `
123
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); it('Should show the component when key of role is the same', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + expect(getFixtureContent()).toEqual(null); permissionsService.add('AWESOME'); - rolesService.add('ADMIN', ['AWESOME']); - detectChanges(fixture); + rolesService.add({ ADMIN: ['AWESOME'] }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should hide the component when key of role is the same', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should hide the component when key of role is the same', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('GG', ['Awsesome']); - detectChanges(fixture); + rolesService.add({ GG: ['Awsesome'] }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); }); @@ -250,54 +270,48 @@ describe('Permission directive angular testing different async functions in role
123
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should show the component when promise returns truthy value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when promise returns truthy value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('ADMIN', () => true); - detectChanges(fixture); + rolesService.add({ ADMIN: () => true }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should not show the component when promise returns truthy value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when promise returns truthy value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('ADMIN', () => false); - detectChanges(fixture); + rolesService.add({ ADMIN: () => false }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when promise returns truthy value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when promise returns truthy value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('ADMIN', () => Promise.resolve(true)); - detectChanges(fixture); + rolesService.add({ ADMIN: () => Promise.resolve(true) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should not show the component when promise rejects', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when promise rejects', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('ADMIN', () => Promise.reject()); - detectChanges(fixture); + rolesService.add({ ADMIN: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); }); @@ -307,524 +321,455 @@ describe('Permission directive angular testing different async functions in role
123
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should show the component when promise returns truthy value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when promise returns truthy value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('ADMIN', () => Promise.resolve(true)); - detectChanges(fixture); + rolesService.add({ ADMIN: () => Promise.resolve(true) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should not show the component when promise returns false value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when promise returns false value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('ADMIN', () => Promise.resolve(false)); - detectChanges(fixture); + rolesService.add({ ADMIN: () => Promise.resolve(false) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should not show the component when promise rejects', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); - rolesService.add('ADMIN', () => Promise.reject()); - detectChanges(fixture); + it('should not show the component when promise rejects', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + rolesService.add({ ADMIN: () => Promise.reject() }); + tick(); + + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one of the promises fulfills ', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when one of the promises fulfills', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('ADMIN', () => Promise.reject()); - rolesService.add('GUEST', () => Promise.resolve(true)); - detectChanges(fixture); + rolesService.add({ ADMIN: () => Promise.reject(), GUEST: () => Promise.resolve(true) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should show the component when one of the promises fulfills with 0 value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); - rolesService.add('ADMIN', () => { - return Promise.reject(); - }); + it('should show the component when one of the promises fulfills with true value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('GUEST', () => { - return Promise.resolve(); - }); + rolesService.add({ ADMIN: () => Promise.reject(), GUEST: () => Promise.resolve(true) }); + tick(); - detectChanges(fixture); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should not show the component when all promises fails', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); - rolesService.add('ADMIN', () => { - return Promise.reject(); - }); + it('should not show the component when all promises fails', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('GUEST', () => { - return Promise.reject(); - }); + rolesService.add({ ADMIN: () => Promise.reject(), GUEST: () => Promise.reject() }); + tick(); - detectChanges(fixture); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one of promises returns true', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when one of promises returns true', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - rolesService.add('GUEST', () => { - return true; - }); + rolesService.add({ GUEST: () => true, ADMIN: () => Promise.reject() }); + tick(); - rolesService.add('ADMIN', () => { - return Promise.reject(); - }); - - detectChanges(fixture); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should show the component when 1 passes second fails', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); - rolesService.add('ADMIN', () => { - return Promise.reject(); - }); - permissionsService.add('AWESOME'); - rolesService.add('GUEST', ['AWESOME']); + it('should show the component when 1 passes second fails', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - detectChanges(fixture); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + rolesService.add({ ADMIN: () => Promise.reject() }); + rolesService.add({ ADMIN: ['AWESOME'] }); + tick(); + + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should show the component when one rejects but another one fulfils', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); - rolesService.add('ADMIN', () => { - return Promise.reject(); - }); - permissionsService.add('AWESOME'); - rolesService.add('GUEST', ['AWESOME']); + it('should show the component when one rejects but another one fulfils', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - detectChanges(fixture); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + rolesService.add({ ADMIN: () => Promise.reject(), GUEST: ['AWESOME'] }); + tick(); + + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); }); -describe('Permission directive angular testing different async functions in permissions via array', () => { +describe('Permission directive with different async functions in permissions via array', () => { @Component({ template: `
123
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should show the component when promise returns truthy value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when promise returns truthy value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => true); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => true }); + permissionsService.add({ GUEST: () => Promise.resolve(true) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); + })); + + it('should not show the component when promise returns false value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); + + permissionsService.add({ ADMIN: () => false }); + tick(); + + expect(getFixtureContent()).toEqual(null); + })); + + it('should show the component when promises returns truthy value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); + + permissionsService.add({ + ADMIN: () => Promise.resolve(true), + GUEST: () => Promise.resolve(true), + }); + tick(); + + const content2 = getFixtureContent(); expect(content2).toBeTruthy(); expect(content2.innerHTML).toEqual('
123
'); })); - it('Should not show the component when promise returns false value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when promise rejects', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => false); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when promise returns truthy value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when only one of the promises fulfills ', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.resolve(true)); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.resolve(true) }); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + expect(getFixtureContent()).toEqual(null); })); - it('Should not show the component when promise rejects', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when one of the promises fulfills with false value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.resolve(true) }); + permissionsService.add({ GUEST: () => Promise.resolve(false) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one of the promises fulfills ', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when all promises fails', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.resolve()); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.reject() }); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one of the promises fulfills with 0 value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when one of promises rejects', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.resolve()); - permissionsService.add('GUEST', () => Promise.resolve()); - detectChanges(fixture); + permissionsService.add({ GUEST: () => true }); + permissionsService.add({ ADMIN: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + expect(getFixtureContent()).toEqual(null); })); - it('Should not show the component when all promises fails', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when a promise fails', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.reject()); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.reject() }); + permissionsService.add({ GUEST: () => Promise.resolve(true) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one of promises returns true', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when one rejects but another one fulfils', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('GUEST', () => true); - permissionsService.add('ADMIN', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.reject() }); + permissionsService.add({ GUEST: () => true }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + expect(getFixtureContent()).toEqual(null); })); - it('Should not show the component when all promises fails', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when one rejects but another one fulfils', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.reject()); - permissionsService.add('GUEST', () => Promise.resolve(true)); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => true }); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one rejects but another one fulfils', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when functions with name and store fulfils', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.reject()); - permissionsService.add('GUEST', () => true); - detectChanges(fixture); - - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); - })); - - it('Should show the component when one rejects but another one fulfils', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); - - permissionsService.add('ADMIN', () => true); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); - - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); - })); - - it('Should show the component when functions with name and store fulfils', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); - - permissionsService.add('ADMIN', (name, store) => { - expect(name).toBeTruthy(); - expect(store[name!].name).toBeTruthy(); - return name === 'ADMIN'; + permissionsService.add({ + ADMIN: (name, store) => { + expect(name).toBeTruthy(); + expect(store[name!].name).toBeTruthy(); + return name === ADMIN; + }, }); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + expect(getFixtureContent()).toEqual(null); })); }); -describe('Permission directive angular testing different async functions in permissions via string', () => { +describe('Permission directive testing different async functions in permissions via string', () => { @Component({ template: `
123
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should show the component when promise returns truthy value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when promise returns truthy value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => true); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => true }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should not show the component when promise returns false value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when promise returns false value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => false); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => false }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when promise returns truthy value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when promise returns truthy value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.resolve(true)); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.resolve(true) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should not show the component when promise rejects', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when promise rejects', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one of the promises fulfills ', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when only one of the promises fulfills', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.resolve()); - permissionsService.add('GUEST', () => Promise.resolve(true)); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.resolve(false) }); + permissionsService.add({ GUEST: () => Promise.resolve(true) }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one of the promises fulfills with 0 value', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when one of the promises fulfills with 0 value', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.resolve()); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.resolve(true) }); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should not show the component when all promises fails', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when all promises fails', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.reject()); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.reject() }); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toEqual(null); + expect(getFixtureContent()).toEqual(null); })); - it('Should show the component when one of promises returns true', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when one of promises returns true', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('GUEST', () => Promise.reject()); - permissionsService.add('ADMIN', () => true); - detectChanges(fixture); + permissionsService.add({ GUEST: () => Promise.reject() }); + permissionsService.add({ ADMIN: () => true }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should not show the component when all promises fails', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should not show the component when all promises fails', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => Promise.resolve(true)); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => Promise.resolve(true) }); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should show the component when one rejects but another one fulfils', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); + it('should show the component when one rejects but another one fulfils', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); - permissionsService.add('ADMIN', () => true); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ ADMIN: () => true }); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); - it('Should show the component when functions with name and store fulfils', fakeAsync(() => { - const content = fixture.debugElement.nativeElement.querySelector('div'); - expect(content).toEqual(null); - permissionsService.add('ADMIN', (name, store) => { - expect(name).toBeTruthy(); - expect(store[name!].name).toBeTruthy(); - return name === 'ADMIN'; + it('should show the component when functions with name and store fulfils', fakeAsync(() => { + expect(getFixtureContent()).toEqual(null); + + permissionsService.add({ + ADMIN: (name, store) => { + expect(name).toBeTruthy(); + expect(store[name!].name).toBeTruthy(); + return name === ADMIN; + }, }); - permissionsService.add('GUEST', () => Promise.reject()); - detectChanges(fixture); + permissionsService.add({ GUEST: () => Promise.reject() }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual('
123
'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('
123
'); })); }); -describe('Permissions directive testing else block', () => { +describe('Permissions directive with else block', () => { @Component({ template: `
main
-
elseBlock
+
elseBlockContent
-
thenBlock
+
thenBlockContent
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should fail and show else block', () => { - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual(`elseBlock`); + it('should show else block when permissions fail', () => { + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual(`elseBlockContent`); }); - it('Should add element remove element and show then block', fakeAsync(() => { - rolesService.add('FAILED_BLOCK', () => true); - detectChanges(fixture); + it('should show else block when permissions change', fakeAsync(() => { + rolesService.add({ FAILED_BLOCK: () => true }); + tick(); - const content3 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content3).toBeTruthy(); - expect(content3.innerHTML).toEqual('main'); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual('main'); rolesService.remove('FAILED_BLOCK'); - detectChanges(fixture); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); + const content2 = getFixtureContent(); expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual(`elseBlock`); + expect(content2.innerHTML).toEqual(`elseBlockContent`); })); }); -describe('Permissions directive testing then block', () => { +describe('Permissions directive with then block', () => { @Component({ template: `
main
-
elseBlock
+
elseBlockContent
-
thenBlock
+
thenBlockContent
`, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Should fail and show then block', fakeAsync(() => { - rolesService.add('THEN_BLOCK', () => true); - detectChanges(fixture); + it('should fail and show then block', fakeAsync(() => { + rolesService.add({ THEN_BLOCK: () => true }); + tick(); - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual(`thenBlock`); + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual(`thenBlockContent`); })); }); -describe('Permissions directive when no permission specified should return true', () => { +describe('Permissions directive with empty argument', () => { @Component({ template: ` @@ -832,18 +777,18 @@ describe('Permissions directive when no permission specified should return true' `, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Except and only should success and show then block', () => { - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual(`123`); + it('should succeed and show the component', () => { + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual(`123`); }); }); -describe('Permissions directive when no permission specified as array should return true', () => { +describe('Permissions directive with an empty array', () => { @Component({ template: ` @@ -851,13 +796,13 @@ describe('Permissions directive when no permission specified as array should ret `, }) - class TestComponent {} + class TestComponent extends BaseTestComponent {} - beforeEach(fakeAsync(() => configureTestBed(TestComponent))); + beforeEach(() => configureTestBed(TestComponent)); - it('Except and only should success and show then block', () => { - const content2 = fixture.debugElement.nativeElement.querySelector('div'); - expect(content2).toBeTruthy(); - expect(content2.innerHTML).toEqual(`123`); + it('should succeed and show the component', () => { + const content = getFixtureContent(); + expect(content).toBeTruthy(); + expect(content.innerHTML).toEqual(`123`); }); }); diff --git a/src/lib/permissions/permissions.directive.ts b/src/lib/permissions/permissions.directive.ts index 4ba9676..b18fc22 100644 --- a/src/lib/permissions/permissions.directive.ts +++ b/src/lib/permissions/permissions.directive.ts @@ -14,9 +14,9 @@ import { import { merge, Subject, Subscription, switchMap } from 'rxjs'; import { tap } from 'rxjs/operators'; -import { notEmpty } from './utils'; import { IqserRolesService } from './services/roles.service'; import { IqserPermissionsService } from './services/permissions.service'; +import { List } from '../utils'; type NgTemplate = TemplateRef; @@ -38,11 +38,11 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { @Output() readonly permissionsAuthorized = new EventEmitter(); @Output() readonly permissionsUnauthorized = new EventEmitter(); - #permissions?: string | string[]; + #permissions?: string | List; #thenTemplateRef: TemplateRef; #elseTemplateRef?: TemplateRef; - #thenViewRef?: EmbeddedViewRef; - #elseViewRef?: EmbeddedViewRef; + #thenViewRef: EmbeddedViewRef | boolean = false; + #elseViewRef: EmbeddedViewRef | boolean = false; readonly #updateView = new Subject(); readonly #subscription = new Subscription(); @@ -58,7 +58,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { } @Input() - set allow(value: string | string[]) { + set allow(value: string | List) { this.#permissions = value; this.#updateView.next(); } @@ -67,7 +67,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { set allowThen(template: NgTemplate) { assertTemplate('allowThen', template); this.#thenTemplateRef = template; - this.#thenViewRef = undefined; + this.#thenViewRef = false; this.#updateView.next(); } @@ -75,7 +75,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { set allowElse(template: NgTemplate) { assertTemplate('allowElse', template); this.#elseTemplateRef = template; - this.#elseViewRef = undefined; + this.#elseViewRef = false; this.#updateView.next(); } @@ -91,21 +91,17 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { } #waitForRolesAndPermissions() { - return merge(this._permissionsService.permissions$, this._rolesService.roles$).pipe( - tap(() => (notEmpty(this.#permissions) ? this.#validate() : this.#showThenBlock())), - ); + return merge(this._permissionsService.permissions$, this._rolesService.roles$).pipe(tap(() => this.#validate())); } #validate(): void { - Promise.all([this._permissionsService.has(this.#permissions), this._rolesService.has(this.#permissions)]) - .then(([hasPermissions, hasRoles]) => { - if (hasPermissions || hasRoles) { - return this.#showThenBlock(); - } + if (!this.#permissions) { + return this.#showThenBlock(); + } - return this.#showElseBlock(); - }) - .catch(() => this.#showElseBlock()); + const promises = [this._permissionsService.has(this.#permissions), this._rolesService.has(this.#permissions)]; + const result = Promise.all(promises).then(([hasPermission, hasRole]) => hasPermission || hasRole); + result.then(isAllowed => (isAllowed ? this.#showThenBlock() : this.#showElseBlock())).catch(() => this.#showElseBlock()); } #showElseBlock() { @@ -114,8 +110,8 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { } this.permissionsUnauthorized.emit(); - this.#thenViewRef = undefined; - this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef); + this.#thenViewRef = false; + this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef) ?? true; } #showThenBlock() { @@ -124,18 +120,13 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit { } this.permissionsAuthorized.emit(); - this.#elseViewRef = undefined; - this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef); + this.#elseViewRef = false; + this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef) ?? true; } #showTemplate(template?: NgTemplate) { this._viewContainer.clear(); - - if (!template) { - return; - } - - return this._viewContainer.createEmbeddedView(template); + return template ? this._viewContainer.createEmbeddedView(template) : undefined; } } diff --git a/src/lib/permissions/services/permissions-guard.service.spec.ts b/src/lib/permissions/services/permissions-guard.service.spec.ts index b6e8fcf..316408a 100644 --- a/src/lib/permissions/services/permissions-guard.service.spec.ts +++ b/src/lib/permissions/services/permissions-guard.service.spec.ts @@ -82,7 +82,7 @@ describe('Permissions guard', () => { expect(result).toEqual(true); }); - it('should return false when only doesnt match', async () => { + it('should return false when allow doesnt match', async () => { testRoute = { data: { permissions: { @@ -440,11 +440,11 @@ describe('Role guard with redirectTo as function', () => { roleService = TestBed.inject(IqserRolesService); permissionsService.add('canReadAgenda'); permissionsService.add('AWESOME'); - roleService.add('ADMIN', ['AWESOME', 'canReadAgenda']); + roleService.add({ ADMIN: ['AWESOME', 'canReadAgenda'] }); }); it('should dynamically pass if one satisfies', async () => { - roleService.add('RUN', ['BLABLA', 'BLABLA2']); + roleService.add({ RUN: ['BLABLA', 'BLABLA2'] }); testRoute = { data: { diff --git a/src/lib/permissions/services/permissions-guard.service.ts b/src/lib/permissions/services/permissions-guard.service.ts index 5be0828..c8e9db3 100644 --- a/src/lib/permissions/services/permissions-guard.service.ts +++ b/src/lib/permissions/services/permissions-guard.service.ts @@ -15,18 +15,20 @@ import { first, mergeMap, tap } from 'rxjs/operators'; import { DEFAULT_REDIRECT_KEY, - IqserPermissionsRouterData, + IqserActivatedRouteSnapshot, + IqserRoute, NavigationCommandsFn, NavigationExtrasFn, RedirectTo, RedirectToFn, -} from '../models/permissions-router-data.model'; +} from '../types'; import { IqserPermissionsService } from './permissions.service'; import { IqserRolesService } from './roles.service'; -import { isFunction, isRedirectWithParameters, isString, transformPermission } from '../utils'; +import { isArray, isFunction, isRedirectWithParameters, isString, transformPermission } from '../utils'; +import { List } from '../../utils'; export interface IqserPermissionsData { - allow: string | string[]; + allow: string | List; redirectTo?: RedirectTo | RedirectToFn; } @@ -38,15 +40,15 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC private readonly _router: Router, ) {} - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + canActivate(route: IqserActivatedRouteSnapshot, state: RouterStateSnapshot) { return this.#checkPermissions(route, state); } - canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + canActivateChild(childRoute: IqserActivatedRouteSnapshot, state: RouterStateSnapshot) { return this.#checkPermissions(childRoute, state); } - canLoad(route: Route) { + canLoad(route: IqserRoute) { return this.#checkPermissions(route); } @@ -58,15 +60,15 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC return this.#validatePermissions(permissions, route, state); } - #checkPermissions(route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) { - const routePermissions = route?.data ? (route.data['permissions'] as IqserPermissionsRouterData) : undefined; + #checkPermissions(route: IqserActivatedRouteSnapshot | IqserRoute, state?: RouterStateSnapshot) { + const routePermissions = route.data?.permissions; if (!routePermissions) { return Promise.resolve(true); } const permissions = transformPermission(routePermissions, route, state); - if (permissions?.allow?.length > 0) { + if (permissions.allow?.length > 0) { return this.#validate(permissions, route, state); } @@ -126,7 +128,6 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC if (failed) { failedPermission = permission; - console.log(`Permission ${permission} is not allowed`); } }), ); @@ -172,7 +173,7 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC return this.#redirectToAnotherRoute(failedPermissionRedirectTo, route, failedPermission, state); } - if (isFunction(permissions.redirectTo) || isString(permissions.redirectTo) || Array.isArray(permissions.redirectTo)) { + if (isFunction(permissions.redirectTo) || isString(permissions.redirectTo) || isArray(permissions.redirectTo)) { return this.#redirectToAnotherRoute(permissions.redirectTo, route, failedPermission, state); } @@ -195,7 +196,7 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC !isFunction(redirectTo) && !isString(redirectTo) && !isRedirectWithParameters(redirectTo) && - !Array.isArray(redirectTo) + !isArray(redirectTo) ) { return redirectTo[failedPermission]; } diff --git a/src/lib/permissions/services/permissions.service.spec.ts b/src/lib/permissions/services/permissions.service.spec.ts index f8f993f..44a36a5 100644 --- a/src/lib/permissions/services/permissions.service.spec.ts +++ b/src/lib/permissions/services/permissions.service.spec.ts @@ -8,6 +8,10 @@ const GUEST = 'GUEST' as const; describe('Permissions Service', () => { let permissionsService: IqserPermissionsService; + function getPermissionsLength() { + return Object.keys(permissionsService.get()).length; + } + beforeEach(() => { TestBed.configureTestingModule({ imports: [IqserPermissionsModule.forRoot()], @@ -35,32 +39,29 @@ describe('Permissions Service', () => { }); it('should remove all permissions from permissions object', () => { - expect(Object.keys(permissionsService.get()).length).toEqual(0); + expect(getPermissionsLength()).toEqual(0); permissionsService.add(ADMIN); permissionsService.add(GUEST); - expect(Object.keys(permissionsService.get()).length).toEqual(2); + expect(getPermissionsLength()).toEqual(2); permissionsService.clear(); - expect(Object.keys(permissionsService.get()).length).toEqual(0); + expect(getPermissionsLength()).toEqual(0); }); it('should add multiple permissions', () => { - expect(Object.keys(permissionsService.get()).length).toEqual(0); + expect(getPermissionsLength()).toEqual(0); permissionsService.add([ADMIN, GUEST]); - expect(Object.keys(permissionsService.get()).length).toEqual(2); - expect(permissionsService.get()).toEqual({ - ADMIN: { name: 'ADMIN' }, - GUEST: { name: 'GUEST' }, - }); + expect(getPermissionsLength()).toEqual(2); + expect(Object.keys(permissionsService.get())).toEqual([ADMIN, GUEST]); }); it('should return true when permission name is present in permissions object', async () => { - expect(Object.keys(permissionsService.get()).length).toEqual(0); + expect(getPermissionsLength()).toEqual(0); permissionsService.add([ADMIN, GUEST]); - expect(Object.keys(permissionsService.get()).length).toEqual(2); + expect(getPermissionsLength()).toEqual(2); let result = await permissionsService.has('ADMIN'); expect(result).toEqual(true); @@ -72,74 +73,75 @@ describe('Permissions Service', () => { expect(result).toEqual(true); result = await permissionsService.has(['ADMIN', 'IRIISISTABLE']); - expect(result).toEqual(true); + expect(result).toEqual(false); }); it('should return true when permission function return true', async () => { - expect(Object.keys(permissionsService.get()).length).toEqual(0); + expect(getPermissionsLength()).toEqual(0); - permissionsService.add(ADMIN, () => true); - expect(Object.keys(permissionsService.get()).length).toEqual(1); + permissionsService.add({ ADMIN: () => true }); + expect(getPermissionsLength()).toEqual(1); let result = await permissionsService.has('ADMIN'); expect(result).toEqual(true); - permissionsService.add(GUEST, () => false); - expect(Object.keys(permissionsService.get()).length).toEqual(2); + permissionsService.add({ GUEST: () => false }); + expect(getPermissionsLength()).toEqual(2); result = await permissionsService.has('GUEST'); expect(result).toEqual(false); - permissionsService.add('TEST1', () => Promise.resolve(true)); - expect(Object.keys(permissionsService.get()).length).toEqual(3); + permissionsService.add({ TEST1: () => Promise.resolve(true) }); + expect(getPermissionsLength()).toEqual(3); result = await permissionsService.has('TEST1'); expect(result).toEqual(true); - permissionsService.add('TEST2', () => Promise.resolve(false)); - expect(Object.keys(permissionsService.get()).length).toEqual(4); + permissionsService.add({ TEST2: () => Promise.resolve(false) }); + expect(getPermissionsLength()).toEqual(4); result = await permissionsService.has('TEST2'); expect(result).toEqual(false); }); - // TODO: permissions array with function should not be allowed it('should return true when permissions array function return true', async () => { - expect(Object.keys(permissionsService.get()).length).toEqual(0); + expect(getPermissionsLength()).toEqual(0); - permissionsService.add([ADMIN], () => true); - expect(Object.keys(permissionsService.get()).length).toEqual(1); + permissionsService.add({ ADMIN: () => true }); + expect(getPermissionsLength()).toEqual(1); let result = await permissionsService.has('ADMIN'); expect(result).toEqual(true); - permissionsService.add([GUEST], () => false); - expect(Object.keys(permissionsService.get()).length).toEqual(2); + permissionsService.add({ GUEST: () => false }); + expect(getPermissionsLength()).toEqual(2); result = await permissionsService.has('GUEST'); expect(result).toEqual(false); - permissionsService.add(['TEST1'], () => Promise.resolve(true)); - expect(Object.keys(permissionsService.get()).length).toEqual(3); + permissionsService.add({ TEST1: () => Promise.resolve(true) }); + expect(getPermissionsLength()).toEqual(3); result = await permissionsService.has('TEST1'); expect(result).toEqual(true); - permissionsService.add(['TEST9'], () => Promise.resolve(false)); - expect(Object.keys(permissionsService.get()).length).toEqual(4); + permissionsService.add({ TEST9: () => Promise.resolve(false) }); + expect(getPermissionsLength()).toEqual(4); result = await permissionsService.has(['TEST9']); expect(result).toEqual(false); }); it('should call validationFn with permission name and store', async () => { - permissionsService.add('TEST11', (name, store) => { - expect(name).toEqual('TEST11'); - expect(store['TEST11']).toBeTruthy(); - return Promise.resolve(true); + permissionsService.add({ + TEST11: (name, store) => { + expect(name).toEqual('TEST11'); + expect(store['TEST11']).toBeTruthy(); + return Promise.resolve(true); + }, }); - expect(Object.keys(permissionsService.get()).length).toEqual(1); + expect(getPermissionsLength()).toEqual(1); const result = await permissionsService.has(['TEST11']); expect(result).toEqual(true); diff --git a/src/lib/permissions/services/permissions.service.ts b/src/lib/permissions/services/permissions.service.ts index 244bf14..e0ba415 100644 --- a/src/lib/permissions/services/permissions.service.ts +++ b/src/lib/permissions/services/permissions.service.ts @@ -1,103 +1,79 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, firstValueFrom, from, Observable, of } from 'rxjs'; -import { catchError, first, map, mergeAll, switchMap } from 'rxjs/operators'; +import { BehaviorSubject, Observable } from 'rxjs'; -import { isFunction, toArray } from '../utils'; -import { ValidationFn } from '../models/permissions-router-data.model'; -import { IqserPermission, IqserPermissionsObject } from '../models/permission.model'; +import { isArray, isString, toArray } from '../utils'; +import { IqserPermissions, PermissionValidationFn } from '../types'; +import { List } from '../../utils'; @Injectable() export class IqserPermissionsService { - readonly permissions$: Observable; - readonly #permissions$ = new BehaviorSubject({}); + readonly permissions$: Observable; + readonly #permissions$ = new BehaviorSubject({}); constructor() { this.permissions$ = this.#permissions$.asObservable(); } - /** - * Remove all permissions from permissions source - */ clear(): void { this.#permissions$.next({}); } - has(permission?: string | string[]): Promise { - if (!permission || (Array.isArray(permission) && permission.length === 0)) { - return Promise.resolve(true); - } - - permission = toArray(permission); - return this.#hasArray(permission); + has(permission: string): Promise; + has(permissions: List): Promise; + has(permissions: string | List): Promise; + has(value: string | List): Promise { + const isEmpty = !value || value.length === 0; + return isEmpty ? Promise.resolve(true) : this.#has(toArray(value)); } - load(permissions: string[], validationFn?: ValidationFn): void { - const newPermissions = permissions.reduce((source, name) => this.#reduce(source, name, validationFn), {}); - this.#permissions$.next(newPermissions); + load(permissions: IqserPermissions | List) { + return this.#reduce(permissions); } - add(permission: string | string[], validationFn?: ValidationFn) { - const permissions = toArray(permission).reduce( - (source, name) => this.#reduce(source, name, validationFn), - this.#permissions$.value, - ); - - return this.#permissions$.next(permissions); + add(permissions: string | List | IqserPermissions) { + const _permissions = isString(permissions) ? { [permissions]: () => true } : permissions; + return this.#reduce(_permissions, this.#permissions$.value); } - remove(name: string): void { + remove(permission: string) { const permissions = { ...this.#permissions$.value }; - delete permissions[name]; + delete permissions[permission]; this.#permissions$.next(permissions); } - get(): IqserPermissionsObject; - get(name: string): IqserPermission | undefined; - get(name?: string): IqserPermission | IqserPermissionsObject | undefined { - return name ? this.#permissions$.value[name] : this.#permissions$.value; + get(): IqserPermissions; + get(permission: string): PermissionValidationFn | undefined; + get(permission?: string): IqserPermissions | PermissionValidationFn | undefined { + return permission ? this.#permissions$.value[permission] : this.#permissions$.value; } - #reduce(source: IqserPermissionsObject, name: string, validationFn?: ValidationFn): IqserPermissionsObject { - if (!!validationFn && isFunction(validationFn)) { - return { ...source, [name]: { name, validationFn } }; + #reduce(permissions: IqserPermissions | List, initialValue = {} as IqserPermissions) { + if (isArray(permissions)) { + return this.#permissions$.next(this.#reduceList(permissions, initialValue)); } - return { ...source, [name]: { name } }; + return this.#permissions$.next(this.#reduceObject(permissions, initialValue)); } - #hasArray(permissions: string[]): Promise { - const promises = permissions.map(key => { - if (this.#hasValidationFn(key)) { - const validationFunction = this.#permissions$.value[key].validationFn; - if (!validationFunction) { - return of(false); - } - const immutableValue = { ...this.#permissions$.value }; + #reduceObject(permissions: IqserPermissions, initialValue: IqserPermissions = {}) { + return Object.entries(permissions).reduce((acc, [permission, validationFn]) => { + return { ...acc, [permission]: validationFn }; + }, initialValue); + } - return of(null).pipe( - switchMap(async () => validationFunction(key, immutableValue)), - catchError(() => of(false)), - ); - } + #reduceList(permissions: List, initialValue: IqserPermissions = {}): IqserPermissions { + return permissions.reduce((acc, permission) => { + return { ...acc, [permission]: () => true }; + }, initialValue); + } - return of(!!this.#permissions$.value[key]); + #has(permissions: List): Promise { + const promises = permissions.map(permission => { + const validationFn = this.#permissions$.value[permission]; + return validationFn?.(permission, this.#permissions$.value) ?? false; }); - const res = from(promises).pipe( - mergeAll(), - first(data => data !== false, false), - map(data => data !== false), - ); - - return firstValueFrom(res); - } - - #hasValidationFn(key: string): boolean { - return ( - !!this.#permissions$.value[key] && - !!this.#permissions$.value[key].validationFn && - isFunction(this.#permissions$.value[key].validationFn) - ); + return Promise.all(promises).then(results => results.every(result => result)); } } diff --git a/src/lib/permissions/services/roles.service.spec.ts b/src/lib/permissions/services/roles.service.spec.ts index e1c904b..908e50a 100644 --- a/src/lib/permissions/services/roles.service.spec.ts +++ b/src/lib/permissions/services/roles.service.spec.ts @@ -1,17 +1,23 @@ import { TestBed } from '@angular/core/testing'; -import { ValidationFn } from '../models/permissions-router-data.model'; +import { RoleValidationFn } from '../types'; import { IqserRolesService } from './roles.service'; import { IqserPermissionsService } from './permissions.service'; import { IqserPermissionsModule } from '../permissions.module'; -import { IqserRole } from '../models/role.model'; const ADMIN = 'ADMIN' as const; -const GUEST = 'GUEST' as const; describe('Roles Service', () => { let rolesService: IqserRolesService; let permissionsService: IqserPermissionsService; + function getRolesLength() { + return Object.keys(rolesService.get()).length; + } + + function getPermissionsLength() { + return Object.keys(permissionsService.get()).length; + } + beforeEach(() => { TestBed.configureTestingModule({ imports: [IqserPermissionsModule.forRoot()], @@ -28,16 +34,16 @@ describe('Roles Service', () => { it('should add role to role object', () => { expect(rolesService.get(ADMIN)).toBeFalsy(); - rolesService.add(ADMIN, ['edit', 'remove']); + rolesService.add({ ADMIN: ['edit', 'remove'] }); expect(rolesService.get(ADMIN)).toBeTruthy(); - expect(rolesService.get()).toEqual({ ADMIN: { name: ADMIN, validationFn: ['edit', 'remove'] } }); + expect(rolesService.get()).toEqual({ ADMIN: ['edit', 'remove'] }); }); it('should remove role from role object', () => { expect(rolesService.get(ADMIN)).toBeFalsy(); - rolesService.add(ADMIN, ['edit', 'remove']); + rolesService.add({ ADMIN: ['edit', 'remove'] }); expect(rolesService.get(ADMIN)).toBeTruthy(); rolesService.remove(ADMIN); @@ -45,39 +51,41 @@ describe('Roles Service', () => { }); it('should remove all roles from object', () => { - expect(Object.keys(rolesService.get()).length).toEqual(0); + expect(getRolesLength()).toEqual(0); - rolesService.add(ADMIN, ['edit', 'remove']); - rolesService.add(GUEST, ['edit', 'remove']); - expect(Object.keys(rolesService.get()).length).toEqual(2); + rolesService.add({ + ADMIN: ['edit', 'remove'], + GUEST: ['edit', 'remove'], + }); + expect(getRolesLength()).toEqual(2); rolesService.clear(); - expect(Object.keys(rolesService.get()).length).toEqual(0); + expect(getRolesLength()).toEqual(0); }); it('should add multiple roles', () => { - expect(Object.keys(rolesService.get()).length).toEqual(0); + expect(getRolesLength()).toEqual(0); rolesService.add({ ADMIN: ['Nice'], GUEST: ['Awesome'], }); - expect(Object.keys(rolesService.get()).length).toEqual(2); + expect(getRolesLength()).toEqual(2); expect(rolesService.get()).toEqual({ - ADMIN: { name: ADMIN, validationFn: ['Nice'] }, - GUEST: { name: GUEST, validationFn: ['Awesome'] }, + ADMIN: ['Nice'], + GUEST: ['Awesome'], }); }); - it('return true when role name is present in Roles object', async () => { - expect(Object.keys(rolesService.get()).length).toEqual(0); + it('return true when role name is present in roles object', async () => { + expect(getRolesLength()).toEqual(0); rolesService.add({ ADMIN: ['Nice'], GUEST: ['Awesome'], }); - expect(Object.keys(rolesService.get()).length).toEqual(2); + expect(getRolesLength()).toEqual(2); let result = await rolesService.has(ADMIN); expect(result).toEqual(true); @@ -86,23 +94,24 @@ describe('Roles Service', () => { expect(result).toEqual(false); result = await rolesService.has([ADMIN, 'IRIISISTABLE']); - expect(result).toEqual(true); + expect(result).toEqual(false); }); it('return true when role permission name is present in Roles object', async () => { - expect(Object.keys(rolesService.get()).length).toEqual(0); + expect(getRolesLength()).toEqual(0); rolesService.add({ ADMIN: ['Nice'], GUEST: ['Awesome'], }); - expect(Object.keys(rolesService.get()).length).toEqual(2); + expect(getRolesLength()).toEqual(2); + let result = await rolesService.has(ADMIN); expect(result).toEqual(true); result = await rolesService.has([ADMIN, 'IRRISISTABLE']); - expect(result).toEqual(true); + expect(result).toEqual(false); result = await rolesService.has('SHOULDNOTHAVEROLE'); expect(result).toEqual(false); @@ -112,17 +121,11 @@ describe('Roles Service', () => { }); it('should return role when requested with has role', () => { - rolesService.add('role', () => true); - const role = rolesService.get('role'); + rolesService.add({ role: () => true }); + const validationFn = rolesService.get('role') as RoleValidationFn; - if (!role) { - return expect(role).toBeTruthy(); - } - - expect(role.name).toBe('role'); - - const validationFn = role.validationFn as ValidationFn; - expect(validationFn(role.name, rolesService.get())).toEqual(true); + expect(validationFn).toBeTruthy(); + expect(validationFn('role', rolesService.get())).toEqual(true); }); it('should return true when checking with empty permission(not specified)', async () => { @@ -136,31 +139,32 @@ describe('Roles Service', () => { }); it('should return false when role is not specified in the list', async () => { - rolesService.add('test', ['One']); + rolesService.add({ test: ['One'] }); const result = await rolesService.has('nice'); expect(result).toBe(false); }); it('should return true when passing empty array', async () => { - rolesService.add('test', ['One']); + rolesService.add({ test: ['One'] }); const result = await rolesService.has([]); expect(result).toBe(true); }); it('should add permissions to roles automatically', async () => { - rolesService.add('test', ['one', 'two']); + rolesService.add({ test: ['one', 'two'] }); const result = await rolesService.has('test'); expect(result).toBe(true); }); it('should remove roles and permissions add the same time', async () => { - rolesService.add('test', ['one', 'two']); + rolesService.add({ test: ['one', 'two'] }); let result = await rolesService.has('test'); expect(result).toBe(true); + result = await permissionsService.has('one'); expect(result).toBe(true); @@ -169,41 +173,42 @@ describe('Roles Service', () => { result = await rolesService.has('test'); expect(result).toBe(false); + result = await permissionsService.has('one'); expect(result).toBe(false); }); it('should remove all permissions and roles', () => { - expect(Object.keys(rolesService.get()).length).toEqual(0); + expect(getRolesLength()).toEqual(0); - rolesService.add(ADMIN, ['edit', 'remove']); - rolesService.add(GUEST, ['edit1', 'remove2']); + rolesService.add({ ADMIN: ['edit', 'remove'] }); + rolesService.add({ GUEST: ['edit1', 'remove2'] }); - expect(Object.keys(rolesService.get()).length).toEqual(2); - expect(Object.keys(permissionsService.get()).length).toEqual(4); + expect(getRolesLength()).toEqual(2); + expect(getPermissionsLength()).toEqual(4); rolesService.clear(); permissionsService.clear(); - expect(Object.keys(rolesService.get()).length).toEqual(0); - expect(Object.keys(permissionsService.get()).length).toEqual(0); + expect(getRolesLength()).toEqual(0); + expect(getPermissionsLength()).toEqual(0); }); it('should add multiple roles with permissions', () => { - expect(Object.keys(rolesService.get()).length).toEqual(0); + expect(getRolesLength()).toEqual(0); rolesService.add({ ADMIN: ['Nice'], GUEST: ['Awesome', 'Another awesome'], }); - expect(Object.keys(rolesService.get()).length).toEqual(2); + expect(getRolesLength()).toEqual(2); expect(rolesService.get()).toEqual({ - ADMIN: { name: ADMIN, validationFn: ['Nice'] }, - GUEST: { name: GUEST, validationFn: ['Awesome', 'Another awesome'] }, + ADMIN: ['Nice'], + GUEST: ['Awesome', 'Another awesome'], }); - expect(Object.keys(rolesService.get()).length).toEqual(2); - expect(Object.keys(permissionsService.get()).length).toEqual(3); + expect(getRolesLength()).toEqual(2); + expect(getPermissionsLength()).toEqual(3); }); }); diff --git a/src/lib/permissions/services/roles.service.ts b/src/lib/permissions/services/roles.service.ts index 1e48b1b..de3b396 100644 --- a/src/lib/permissions/services/roles.service.ts +++ b/src/lib/permissions/services/roles.service.ts @@ -1,38 +1,27 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject, firstValueFrom, from, Observable, of } from 'rxjs'; -import { catchError, every, first, map, mergeAll, mergeMap, switchMap } from 'rxjs/operators'; - +import { BehaviorSubject, Observable } from 'rxjs'; +import { IqserRoles, RoleValidationFn } from '../types'; +import { isArray, isBoolean, isString, toArray } from '../utils'; +import { List } from '../../utils'; import { IqserPermissionsService } from './permissions.service'; -import { ValidationFn } from '../models/permissions-router-data.model'; -import { IqserRole } from '../models/role.model'; -import { isFunction, isString, toArray } from '../utils'; - -export interface IqserRolesObject { - [name: string]: IqserRole; -} @Injectable() export class IqserRolesService { - readonly roles$: Observable; - readonly #roles$ = new BehaviorSubject({}); + readonly roles$: Observable; + readonly #roles$ = new BehaviorSubject({}); constructor(private readonly _permissionsService: IqserPermissionsService) { this.roles$ = this.#roles$.asObservable(); } - add(role: string, validationFn: ValidationFn | string[]): void; - add(rolesObj: Record | string[]>): void; - add(role: string | Record | string[]>, validationFn?: ValidationFn | string[]) { - if (isString(role) && validationFn) { - return this.#add(role, validationFn); - } + add(roles: string | List | IqserRoles) { + const _roles = isString(roles) ? { [roles]: () => true } : roles; + this.#reduce(_roles, this.#roles$.value); + } - if (typeof role === 'object') { - return Object.keys(role).forEach(key => this.#add(key, role[key])); - } - - throw new Error('Invalid add role arguments'); + load(roles: List | IqserRoles) { + this.#reduce(roles); } clear() { @@ -45,85 +34,55 @@ export class IqserRolesService { this.#roles$.next(roles); } - get(): IqserRolesObject; - - get(role: string): IqserRole | undefined; - - get(role?: string): IqserRolesObject | IqserRole | undefined { + get(): IqserRoles; + get(role: string): RoleValidationFn | List | undefined; + get(role?: string): IqserRoles | RoleValidationFn | List | undefined { return role ? this.#roles$.value[role] : this.#roles$.value; } - has(names?: string | string[]): Promise { - const isNamesEmpty = !names || (Array.isArray(names) && names.length === 0); + has(roles: string | List): Promise { + const isEmpty = !roles || roles.length === 0; - if (isNamesEmpty) { + if (isEmpty) { return Promise.resolve(true); } - names = toArray(names); - - return Promise.all([this.#hasRoleKey(names), this.#hasPermission(names)]).then(([hasRoles, hasPermissions]) => { - return !!hasRoles || !!hasPermissions; - }); + const validations = toArray(roles).map(role => this.#runValidation(role)); + return Promise.all(validations) + .then(results => this.#checkPermissionsIfNeeded(results)) + .then(results => results.every(result => result === true)); } - #add(role: string, validationFn: ValidationFn | string[]) { - const roles: IqserRolesObject = { - ...this.#roles$.value, - [role]: { name: role, validationFn }, - }; + #checkPermissionsIfNeeded(results: List) { + return results.map(result => (isBoolean(result) ? result : this._permissionsService.has(result))); + } - if (Array.isArray(validationFn)) { - this._permissionsService.add(validationFn); + #runValidation(role: string) { + const validationFn = this.#roles$.value[role]; + + if (isArray(validationFn)) { + return this._permissionsService.has(validationFn); } - return this.#roles$.next(roles); + return validationFn?.(role, this.#roles$.value) ?? false; } - #hasRoleKey(roleName: string[]): Promise { - const promises = roleName.map(key => { - const role = this.#roles$.value[key]; - const hasValidationFn = !!role && !!role.validationFn; + #reduce(roles: List | IqserRoles, initialValue: IqserRoles = {}) { + const result = isArray(roles) ? this.#reduceList(roles, initialValue) : this.#reduceObject(roles, initialValue); + return this.#roles$.next(result); + } - if (hasValidationFn && isFunction>(role.validationFn)) { - const validationFn = role.validationFn; - const immutableValue = { ...this.#roles$.value }; - - return of(null).pipe( - switchMap(async () => validationFn(key, immutableValue)), - catchError(() => of(false)), - ); + #reduceObject(roles: IqserRoles, initialValue: IqserRoles = {}) { + return Object.entries(roles).reduce((acc, [role, validationFn]) => { + if (isArray(validationFn)) { + this._permissionsService.add(validationFn); } - return of(false); - }); - - const res = from(promises).pipe( - mergeAll(), - first(data => data !== false, false), - map(data => data !== false), - ); - - return firstValueFrom(res); + return { ...acc, [role]: validationFn }; + }, initialValue); } - #hasPermission(roleNames: string[]): Promise { - const res = from(roleNames).pipe( - mergeMap(key => { - const role = this.#roles$.value[key]; - - if (role && !isFunction>(role.validationFn)) { - return from(role.validationFn).pipe( - mergeMap(permission => this._permissionsService.has(permission)), - every(hasPermission => hasPermission === true), - ); - } - - return of(false); - }), - first(hasPermission => hasPermission === true, false), - ); - - return firstValueFrom(res); + #reduceList(roles: List, initialValue: IqserRoles = {}) { + return roles.reduce((acc, role) => ({ ...acc, [role]: () => true }), initialValue); } } diff --git a/src/lib/permissions/models/permissions-router-data.model.ts b/src/lib/permissions/types.ts similarity index 61% rename from src/lib/permissions/models/permissions-router-data.model.ts rename to src/lib/permissions/types.ts index d194852..59bb440 100644 --- a/src/lib/permissions/models/permissions-router-data.model.ts +++ b/src/lib/permissions/types.ts @@ -1,7 +1,11 @@ import { ActivatedRouteSnapshot, NavigationExtras, Route, RouterStateSnapshot } from '@angular/router'; +import { List } from '../utils'; + +export type IqserPermissions = Record; +export type IqserRoles = Record; export interface IqserPermissionsRouterData { - allow: string | string[] | AllowFn; + allow: string | List | AllowFn; redirectTo?: RedirectTo | RedirectToFn; } @@ -10,29 +14,29 @@ export interface IqserRedirectToNavigationParameters { navigationExtras?: NavigationExtras | NavigationExtrasFn; } -export type AllowFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => string | string[]; +export type AllowFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => string | List; export type RedirectTo = | string - | string[] + | List | IqserRedirectToNavigationParameters - | { [name: string]: IqserRedirectToNavigationParameters | string | RedirectToFn }; + | Record; export type RedirectToFn = (failedPermission: string, route?: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => RedirectTo; - export type NavigationCommandsFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => any[]; export type NavigationExtrasFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => NavigationExtras; -export type ValidationFn = (name: string, store: Record) => Promise | boolean | string[]; +export type RoleValidationFn = (name: string, store: IqserRoles) => Promise | boolean | List; +export type PermissionValidationFn = (name: string, store: IqserPermissions) => Promise | boolean; export type IqserActivatedRouteSnapshot = ActivatedRouteSnapshot & { data: { - permissions: IqserPermissionsRouterData; + permissions?: IqserPermissionsRouterData; }; }; export type IqserRoute = Route & { data: { - permissions: IqserPermissionsRouterData; + permissions?: IqserPermissionsRouterData; }; }; diff --git a/src/lib/permissions/utils.ts b/src/lib/permissions/utils.ts index f1b6f9e..da56331 100644 --- a/src/lib/permissions/utils.ts +++ b/src/lib/permissions/utils.ts @@ -1,6 +1,7 @@ -import { AllowFn, IqserPermissionsRouterData, IqserRedirectToNavigationParameters } from './models/permissions-router-data.model'; +import { AllowFn, IqserPermissionsRouterData, IqserRedirectToNavigationParameters } from './types'; import { ActivatedRouteSnapshot, Route, RouterStateSnapshot } from '@angular/router'; import { IqserPermissionsData } from './services/permissions-guard.service'; +import { List } from '../utils'; export function isFunction(value: unknown): value is T { return typeof value === 'function'; @@ -16,18 +17,20 @@ export function isPlainObject(value: unknown): boolean { } export function isString(value: unknown): value is string { - return !!value && typeof value === 'string'; + return typeof value === 'string'; } -export function notEmpty(value?: string | string[]): boolean { - return Array.isArray(value) ? value.length > 0 : !!value; +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; } -export function toArray(value?: string | string[]): string[] { - if (isString(value)) { - return [value]; - } - return value ?? []; +export function isArray(value: unknown): value is List; +export function isArray(value: unknown): value is T[] { + return Array.isArray(value); +} + +export function toArray(value?: string | List): List { + return isString(value) ? [value] : value ?? []; } export function isRedirectWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters { @@ -39,11 +42,11 @@ export function transformPermission( route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot, ): IqserPermissionsData { - const only = isFunction(permissions.allow) ? permissions.allow(route, state) : toArray(permissions.allow); + const allow = isFunction(permissions.allow) ? permissions.allow(route, state) : toArray(permissions.allow); const redirectTo = permissions.redirectTo; return { - allow: only, + allow, redirectTo, }; }