From a36d16c39fcafb6f6ce7a313d0e8dffa2c2666c2 Mon Sep 17 00:00:00 2001 From: Youssef Bouchara <101522419+YoussefBouch@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:18:52 +0200 Subject: [PATCH] SPSH-816-permissions-rollen (#564) * findRollen only with permissions * fixed failing test * more tests * fixed array check * check for Rollen * SPSH-816: PR Review --------- Co-authored-by: Phael Mouko --- .../api/rolle.controller.integration-spec.ts | 41 ++++++++++- .../rolle/api/rolle.controller.spec.ts | 6 +- src/modules/rolle/api/rolle.controller.ts | 21 +++--- src/modules/rolle/repo/rolle.repo.spec.ts | 70 +++++++++++++++++++ src/modules/rolle/repo/rolle.repo.ts | 39 ++++++++++- 5 files changed, 163 insertions(+), 14 deletions(-) diff --git a/src/modules/rolle/api/rolle.controller.integration-spec.ts b/src/modules/rolle/api/rolle.controller.integration-spec.ts index 0c1af824b..3d492e642 100644 --- a/src/modules/rolle/api/rolle.controller.integration-spec.ts +++ b/src/modules/rolle/api/rolle.controller.integration-spec.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import { EntityManager, MikroORM } from '@mikro-orm/core'; -import { INestApplication } from '@nestjs/common'; -import { APP_PIPE } from '@nestjs/core'; +import { CallHandler, ExecutionContext, INestApplication } from '@nestjs/common'; +import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { Test, TestingModule } from '@nestjs/testing'; import request, { Response } from 'supertest'; import { App } from 'supertest/types.js'; @@ -30,6 +30,11 @@ import { RolleWithServiceProvidersResponse } from './rolle-with-serviceprovider. import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; import { PagedResponse } from '../../../shared/paging/index.js'; import { ServiceProviderIdNameResponse } from './serviceprovider-id-name.response.js'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Observable } from 'rxjs'; +import { PersonPermissionsRepo } from '../../authentication/domain/person-permission.repo.js'; +import { PassportUser } from '../../authentication/types/user.js'; +import { Request } from 'express'; describe('Rolle API', () => { let app: INestApplication; @@ -37,6 +42,7 @@ describe('Rolle API', () => { let em: EntityManager; let rolleRepo: RolleRepo; let serviceProviderRepo: ServiceProviderRepo; + let personpermissionsRepoMock: DeepMocked; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -51,6 +57,24 @@ describe('Rolle API', () => { provide: APP_PIPE, useClass: GlobalValidationPipe, }, + { + provide: APP_INTERCEPTOR, + useValue: { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req: Request = context.switchToHttp().getRequest(); + req.passportUser = createMock({ + async personPermissions() { + return personpermissionsRepoMock.loadPersonPermissions(''); + }, + }); + return next.handle(); + }, + }, + }, + { + provide: PersonPermissionsRepo, + useValue: createMock(), + }, OrganisationRepository, RolleFactory, ServiceProviderRepo, @@ -61,6 +85,7 @@ describe('Rolle API', () => { em = module.get(EntityManager); rolleRepo = module.get(RolleRepo); serviceProviderRepo = module.get(ServiceProviderRepo); + personpermissionsRepoMock = module.get(PersonPermissionsRepo); await DatabaseTestModule.setupDatabase(module.get(MikroORM)); app = module.createNestApplication(); await app.init(); @@ -211,6 +236,18 @@ describe('Rolle API', () => { expect(pagedResponse.items).toHaveLength(3); }); + it('should return no rollen', async () => { + const response: Response = await request(app.getHttpServer() as App) + .get('/rolle') + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + const pagedResponse: PagedResponse = + response.body as PagedResponse; + expect(pagedResponse.items).toHaveLength(0); + }); + it('should return rollen with the given queried name', async () => { const testRolle: { name: string } = await rolleRepo.save(DoFactory.createRolle(false)); diff --git a/src/modules/rolle/api/rolle.controller.spec.ts b/src/modules/rolle/api/rolle.controller.spec.ts index 851871b11..1a55a8441 100644 --- a/src/modules/rolle/api/rolle.controller.spec.ts +++ b/src/modules/rolle/api/rolle.controller.spec.ts @@ -14,6 +14,7 @@ import { FindRolleByIdParams } from './find-rolle-by-id.params.js'; import { OrganisationService } from '../../organisation/domain/organisation.service.js'; import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; import { RolleNameQueryParams } from './rolle-name-query.param.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; describe('Rolle API with mocked ServiceProviderRepo', () => { let rolleRepoMock: DeepMocked; @@ -84,11 +85,14 @@ describe('Rolle API with mocked ServiceProviderRepo', () => { const params: RolleNameQueryParams = { searchStr: faker.string.alpha(), }; + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + //mock getRollenByName rolleRepoMock.findByName.mockResolvedValueOnce(undefined); //mock call to get sp (direct in controller-method) serviceProviderRepoMock.findById.mockResolvedValueOnce(undefined); - await expect(rolleController.findRollen(params)).resolves.not.toThrow(Error); + await expect(rolleController.findRollen(params, permissions)).resolves.not.toThrow(Error); }); }); }); diff --git a/src/modules/rolle/api/rolle.controller.ts b/src/modules/rolle/api/rolle.controller.ts index 4bd325dde..6e73ebb4b 100644 --- a/src/modules/rolle/api/rolle.controller.ts +++ b/src/modules/rolle/api/rolle.controller.ts @@ -49,6 +49,8 @@ import { ServiceProviderResponse } from '../../service-provider/api/service-prov import { SchulConnexError } from '../../../shared/error/schul-connex.error.js'; import { RolleExceptionFilter } from './rolle-exception-filter.js'; import { Paged, PagedResponse, PagingHeadersObject } from '../../../shared/paging/index.js'; +import { Permissions } from '../../authentication/api/permissions.decorator.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; @UseFilters(new SchulConnexValidationErrorFilter(), new RolleExceptionFilter()) @ApiTags('rolle') @@ -75,18 +77,16 @@ export class RolleController { @ApiInternalServerErrorResponse({ description: 'Internal server error while getting all rollen.' }) public async findRollen( @Query() queryParams: RolleNameQueryParams, + @Permissions() permissions: PersonPermissions, ): Promise> { - let rollen: Option[]>; - - if (queryParams.searchStr) { - rollen = await this.rolleRepo.findByName(queryParams.searchStr, queryParams.limit, queryParams.offset); - } else { - rollen = await this.rolleRepo.find(queryParams.limit, queryParams.offset); - } - - const serviceProviders: ServiceProvider[] = await this.serviceProviderRepo.find(); + const rollen: Option[]> = await this.rolleRepo.findRollenAuthorized( + permissions, + queryParams.searchStr, + queryParams.limit, + queryParams.offset, + ); - if (!rollen) { + if (!rollen || rollen.length === 0) { const pagedRolleWithServiceProvidersResponse: Paged = { total: 0, offset: 0, @@ -95,6 +95,7 @@ export class RolleController { }; return new PagedResponse(pagedRolleWithServiceProvidersResponse); } + const serviceProviders: ServiceProvider[] = await this.serviceProviderRepo.find(); const rollenWithServiceProvidersResponses: RolleWithServiceProvidersResponse[] = rollen.map( (r: Rolle) => { diff --git a/src/modules/rolle/repo/rolle.repo.spec.ts b/src/modules/rolle/repo/rolle.repo.spec.ts index 1c3d67e3d..54a7df4f0 100644 --- a/src/modules/rolle/repo/rolle.repo.spec.ts +++ b/src/modules/rolle/repo/rolle.repo.spec.ts @@ -16,6 +16,9 @@ import { ServiceProviderRepo } from '../../service-provider/repo/service-provide import { ServiceProvider } from '../../service-provider/domain/service-provider.js'; import { EventService } from '../../../core/eventbus/index.js'; import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { OrganisationID } from '../../../shared/types/index.js'; describe('RolleRepo', () => { let module: TestingModule; @@ -118,7 +121,74 @@ describe('RolleRepo', () => { expect(rolle).toBeNull(); }); }); + describe('findRollenAuthorized', () => { + it('should return no rollen because there are none', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationId]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, undefined, 10, 0); + + expect(rolleResult?.length).toBe(0); + }); + + it('should return the rollen when permissions are sufficient', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + await sut.save(DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisationId })); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationId]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, undefined, 10, 0); + + expect(rolleResult?.length).toBe(1); + }); + + it('should return empty array when permissions are insufficient', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + await sut.save(DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisationId })); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, undefined, 10, 0); + + expect(rolleResult?.length).toBe(0); + }); + + it('should filter rollen based on search string and permissions', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + await sut.save( + DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisationId, name: 'Test' }), + ); + await sut.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: organisationId, + name: 'AnotherName', + }), + ); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationId]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, 'Test', 10, 0); + + expect(rolleResult?.length).toBe(1); + }); + + it('should return all rollen when no search string is provided and permissions are sufficient', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + await sut.save(DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisationId })); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationId]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, undefined, 10, 0); + + expect(rolleResult?.length).toBe(1); + }); + }); describe('findByName', () => { it('should return the rolle', async () => { const rolle: Rolle = await sut.save(DoFactory.createRolle(false)); diff --git a/src/modules/rolle/repo/rolle.repo.ts b/src/modules/rolle/repo/rolle.repo.ts index de63c2275..7b72a8896 100644 --- a/src/modules/rolle/repo/rolle.repo.ts +++ b/src/modules/rolle/repo/rolle.repo.ts @@ -7,8 +7,9 @@ import { RolleMerkmalEntity } from '../entity/rolle-merkmal.entity.js'; import { RolleEntity } from '../entity/rolle.entity.js'; import { RolleFactory } from '../domain/rolle.factory.js'; import { RolleServiceProviderEntity } from '../entity/rolle-service-provider.entity.js'; -import { RolleID } from '../../../shared/types/index.js'; +import { OrganisationID, RolleID } from '../../../shared/types/index.js'; import { RolleSystemrechtEntity } from '../entity/rolle-systemrecht.entity.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; /** * @deprecated Not for use outside of rolle-repo, export will be removed at a later date @@ -129,6 +130,42 @@ export class RolleRepo { return rollen.map((rolle: RolleEntity) => mapEntityToAggregate(rolle, this.rolleFactory)); } + public async findRollenAuthorized( + permissions: PersonPermissions, + searchStr?: string, + limit?: number, + offset?: number, + ): Promise[]>> { + let rollen: Option; + if (searchStr) { + rollen = await this.em.find( + this.entityName, + { name: { $ilike: '%' + searchStr + '%' } }, + { populate: ['merkmale', 'systemrechte', 'serviceProvider'] as const, limit: limit, offset: offset }, + ); + } else { + rollen = await this.em.findAll(this.entityName, { + populate: ['merkmale', 'systemrechte', 'serviceProvider'] as const, + limit: limit, + offset: offset, + }); + } + if (rollen.length === 0) { + return []; + } + + const orgIdsWithRecht: OrganisationID[] = await permissions.getOrgIdsWithSystemrecht( + [RollenSystemRecht.ROLLEN_VERWALTEN], + true, + ); + + const filteredRollen: RolleEntity[] = rollen.filter((rolle: RolleEntity) => + orgIdsWithRecht.includes(rolle.administeredBySchulstrukturknoten), + ); + + return filteredRollen.map((rolle: RolleEntity) => mapEntityToAggregate(rolle, this.rolleFactory)); + } + public async exists(id: RolleID): Promise { const rolle: Option> = await this.em.findOne( RolleEntity,