From 9ccbfd9aa7f0eb27d812f81b06bdbd6481fac1af Mon Sep 17 00:00:00 2001 From: aimee-889 <93951322+aimee-889@users.noreply.github.com> Date: Mon, 8 Jul 2024 21:41:42 +0200 Subject: [PATCH 1/2] DBP-799-openldap-image-helm-chart (#577) DBP-799-openldap-image-helm-chart (#577) --- ...image-and-helm-publish-check-deploy-on-push-scheduled.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/image-and-helm-publish-check-deploy-on-push-scheduled.yml b/.github/workflows/image-and-helm-publish-check-deploy-on-push-scheduled.yml index 6ca7416c1..56e4fe9e1 100644 --- a/.github/workflows/image-and-helm-publish-check-deploy-on-push-scheduled.yml +++ b/.github/workflows/image-and-helm-publish-check-deploy-on-push-scheduled.yml @@ -8,7 +8,7 @@ on: branches: - "**" schedule: - - cron: '0 2 * * *' + - cron: '0 2 * * *' delete: concurrency: @@ -133,11 +133,12 @@ jobs: - create_branch_identifier - release_helm - build_image_on_push - uses: dBildungsplattform/spsh-app-deploy/.github/workflows/deploy.yml@4 + uses: dBildungsplattform/spsh-app-deploy/.github/workflows/deploy.yml@5 with: dbildungs_iam_server_branch: ${{ needs.branch_meta.outputs.ticket }} schulportal_client_branch: ${{ needs.branch_meta.outputs.ticket }} dbildungs_iam_keycloak_branch: ${{ needs.branch_meta.outputs.ticket }} + dbildungs_iam_ldap_branch: ${{ needs.branch_meta.outputs.ticket }} namespace: ${{ needs.create_branch_identifier.outputs.namespace_from_branch }} secrets: inherit From 28b742a66f0ef07bf2fd3caba47355403c55b32f Mon Sep 17 00:00:00 2001 From: Cornelius <144817755+DPDS93CT@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:37:12 +0200 Subject: [PATCH 2/2] =?UTF-8?q?SPSH-698:=20Personenkontexte=20bearbeiten:?= =?UTF-8?q?=20Weitere=20Zuordnung=20hinzuf=C3=BCgen=20(#524)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SPSH-658: Umbau des Personenkontextes nach DDD Richtlinien * Create personenkontext factory and validity checks * SPSH-658: Erste Iteration für die Filterung be der Rollenabfrage des eingeloggten Admins. * Fix dependencies * SPSH-658: Undone the first implementation for filtering roles, implemented the first draft for checking roles assignment. * More permission checks * SPSH-658: Extra Prüfung für die Systemrechte bei der Anlage eines Personenkontextes. * SPSH-658: Tests wegen Dependencies & setups fixen. * SPSH-658: Fixed Lint Warnungen * SPSH-658: Fixed more tests * SPSH-658: Fixed more tests * SPSH-658: Fixed integration tests für den Personenkontext-Controller * Move logic to permissions * Fix tests * Fix more tests * Fix remaining non-api-tests * Allow logged in user to see all personenkontexte of another user if authorized * Fix more tests * Fix all tests * Merge remote-tracking branch 'origin/main' into spsh-606 * More code coverage * SPSH-658: Unittests für den Personenkontext Aggregate, neue Error Klassen & OrganisationRepository, & Integrationtests für dbiam-personenkontext.repo * Fix tests * SPSH-658: Fixed unittests wegen merge conflicts * move param and response files in personenkontext/api * Add Repo to person module * fix imports * add PersonenkontexteUpdate as aggreate * Mock keycloak in integration test * Cover createAuthorized in tests * Fix personenkontext.uc tests * SPSH-658: Specification for landes-admin implemented and extra check for the creation of personenkontext. * Fix personenkontext tests * Fix tests * add validation for Personenkontexte-Update aggregate * add test for PersonenkontexteUpdate and corresponding classes * SPSH-658: Changed the logic for the extra check for the permission of the current admin by the creation of the personenkontext. Refactored the OrganisationMatchesRollenart from the personenkontext-anlage in an helper class. * SPSH-658: Fixed lint issues * add exception-filter for PersonenkontexteUpdateErrors * SPSH-658: Refactored the Personenkontext aggregate and changed the exception type for the check for the OrganisationMatchesRollenart * Fix repo tests * SPSH-658: Fixed tests for Personenkontext * Add error to mapper * Cover dbiam-personenkontext.repo * Cover organisation-repository * Fix dbiam-personenkontext-controller coverage * Remove unused code * SPSH-658: Corrected the unit tests for personenkontext * Fix tests * SPSH-658: Implemented unit-tests for the organisation-matches-rollenart * Revert test timeout * Fix OutOfMemory * adjust PersonenkontexteUpdate to handle addtions of PK * SPSH-606: Added a partial Personenkontext for the tests in order to fix the memory issue. * adjust deletion of Personenkontext to be native * adjust delete-method in PersonenkonteUpdate * remove unused method * adjust PersonenKontext.rolleId to reference Rolle * SPSH-731: Implemented the restriction of roles that can be viewed for the creation of personenkontext, and made the parameter rollenName for the endpoint /GET/api/personenkontext/rollen optional * SPSH-658: PR Review * Rename createAuthorized * SPSH-606: Fixed tests due to the error resulting from the sequence of setup data * SPSH-731: Implemented integration tests for the DbiamPersonenkontextFilterController * SPSH-731: Implemented unit tests for the PersonenkontextAnlage * fix automapper issues in test * adjust PersonenkontexteUpdate and tests * add PersonenkontexteUpdate-event * adjust events for Personenkontext * adjust tests for LdapEventHandler * SPSH-732: Implemented a new endpoint that checks the validity of personenkontext before saving the person and the personenkontext. * SPSH-732: Undid the extra method for checkReferences in Personenkontext * add DbiamPersonenUebersicht aggregate and implement persion check for PK-retrieval * SPSH-732: Refactored error codes for FE. * SPSH-732: Updated the payload for the request POST api/dbiam/personen * move publishing of events for Personenkontext and Orga into the repos * SPSH-732: Implemented unit tests * SPSH-732: Fixed lint * SPSH-732: Fixed dependency issue for the dbiam person controller * SPSH-732: Implemented the integration tests * SPSH-732: Fixed tests for Keycloak * adjusted DbiamPersonenkontext controller and tests * SPSH-732: Added more unit tests and removed unused specifications * SPSH-732: Fixed KC issues for the integration tests * SPSH-732: Fixed the KC issue for the integration tests * SPSH-732: PR review * fixed linting * Event published after creating the kontext hold personId, OrgaId and RolleId * Introduced the workflow controller * Enhanced new error class that is thrown at commit time. * Added necessary classes to the person module * Fixed linting * Fixed response and test * Search by string is possible for both ssk and rolle now * fixed test and renamed aggregate * Fixed tests * remvoed log statement * Handled error in commit endpoint and fixed test * Catch error in the commit controller * can commit method now also returns errors instead of boolean to propagate errors to controlelr * Fixed person service test * fix * test for commit method * more tests * fixed test * Covered all lines for workflow aggregate * Test for find method * errors test * covered more lines * fixed linting * covered badrequestexception line * Fixed tests * Fixed test * covered last line * added types to test for the orga and rolle to match * Test for handling request with no orgaId * trigger rollen method if orga is provided * made use of class properties instead of method parameters * added initiazer to test * last test * fixed lint * modified rollen logic * fixed test * added roles to array in test * Fixed test * fixed litning * return orgas also if the orgaId was provided * fixed tests after merge * linting * added test for error * another test for error * changed path to workflow and added description of the API * Not considering child orgas while fetching for rollen. * Sorting Rollen b y name, Orgas by kennng And Name and Klassen by name * Removed unncesseary assertions as now orgas are sorted and can't be compared to unsorted orgas * tests for sorting * covered last line * added another Rolle to the spec to sort * fixed assert * test for undefined name while sorting * test for undefined kennung and undefined rolle * fixed assertion * test for undefined everything * more coverage * couple more tests to reach 100% coverage * undefined kennung but present name * test * removed the only * check for rollen * remove check for rollen * changed the logic for sorting * more test for sorting * test * linting * fixed linting * last test * linting * SPSH-698: Refactoring, moved the creation of persons with personenkontext in the PersonenkontextModule * SPSH-698: Renamed the route of the POST method createPersonWithKontext to fix integration tests. * SPSH-698: Fixed the issue with integration tests failing in the pipeline due to KC test-container. * SPSH-698: PR Review * SPSH-698: Removed the sort unit test for the organisation.uc.spec * SPSH-698: Added unit tests for the limit parameter & updated the logic for returned allowedRollen when the limit is set. * SPSH-698: Changed the endpoint for the request CreatePersonWithPersonenkontext * SPSH-698: Changed the logic for the findAllSchulstrukturknoten() method. * SPSH-698: Implemented the query filter for requesting organisations from the DB and updated the unit tests * SPSH-698: Fixed KC issues * SPSH-698: Fixed integration tests failing due modules setup * SPSH-698: Moved the OrganisationScope for querying the DB from the domain to the repository. * SPSH-698: PR Review 2 --------- Co-authored-by: Phael Mouko Co-authored-by: Marvin Rode Co-authored-by: Marvin Rode (Cap) <127723478+marode-cap@users.noreply.github.com> Co-authored-by: Haby-Phael Mouko <130637379+phaelcg@users.noreply.github.com> Co-authored-by: godtierbatuhan Co-authored-by: Youssef Bouchara <101522419+YoussefBouch@users.noreply.github.com> --- .../organisation/api/organisation.uc.spec.ts | 68 +- .../organisation.repo.integration-spec.ts | 66 + .../persistence/organisation.repo.ts | 30 + ...biam-person.controller.integration-spec.ts | 214 --- .../person/api/dbiam-person.controller.ts | 96 -- .../person/domain/person.service.spec.ts | 190 +-- src/modules/person/domain/person.service.ts | 113 +- src/modules/person/person-api.module.ts | 11 +- src/modules/person/person.module.ts | 4 - ...text-filter.controller.integration-spec.ts | 207 --- ...dbiam-personenkontext-filter.controller.ts | 94 -- ...xt-workflow.controller.integration-spec.ts | 648 +++++++++ ...iam-personenkontext-workflow.controller.ts | 264 ++++ ...onenkontext.controller.integration.spec.ts | 12 +- ...-create-person-with-context.body.params.ts | 4 +- ...ind-personenkontextworkflow-body.params.ts | 50 + .../api/personenkontext.uc.spec.ts | 5 - .../api/response}/dbiam-person.response.ts | 11 +- ...dbiam-personenkontext-workflow-response.ts | 65 + .../domain/dbiam-personenkontext.factory.ts | 6 +- .../personenkontext-commit.error.spec.ts | 26 + .../error/personenkontext-commit.error.ts | 10 + .../personenkontexte-update.error.spec.ts | 21 + .../error/update-not-found.error.spec.ts | 36 + .../domain/personenkontext-anlage.factory.ts | 16 - .../domain/personenkontext-anlage.spec.ts | 623 --------- .../domain/personenkontext-anlage.ts | 108 -- .../personenkontext-creation.service.spec.ts | 233 ++++ .../personenkontext-creation.service.ts | 70 + .../personenkontext-workflow.factory.ts | 25 + .../domain/personenkontext-workflow.spec.ts | 1183 +++++++++++++++++ .../domain/personenkontext-workflow.ts | 332 +++++ .../domain/personenkontexte-update.spec.ts | 70 +- .../domain/personenkontexte-update.ts | 77 +- .../personenkontext-api.module.ts | 10 +- src/modules/rolle/api/rolle.controller.ts | 1 - src/modules/rolle/domain/rolle.ts | 1 - 37 files changed, 3228 insertions(+), 1772 deletions(-) delete mode 100644 src/modules/person/api/dbiam-person.controller.integration-spec.ts delete mode 100644 src/modules/person/api/dbiam-person.controller.ts delete mode 100644 src/modules/personenkontext/api/dbiam-personenkontext-filter.controller.integration-spec.ts delete mode 100644 src/modules/personenkontext/api/dbiam-personenkontext-filter.controller.ts create mode 100644 src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.integration-spec.ts create mode 100644 src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.ts rename src/modules/{person/api => personenkontext/api/param}/dbiam-create-person-with-context.body.params.ts (79%) create mode 100644 src/modules/personenkontext/api/param/dbiam-find-personenkontextworkflow-body.params.ts rename src/modules/{person/api => personenkontext/api/response}/dbiam-person.response.ts (56%) create mode 100644 src/modules/personenkontext/api/response/dbiam-personenkontext-workflow-response.ts create mode 100644 src/modules/personenkontext/domain/error/personenkontext-commit.error.spec.ts create mode 100644 src/modules/personenkontext/domain/error/personenkontext-commit.error.ts create mode 100644 src/modules/personenkontext/domain/error/personenkontexte-update.error.spec.ts create mode 100644 src/modules/personenkontext/domain/error/update-not-found.error.spec.ts delete mode 100644 src/modules/personenkontext/domain/personenkontext-anlage.factory.ts delete mode 100644 src/modules/personenkontext/domain/personenkontext-anlage.spec.ts delete mode 100644 src/modules/personenkontext/domain/personenkontext-anlage.ts create mode 100644 src/modules/personenkontext/domain/personenkontext-creation.service.spec.ts create mode 100644 src/modules/personenkontext/domain/personenkontext-creation.service.ts create mode 100644 src/modules/personenkontext/domain/personenkontext-workflow.factory.ts create mode 100644 src/modules/personenkontext/domain/personenkontext-workflow.spec.ts create mode 100644 src/modules/personenkontext/domain/personenkontext-workflow.ts diff --git a/src/modules/organisation/api/organisation.uc.spec.ts b/src/modules/organisation/api/organisation.uc.spec.ts index 3af72730d..8ad76572a 100644 --- a/src/modules/organisation/api/organisation.uc.spec.ts +++ b/src/modules/organisation/api/organisation.uc.spec.ts @@ -178,12 +178,6 @@ describe('OrganisationUc', () => { expect(result.total).toBe(2); expect(result.items).toHaveLength(2); - expect(result.items[0]?.name).toEqual(organisationDos[0]?.name); - expect(result.items[1]?.name).toEqual(organisationDos[1]?.name); - expect(result.items[0]?.kennung).toEqual(organisationDos[0]?.kennung); - expect(result.items[1]?.kennung).toEqual(organisationDos[1]?.kennung); - expect(result.items[0]?.typ).toEqual(organisationDos[0]?.typ); - expect(result.items[1]?.typ).toEqual(organisationDos[1]?.typ); }); }); @@ -321,12 +315,56 @@ describe('OrganisationUc', () => { if (!(result instanceof SchulConnexError)) { expect(result.total).toBe(2); expect(result.items).toHaveLength(2); - expect(result.items[0]?.name).toEqual(organisationDos[0]?.name); - expect(result.items[1]?.name).toEqual(organisationDos[1]?.name); - expect(result.items[0]?.kennung).toEqual(organisationDos[0]?.kennung); - expect(result.items[1]?.kennung).toEqual(organisationDos[1]?.kennung); - expect(result.items[0]?.typ).toEqual(organisationDos[0]?.typ); - expect(result.items[1]?.typ).toEqual(organisationDos[1]?.typ); + } + }); + + it('should handle undefined names during sorting', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { name: undefined }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { name: 'Alpha' }); + const org3: OrganisationDo = DoFactory.createOrganisation(true, { name: 'Beta' }); + + organisationServiceMock.findOrganisationById.mockResolvedValueOnce({ + ok: true, + value: DoFactory.createOrganisation(true), + }); + + organisationServiceMock.findAllAdministriertVon.mockResolvedValue({ + total: 3, + offset: 0, + limit: 3, + items: [org1, org2, org3], + }); + + const result: Paged | SchulConnexError = + await organisationUc.findAdministriertVon('parent-id'); + + if (!(result instanceof SchulConnexError)) { + expect(result.items.map((o: OrganisationResponseLegacy) => o.name)).toEqual([ + undefined, + 'Alpha', + 'Beta', + ]); + } + }); + + it('should handle empty list of organisations', async () => { + organisationServiceMock.findAllAdministriertVon.mockResolvedValue({ + total: 0, + offset: 0, + limit: 0, + items: [], + }); + + organisationServiceMock.findOrganisationById.mockResolvedValueOnce({ + ok: true, + value: DoFactory.createOrganisation(true), + }); + + const result: Paged | SchulConnexError = + await organisationUc.findAdministriertVon('parent-id'); + + if (!(result instanceof SchulConnexError)) { + expect(result.items).toHaveLength(0); } }); }); @@ -372,12 +410,6 @@ describe('OrganisationUc', () => { if (!(result instanceof SchulConnexError)) { expect(result.total).toBe(2); expect(result.items).toHaveLength(2); - expect(result.items[0]?.name).toEqual(organisationDos[0]?.name); - expect(result.items[1]?.name).toEqual(organisationDos[1]?.name); - expect(result.items[0]?.kennung).toEqual(organisationDos[0]?.kennung); - expect(result.items[1]?.kennung).toEqual(organisationDos[1]?.kennung); - expect(result.items[0]?.typ).toEqual(organisationDos[0]?.typ); - expect(result.items[1]?.typ).toEqual(organisationDos[1]?.typ); } }); }); diff --git a/src/modules/organisation/persistence/organisation.repo.integration-spec.ts b/src/modules/organisation/persistence/organisation.repo.integration-spec.ts index 369a29aa4..933e8d73c 100644 --- a/src/modules/organisation/persistence/organisation.repo.integration-spec.ts +++ b/src/modules/organisation/persistence/organisation.repo.integration-spec.ts @@ -16,6 +16,7 @@ import { OrganisationScope } from './organisation.scope.js'; import { Mapper } from '@automapper/core'; import { getMapperToken } from '@automapper/nestjs'; import { EventModule } from '../../../core/eventbus/index.js'; +import { OrganisationsTyp } from '../domain/organisation.enums.js'; describe('OrganisationRepo', () => { let module: TestingModule; @@ -104,6 +105,16 @@ describe('OrganisationRepo', () => { }); }); + describe('find', () => { + it('should find all organizations', async () => { + const organisationDo: OrganisationDo = DoFactory.createOrganisation(false); + await sut.save(organisationDo); + const foundOrganisations: OrganisationDo[] = await sut.find(25); + + expect(foundOrganisations).toHaveLength(1); + }); + }); + describe('findByIds', () => { it('should find organizations by IDs', async () => { const organisationDo: OrganisationDo = DoFactory.createOrganisation(false); @@ -259,4 +270,59 @@ describe('OrganisationRepo', () => { }); }); }); + + describe('findByNameOrKennungAndExcludeByOrganisationType', () => { + describe('when matching organisations by name were found', () => { + it('should return found organizations', async () => { + const orgaName: string = 'Test-Orga'; + const excludeOrgaType: OrganisationsTyp = OrganisationsTyp.KLASSE; + const organisationDoSchule: OrganisationDo = DoFactory.createOrganisation(false, { + name: orgaName, + typ: OrganisationsTyp.SCHULE, + }); + await sut.save(organisationDoSchule); + + const organisationDoKlasse: OrganisationDo = DoFactory.createOrganisation(false, { + name: orgaName + '1', + typ: excludeOrgaType, + }); + await sut.save(organisationDoKlasse); + + const organisationDoSchule2: OrganisationDo = DoFactory.createOrganisation(false, { + typ: OrganisationsTyp.SCHULE, + }); + await sut.save(organisationDoSchule2); + + const foundOrganisations: Option[]> = + await sut.findByNameOrKennungAndExcludeByOrganisationType(excludeOrgaType, orgaName); + expect(foundOrganisations).toBeInstanceOf(Array); + expect(foundOrganisations).toHaveLength(1); + }); + }); + + describe('when matching organisations were found and search is limit', () => { + it('should return limited found organizations', async () => { + const excludeOrgaType: OrganisationsTyp = OrganisationsTyp.KLASSE; + const organisationDoSchule: OrganisationDo = DoFactory.createOrganisation(false, { + typ: OrganisationsTyp.SCHULE, + }); + await sut.save(organisationDoSchule); + + const organisationDoSchule2: OrganisationDo = DoFactory.createOrganisation(false, { + typ: OrganisationsTyp.SCHULE, + }); + await sut.save(organisationDoSchule2); + + const organisationDoKlasse: OrganisationDo = DoFactory.createOrganisation(false, { + typ: excludeOrgaType, + }); + await sut.save(organisationDoKlasse); + + const foundOrganisations: Option[]> = + await sut.findByNameOrKennungAndExcludeByOrganisationType(excludeOrgaType, undefined, 1); + expect(foundOrganisations).toBeInstanceOf(Array); + expect(foundOrganisations).toHaveLength(1); + }); + }); + }); }); diff --git a/src/modules/organisation/persistence/organisation.repo.ts b/src/modules/organisation/persistence/organisation.repo.ts index 2db460c84..6c1b95bd1 100644 --- a/src/modules/organisation/persistence/organisation.repo.ts +++ b/src/modules/organisation/persistence/organisation.repo.ts @@ -12,6 +12,7 @@ import { OrganisationID } from '../../../shared/types/aggregate-ids.types.js'; import { SchuleCreatedEvent } from '../../../shared/events/schule-created.event.js'; import { EventService } from '../../../core/eventbus/index.js'; import { OrganisationsTyp } from '../domain/organisation.enums.js'; +import { ScopeOperator } from '../../../shared/persistence/scope.enums.js'; @Injectable() export class OrganisationRepo { @@ -66,6 +67,14 @@ export class OrganisationRepo { return !!organisation; } + public async find(limit?: number, offset?: number): Promise[]> { + const organisations: OrganisationEntity[] = await this.em.findAll(OrganisationEntity, { + limit: limit, + offset: offset, + }); + return this.mapper.mapArray(organisations, OrganisationEntity, OrganisationDo); + } + public async findById(id: string): Promise>> { const organisation: Option = await this.em.findOne(OrganisationEntity, { id }); if (organisation) { @@ -135,4 +144,25 @@ export class OrganisationRepo { } return []; } + + public async findByNameOrKennungAndExcludeByOrganisationType( + excludeOrganisationType: OrganisationsTyp, + searchStr?: string, + limit?: number, + ): Promise[]> { + const scope: OrganisationScope = new OrganisationScope(); + if (searchStr) { + scope + .searchString(searchStr) + .setScopeWhereOperator(ScopeOperator.AND) + .excludeTyp([excludeOrganisationType]); + } else { + scope.excludeTyp([excludeOrganisationType]).paged(0, limit); + } + + let foundOrganisations: OrganisationDo[] = []; + [foundOrganisations] = await this.findBy(scope); + + return foundOrganisations; + } } diff --git a/src/modules/person/api/dbiam-person.controller.integration-spec.ts b/src/modules/person/api/dbiam-person.controller.integration-spec.ts deleted file mode 100644 index b636f14cd..000000000 --- a/src/modules/person/api/dbiam-person.controller.integration-spec.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { MikroORM } from '@mikro-orm/core'; -import { CallHandler, ExecutionContext, INestApplication } from '@nestjs/common'; -import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Request } from 'express'; -import { Observable } from 'rxjs'; -import request, { Response } from 'supertest'; -import { App } from 'supertest/types.js'; -import { - ConfigTestModule, - DatabaseTestModule, - DoFactory, - KeycloakConfigTestModule, - MapperTestModule, -} from '../../../../test/utils/index.js'; -import { GlobalValidationPipe } from '../../../shared/validation/index.js'; -import { PersonPermissionsRepo } from '../../authentication/domain/person-permission.repo.js'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; -import { PassportUser } from '../../authentication/types/user.js'; -import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; -import { OrganisationsTyp } from '../../organisation/domain/organisation.enums.js'; -import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; -import { RollenArt } from '../../rolle/domain/rolle.enums.js'; -import { Rolle } from '../../rolle/domain/rolle.js'; -import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; -import { PersonApiModule } from '../person-api.module.js'; -import { KeycloakConfigModule } from '../../keycloak-administration/keycloak-config.module.js'; -import { KeycloakAdministrationModule } from '../../keycloak-administration/keycloak-administration.module.js'; - -describe('dbiam Person API', () => { - let app: INestApplication; - let orm: MikroORM; - - let organisationRepo: OrganisationRepo; - let rolleRepo: RolleRepo; - let personpermissionsRepoMock: DeepMocked; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [ - MapperTestModule, - ConfigTestModule, - DatabaseTestModule.forRoot({ isDatabaseRequired: true }), - KeycloakAdministrationModule, - PersonApiModule, - ], - providers: [ - { - provide: APP_PIPE, - useClass: GlobalValidationPipe, - }, - { - provide: PersonPermissionsRepo, - useValue: createMock(), - }, - { - 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(); - }, - }, - }, - ], - }) - .overrideModule(KeycloakConfigModule) - .useModule(KeycloakConfigTestModule.forRoot({ isKeycloakRequired: true })) - .compile(); - - orm = module.get(MikroORM); - organisationRepo = module.get(OrganisationRepo); - rolleRepo = module.get(RolleRepo); - personpermissionsRepoMock = module.get(PersonPermissionsRepo); - - await DatabaseTestModule.setupDatabase(orm); - app = module.createNestApplication(); - await app.init(); - }, 10000000); - - afterAll(async () => { - await orm.close(); - await app.close(); - }); - - beforeEach(async () => { - await DatabaseTestModule.clearDatabase(orm); - }); - - describe('/POST create person with personenkontext', () => { - it('should return created person and personenkontext', async () => { - const organisation: OrganisationDo = await organisationRepo.save( - DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE }), - ); - const rolle: Rolle = await rolleRepo.save( - DoFactory.createRolle(false, { - administeredBySchulstrukturknoten: organisation.id, - rollenart: RollenArt.LEHR, - }), - ); - - const personpermissions: DeepMocked = createMock(); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisation.id]); - personpermissions.hasSystemrechtAtOrganisation.mockResolvedValueOnce(true); - - const response: Response = await request(app.getHttpServer() as App) - .post('/dbiam/personen') - .send({ - familienname: faker.person.lastName(), - vorname: faker.person.firstName(), - organisationId: organisation.id, - rolleId: rolle.id, - }); - - expect(response.status).toBe(201); - }); - - it('should return error with status-code=404 if organisation does NOT exist', async () => { - const rolle: Rolle = await rolleRepo.save( - DoFactory.createRolle(false, { - administeredBySchulstrukturknoten: faker.string.uuid(), - rollenart: RollenArt.LEHR, - }), - ); - const permissions: DeepMocked = createMock(); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValueOnce(permissions); - permissions.hasSystemrechtAtOrganisation.mockResolvedValueOnce(true); - permissions.canModifyPerson.mockResolvedValueOnce(true); - - const response: Response = await request(app.getHttpServer() as App) - .post('/dbiam/personen') - .send({ - familienname: faker.person.lastName(), - vorname: faker.person.firstName(), - organisationId: faker.string.uuid(), - rolleId: rolle.id, - }); - - expect(response.status).toBe(404); - }); - - it('should return error with status-code 400 if specification ROLLE_NUR_AN_PASSENDE_ORGANISATION is NOT satisfied', async () => { - const organisation: OrganisationDo = await organisationRepo.save( - DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE }), - ); - const rolle: Rolle = await rolleRepo.save( - DoFactory.createRolle(false, { - administeredBySchulstrukturknoten: organisation.id, - rollenart: RollenArt.SYSADMIN, - }), - ); - - const personpermissions: DeepMocked = createMock(); - personpermissions.hasSystemrechtAtOrganisation.mockResolvedValueOnce(true); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisation.id]); - - const response: Response = await request(app.getHttpServer() as App) - .post('/dbiam/personen') - .send({ - familienname: faker.person.lastName(), - vorname: faker.person.firstName(), - organisationId: organisation.id, - rolleId: rolle.id, - }); - - expect(response.status).toBe(400); - expect(response.body).toEqual({ - code: 400, - i18nKey: 'ROLLE_NUR_AN_PASSENDE_ORGANISATION', - }); - }); - - it('should return error with status-code 404 if user does NOT have permissions', async () => { - const organisation: OrganisationDo = await organisationRepo.save( - DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE }), - ); - const rolle: Rolle = await rolleRepo.save( - DoFactory.createRolle(false, { - administeredBySchulstrukturknoten: organisation.id, - rollenart: RollenArt.LEHR, - }), - ); - - const personpermissions: DeepMocked = createMock(); - personpermissions.hasSystemrechtAtOrganisation.mockResolvedValueOnce(false); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); - - const response: Response = await request(app.getHttpServer() as App) - .post('/dbiam/personen') - .send({ - familienname: faker.person.lastName(), - vorname: faker.person.firstName(), - organisationId: organisation.id, - rolleId: rolle.id, - }); - expect(response.status).toBe(404); - expect(response.body).toEqual({ - code: 404, - subcode: '01', - titel: 'Angefragte Entität existiert nicht', - beschreibung: 'Die angeforderte Entität existiert nicht', - }); - }); - }); -}); diff --git a/src/modules/person/api/dbiam-person.controller.ts b/src/modules/person/api/dbiam-person.controller.ts deleted file mode 100644 index 86c057734..000000000 --- a/src/modules/person/api/dbiam-person.controller.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Body, Controller, HttpCode, HttpStatus, Post, UseFilters } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiCreatedResponse, - ApiForbiddenResponse, - ApiInternalServerErrorResponse, - ApiOAuth2, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { DomainError } from '../../../shared/error/index.js'; -import { SchulConnexErrorMapper } from '../../../shared/error/schul-connex-error.mapper.js'; -import { SchulConnexValidationErrorFilter } from '../../../shared/error/schulconnex-validation-error.filter.js'; -import { Permissions } from '../../authentication/api/permissions.decorator.js'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; -import { EventService } from '../../../core/eventbus/index.js'; -import { PersonenkontextCreatedEvent } from '../../../shared/events/personenkontext-created.event.js'; -import { PersonPersonenkontext, PersonService } from '../domain/person.service.js'; -import { DbiamCreatePersonWithContextBodyParams } from './dbiam-create-person-with-context.body.params.js'; -import { DBiamPersonResponse } from './dbiam-person.response.js'; -import { DbiamPersonenkontextError } from '../../personenkontext/api/dbiam-personenkontext.error.js'; -import { PersonenkontextExceptionFilter } from '../../personenkontext/api/personenkontext-exception-filter.js'; -import { PersonenkontextSpecificationError } from '../../personenkontext/specification/error/personenkontext-specification.error.js'; -import { AuthenticationExceptionFilter } from '../../authentication/api/authentication-exception-filter.js'; - -@UseFilters( - new SchulConnexValidationErrorFilter(), - new PersonenkontextExceptionFilter(), - new AuthenticationExceptionFilter(), -) -@ApiTags('dbiam-personen') -@ApiBearerAuth() -@ApiOAuth2(['openid']) -@Controller({ path: 'dbiam/personen' }) -export class DBiamPersonController { - public constructor( - private readonly personService: PersonService, - private readonly eventService: EventService, - ) {} - - @Post() - @HttpCode(HttpStatus.CREATED) - @ApiCreatedResponse({ - description: 'Person with Personenkontext was successfully created.', - type: DBiamPersonResponse, - }) - @ApiBadRequestResponse({ - description: 'The person and the personenkontext could not be created, may due to unsatisfied specifications.', - type: DbiamPersonenkontextError, - }) - @ApiUnauthorizedResponse({ description: 'Not authorized to create person with personenkontext.' }) - @ApiForbiddenResponse({ description: 'Insufficient permission to create person with personenkontext.' }) - @ApiForbiddenResponse({ description: 'Insufficient permissions to create the person with personenkontext.' }) - @ApiBadRequestResponse({ description: 'Request has wrong format.', type: DbiamPersonenkontextError }) - @ApiInternalServerErrorResponse({ - description: 'Internal server error while creating person with personenkontext.', - }) - public async createPersonWithKontext( - @Body() params: DbiamCreatePersonWithContextBodyParams, - @Permissions() permissions: PersonPermissions, - ): Promise { - //Check all references & permissions then save person - const savedPersonWithPersonenkontext: DomainError | PersonPersonenkontext = - await this.personService.createPersonWithPersonenkontext( - permissions, - params.vorname, - params.familienname, - params.organisationId, - params.rolleId, - ); - - if (savedPersonWithPersonenkontext instanceof PersonenkontextSpecificationError) { - throw savedPersonWithPersonenkontext; - } - - if (savedPersonWithPersonenkontext instanceof DomainError) { - throw SchulConnexErrorMapper.mapSchulConnexErrorToHttpException( - SchulConnexErrorMapper.mapDomainErrorToSchulConnexError(savedPersonWithPersonenkontext), - ); - } - - this.eventService.publish( - new PersonenkontextCreatedEvent( - savedPersonWithPersonenkontext.personenkontext.personId, - savedPersonWithPersonenkontext.personenkontext.organisationId, - savedPersonWithPersonenkontext.personenkontext.rolleId, - ), - ); - - return new DBiamPersonResponse( - savedPersonWithPersonenkontext.person, - savedPersonWithPersonenkontext.personenkontext, - ); - } -} diff --git a/src/modules/person/domain/person.service.spec.ts b/src/modules/person/domain/person.service.spec.ts index 8050a693b..0098501b4 100644 --- a/src/modules/person/domain/person.service.spec.ts +++ b/src/modules/person/domain/person.service.spec.ts @@ -2,43 +2,18 @@ import { Dictionary, Mapper } from '@automapper/core'; import { getMapperToken } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { - DomainError, - EntityNotFoundError, - InvalidAttributeLengthError, - KeycloakClientError, - MissingPermissionsError, -} from '../../../shared/error/index.js'; +import { EntityNotFoundError } from '../../../shared/error/index.js'; import { DoFactory } from '../../../../test/utils/do-factory.js'; import { PersonRepo } from '../persistence/person.repo.js'; import { PersonDo } from './person.do.js'; -import { PersonPersonenkontext, PersonService } from './person.service.js'; +import { PersonService } from './person.service.js'; import { Paged } from '../../../shared/paging/index.js'; -import { PersonFactory } from './person.factory.js'; -import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; -import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; -import { faker } from '@faker-js/faker'; -import { Organisation } from '../../organisation/domain/organisation.js'; -import { PersonRepository } from '../persistence/person.repository.js'; -import { DBiamPersonenkontextRepo } from '../../personenkontext/persistence/dbiam-personenkontext.repo.js'; -import { PersonenkontextFactory } from '../../personenkontext/domain/personenkontext.factory.js'; -import { Person } from './person.js'; -import { Rolle } from '../../rolle/domain/rolle.js'; -import { RollenArt } from '../../rolle/domain/rolle.enums.js'; -import { OrganisationsTyp } from '../../organisation/domain/organisation.enums.js'; -import { RolleNurAnPassendeOrganisationError } from '../../personenkontext/specification/error/rolle-nur-an-passende-organisation.js'; -describe('sut', () => { +describe('PersonService', () => { let module: TestingModule; let sut: PersonService; let personRepoMock: DeepMocked; let mapperMock: DeepMocked; - let rolleRepoMock: DeepMocked; - let organisationRepositoryMock: DeepMocked; - let personRepositoryMock: DeepMocked; - let personpermissionsMock: DeepMocked; - let personFactoryMock: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -52,44 +27,11 @@ describe('sut', () => { provide: getMapperToken(), useValue: createMock(), }, - { - provide: RolleRepo, - useValue: createMock(), - }, - { - provide: OrganisationRepository, - useValue: createMock(), - }, - { - provide: PersonRepository, - useValue: createMock(), - }, - { - provide: DBiamPersonenkontextRepo, - useValue: createMock(), - }, - { - provide: PersonFactory, - useValue: createMock(), - }, - { - provide: PersonenkontextFactory, - useValue: createMock(), - }, - { - provide: PersonPermissions, - useValue: createMock(), - }, ], }).compile(); sut = module.get(PersonService); personRepoMock = module.get(PersonRepo); mapperMock = module.get(getMapperToken()); - rolleRepoMock = module.get(RolleRepo); - organisationRepositoryMock = module.get(OrganisationRepository); - personRepositoryMock = module.get(PersonRepository); - personpermissionsMock = module.get(PersonPermissions); - personFactoryMock = module.get(PersonFactory); }); afterAll(async () => { @@ -159,130 +101,4 @@ describe('sut', () => { expect(result.items).toHaveLength(0); }); }); - - describe('createPersonWithPersonenkontext', () => { - it('should return DomainError if Person Aggregate ist invalid ', async () => { - personFactoryMock.createNew.mockResolvedValueOnce(new InvalidAttributeLengthError('name.vorname')); - rolleRepoMock.findById.mockResolvedValueOnce(DoFactory.createRolle(true)); - organisationRepositoryMock.findById.mockResolvedValueOnce(createMock>()); - - const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( - personpermissionsMock, - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - ); - expect(result).toBeInstanceOf(DomainError); - }); - - it('should return EntityNotFoundError if Organisation is not found', async () => { - personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); - rolleRepoMock.findById.mockResolvedValueOnce(DoFactory.createRolle(true)); - organisationRepositoryMock.findById.mockResolvedValueOnce(undefined); - - const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( - personpermissionsMock, - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - ); - expect(result).toBeInstanceOf(EntityNotFoundError); - }); - - it('should return EntityNotFoundError if Rolle is not found', async () => { - personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); - rolleRepoMock.findById.mockResolvedValueOnce(undefined); - organisationRepositoryMock.findById.mockResolvedValueOnce(createMock>()); - - const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( - personpermissionsMock, - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - ); - expect(result).toBeInstanceOf(EntityNotFoundError); - }); - - it('should return EntityNotFoundError if Rolle can NOT be assigned to organisation', async () => { - personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); - const rolleMock: DeepMocked> = createMock>(); - rolleMock.canBeAssignedToOrga.mockResolvedValueOnce(false); - rolleRepoMock.findById.mockResolvedValueOnce(rolleMock); - organisationRepositoryMock.findById.mockResolvedValueOnce(createMock>()); - - const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( - personpermissionsMock, - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - ); - expect(result).toBeInstanceOf(EntityNotFoundError); - }); - - it('should return RolleNurAnPassendeOrganisationError if Rolle does NOT match organisation', async () => { - personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); - const rolleMock: DeepMocked> = createMock>({ rollenart: RollenArt.SYSADMIN }); - rolleMock.canBeAssignedToOrga.mockResolvedValueOnce(true); - rolleRepoMock.findById.mockResolvedValueOnce(rolleMock); - organisationRepositoryMock.findById.mockResolvedValueOnce( - createMock>({ typ: OrganisationsTyp.SCHULE }), - ); - - const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( - personpermissionsMock, - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - ); - expect(result).toBeInstanceOf(RolleNurAnPassendeOrganisationError); - }); - - it('should return MissingPermissionsError if user does NOT have permissions', async () => { - personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); - const rolleMock: DeepMocked> = createMock>({ rollenart: RollenArt.SYSADMIN }); - rolleMock.canBeAssignedToOrga.mockResolvedValueOnce(true); - rolleRepoMock.findById.mockResolvedValueOnce(rolleMock); - organisationRepositoryMock.findById.mockResolvedValueOnce( - createMock>({ typ: OrganisationsTyp.LAND }), - ); - personpermissionsMock.hasSystemrechtAtOrganisation.mockResolvedValueOnce(false); - - const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( - personpermissionsMock, - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - ); - expect(result).toBeInstanceOf(MissingPermissionsError); - }); - - it('should return DomainError if Person can be saved in the DB', async () => { - personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); - const rolleMock: DeepMocked> = createMock>({ rollenart: RollenArt.SYSADMIN }); - rolleMock.canBeAssignedToOrga.mockResolvedValueOnce(true); - rolleRepoMock.findById.mockResolvedValueOnce(rolleMock); - organisationRepositoryMock.findById.mockResolvedValueOnce( - createMock>({ typ: OrganisationsTyp.LAND }), - ); - personpermissionsMock.hasSystemrechtAtOrganisation.mockResolvedValueOnce(true); - personRepositoryMock.create.mockResolvedValueOnce( - new KeycloakClientError('Username or email already exists'), - ); - - const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( - personpermissionsMock, - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - ); - expect(result).toBeInstanceOf(DomainError); - }); - }); }); diff --git a/src/modules/person/domain/person.service.ts b/src/modules/person/domain/person.service.ts index 92697a7df..eb290e139 100644 --- a/src/modules/person/domain/person.service.ts +++ b/src/modules/person/domain/person.service.ts @@ -1,23 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { DomainError, EntityNotFoundError, MissingPermissionsError } from '../../../shared/error/index.js'; +import { DomainError, EntityNotFoundError } from '../../../shared/error/index.js'; import { PersonDo } from '../domain/person.do.js'; import { PersonRepo } from '../persistence/person.repo.js'; import { PersonScope } from '../persistence/person.scope.js'; import { Paged } from '../../../shared/paging/paged.js'; import { ScopeOrder } from '../../../shared/persistence/scope.enums.js'; -import { PersonFactory } from './person.factory.js'; import { Person } from './person.js'; -import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; -import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; -import { Organisation } from '../../organisation/domain/organisation.js'; -import { Rolle } from '../../rolle/domain/rolle.js'; -import { OrganisationMatchesRollenart } from '../../personenkontext/specification/organisation-matches-rollenart.js'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; -import { RollenSystemRecht } from '../../rolle/domain/rolle.enums.js'; -import { PersonRepository } from '../persistence/person.repository.js'; -import { RolleNurAnPassendeOrganisationError } from '../../personenkontext/specification/error/rolle-nur-an-passende-organisation.js'; -import { DBiamPersonenkontextRepo } from '../../personenkontext/persistence/dbiam-personenkontext.repo.js'; -import { PersonenkontextFactory } from '../../personenkontext/domain/personenkontext.factory.js'; import { Personenkontext } from '../../personenkontext/domain/personenkontext.js'; export type PersonPersonenkontext = { @@ -27,15 +15,7 @@ export type PersonPersonenkontext = { @Injectable() export class PersonService { - public constructor( - private readonly personRepo: PersonRepo, - private readonly organisationRepo: OrganisationRepository, - private readonly rolleRepo: RolleRepo, - private readonly personRepository: PersonRepository, - private readonly personenkontextRepo: DBiamPersonenkontextRepo, - private readonly personFactory: PersonFactory, - private readonly personenkontextFactory: PersonenkontextFactory, - ) {} + public constructor(private readonly personRepo: PersonRepo) {} public async findPersonById(id: string): Promise, DomainError>> { const person: Option> = await this.personRepo.findById(id); @@ -67,93 +47,4 @@ export class PersonService { items: persons, }; } - - public async createPersonWithPersonenkontext( - permissions: PersonPermissions, - vorname: string, - familienname: string, - organisationId: string, - rolleId: string, - ): Promise { - const person: Person | DomainError = await this.personFactory.createNew({ - vorname: vorname, - familienname: familienname, - }); - if (person instanceof DomainError) { - return person; - } - //Check references & ob der Admin berechtigt ist - const referenceError: Option = await this.checkReferences(organisationId, rolleId); - if (referenceError) { - return referenceError; - } - //Check Permissions für Personenkontext - const permissionsError: Option = await this.checkPermissions(permissions, organisationId); - if (permissionsError) { - return permissionsError; - } - //Save Person - const savedPerson: DomainError | Person = await this.personRepository.create(person); - if (savedPerson instanceof DomainError) { - return savedPerson; - } - - const personenkontext: Personenkontext = this.personenkontextFactory.createNew( - savedPerson.id, - organisationId, - rolleId, - ); - //Save Personenkontext - const savedPersonenkontext: Personenkontext = await this.personenkontextRepo.save(personenkontext); - return { - person: savedPerson, - personenkontext: savedPersonenkontext, - }; - } - - private async checkReferences(organisationId: string, rolleId: string): Promise> { - const [orga, rolle]: [Option>, Option>] = await Promise.all([ - this.organisationRepo.findById(organisationId), - this.rolleRepo.findById(rolleId), - ]); - - if (!orga) { - return new EntityNotFoundError('Organisation', organisationId); - } - - if (!rolle) { - return new EntityNotFoundError('Rolle', rolleId); - } - - // Can rolle be assigned at target orga - const canAssignRolle: boolean = await rolle.canBeAssignedToOrga(organisationId); - if (!canAssignRolle) { - return new EntityNotFoundError('rolle', rolleId); // Rolle does not exist for the chosen organisation - } - - //The aimed organisation needs to match the type of role to be assigned - const organisationMatchesRollenart: OrganisationMatchesRollenart = new OrganisationMatchesRollenart(); - if (!organisationMatchesRollenart.isSatisfiedBy(orga, rolle)) { - return new RolleNurAnPassendeOrganisationError(); - } - - return undefined; - } - - private async checkPermissions( - permissions: PersonPermissions, - organisationId: string, - ): Promise> { - // Check if logged in person has permission - const hasPermissionAtOrga: boolean = await permissions.hasSystemrechtAtOrganisation(organisationId, [ - RollenSystemRecht.PERSONEN_VERWALTEN, - ]); - - // Missing permission on orga - if (!hasPermissionAtOrga) { - return new MissingPermissionsError('Unauthorized to manage persons at the organisation'); - } - - return undefined; - } } diff --git a/src/modules/person/person-api.module.ts b/src/modules/person/person-api.module.ts index 41d53efd9..830786569 100644 --- a/src/modules/person/person-api.module.ts +++ b/src/modules/person/person-api.module.ts @@ -12,8 +12,6 @@ import { KeycloakAdministrationModule } from '../keycloak-administration/keycloa import { DBiamPersonenuebersichtController } from './api/personenuebersicht/dbiam-personenuebersicht.controller.js'; import { PersonInfoController } from './api/person-info.controller.js'; import { PersonApiMapper } from './mapper/person-api.mapper.js'; -import { DBiamPersonController } from './api/dbiam-person.controller.js'; -import { EventModule } from '../../core/eventbus/event.module.js'; @Module({ imports: [ @@ -24,15 +22,8 @@ import { EventModule } from '../../core/eventbus/event.module.js'; OrganisationModule, KeycloakAdministrationModule, LoggerModule.register(PersonApiModule.name), - EventModule, ], providers: [PersonApiMapperProfile, PersonenkontextUc, PersonApiMapper], - controllers: [ - PersonController, - PersonFrontendController, - DBiamPersonenuebersichtController, - PersonInfoController, - DBiamPersonController, - ], + controllers: [PersonController, PersonFrontendController, DBiamPersonenuebersichtController, PersonInfoController], }) export class PersonApiModule {} diff --git a/src/modules/person/person.module.ts b/src/modules/person/person.module.ts index 2606c2e0a..5721a8c9d 100644 --- a/src/modules/person/person.module.ts +++ b/src/modules/person/person.module.ts @@ -7,8 +7,6 @@ import { UsernameGeneratorService } from './domain/username-generator.service.js import { KeycloakAdministrationModule } from '../keycloak-administration/keycloak-administration.module.js'; import { PersonRepository } from './persistence/person.repository.js'; import { PersonFactory } from './domain/person.factory.js'; -import { DBiamPersonenkontextRepo } from '../personenkontext/persistence/dbiam-personenkontext.repo.js'; -import { PersonenkontextFactory } from '../personenkontext/domain/personenkontext.factory.js'; import { RolleRepo } from '../rolle/repo/rolle.repo.js'; import { OrganisationRepo } from '../organisation/persistence/organisation.repo.js'; import { RolleFactory } from '../rolle/domain/rolle.factory.js'; @@ -22,11 +20,9 @@ import { EventModule } from '../../core/eventbus/event.module.js'; PersonPersistenceMapperProfile, PersonRepo, PersonRepository, - DBiamPersonenkontextRepo, PersonService, PersonFactory, UsernameGeneratorService, - PersonenkontextFactory, RolleRepo, OrganisationRepo, OrganisationRepository, diff --git a/src/modules/personenkontext/api/dbiam-personenkontext-filter.controller.integration-spec.ts b/src/modules/personenkontext/api/dbiam-personenkontext-filter.controller.integration-spec.ts deleted file mode 100644 index 3bdb8b1bb..000000000 --- a/src/modules/personenkontext/api/dbiam-personenkontext-filter.controller.integration-spec.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { MikroORM } from '@mikro-orm/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'; -import { - ConfigTestModule, - DEFAULT_TIMEOUT_FOR_TESTCONTAINERS, - DatabaseTestModule, - DoFactory, - MapperTestModule, -} from '../../../../test/utils/index.js'; -import { GlobalValidationPipe } from '../../../shared/validation/index.js'; -import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; -import { Rolle } from '../../rolle/domain/rolle.js'; -import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; -import { PersonenKontextApiModule } from '../personenkontext-api.module.js'; -import { RollenArt, RollenMerkmal, RollenSystemRecht } from '../../rolle/domain/rolle.enums.js'; -import { RolleFactory } from '../../rolle/domain/rolle.factory.js'; -import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; -import { ServiceProviderRepo } from '../../service-provider/repo/service-provider.repo.js'; -import { PersonPermissionsRepo } from '../../authentication/domain/person-permission.repo.js'; -import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { Observable } from 'rxjs'; -import { PassportUser } from '../../authentication/types/user.js'; -import { Request } from 'express'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; -import { FindRollenResponse } from './response/find-rollen.response.js'; - -function createRolle(this: void, rolleFactory: RolleFactory, params: Partial> = {}): Rolle { - const rolle: Rolle = rolleFactory.createNew( - faker.string.alpha(), - faker.string.uuid(), - faker.helpers.enumValue(RollenArt), - [faker.helpers.enumValue(RollenMerkmal)], - [faker.helpers.enumValue(RollenSystemRecht)], - [], - ); - Object.assign(rolle, params); - - return rolle; -} - -describe('DbiamPersonenkontextFilterController Integration Test', () => { - let app: INestApplication; - let orm: MikroORM; - let organisationRepo: OrganisationRepo; - let rolleRepo: RolleRepo; - let rolleFactory: RolleFactory; - let personpermissionsRepoMock: DeepMocked; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [ - MapperTestModule, - ConfigTestModule, - DatabaseTestModule.forRoot({ isDatabaseRequired: true }), - PersonenKontextApiModule, - ], - providers: [ - RolleFactory, - OrganisationRepository, - ServiceProviderRepo, - { - provide: APP_PIPE, - useClass: GlobalValidationPipe, - }, - { - provide: PersonPermissionsRepo, - useValue: createMock(), - }, - { - 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(); - }, - }, - }, - ], - }).compile(); - - orm = module.get(MikroORM); - organisationRepo = module.get(OrganisationRepo); - rolleRepo = module.get(RolleRepo); - rolleFactory = module.get(RolleFactory); - personpermissionsRepoMock = module.get(PersonPermissionsRepo); - - await DatabaseTestModule.setupDatabase(orm); - app = module.createNestApplication(); - await app.init(); - }, DEFAULT_TIMEOUT_FOR_TESTCONTAINERS); - - afterAll(async () => { - await orm.close(); - await app.close(); - }); - - beforeEach(async () => { - await DatabaseTestModule.clearDatabase(orm); - }); - - describe('/GET rollen for personenkontext', () => { - it('should return all rollen for a personenkontext without filter, if the user is Landesadmin', async () => { - const rolleName: string = faker.string.alpha({ length: 10 }); - await rolleRepo.save(createRolle(rolleFactory, { name: rolleName, rollenart: RollenArt.SYSADMIN })); - const schuladminRolleName: string = faker.string.alpha({ length: 10 }); - await rolleRepo.save(createRolle(rolleFactory, { name: schuladminRolleName, rollenart: RollenArt.LEIT })); - - const personpermissions: DeepMocked = createMock(); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationRepo.ROOT_ORGANISATION_ID]); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); - - const response: Response = await request(app.getHttpServer() as App) - .get('/personenkontext/rollen') - .send(); - - expect(response.status).toBe(200); - expect(response.body).toBeInstanceOf(Object); - expect(response.body).toEqual( - expect.objectContaining({ - total: 2, - }) as FindRollenResponse, - ); - }); - - it('should return all rollen for a personenkontext based on PersonenkontextAnlage', async () => { - const rolleName: string = faker.string.alpha({ length: 10 }); - await rolleRepo.save(createRolle(rolleFactory, { name: rolleName })); - const response: Response = await request(app.getHttpServer() as App) - .get(`/personenkontext/rollen?rolleName=${rolleName}&limit=25`) - .send(); - - expect(response.status).toBe(200); - expect(response.body).toBeInstanceOf(Object); - }); - - it('should return empty list', async () => { - const response: Response = await request(app.getHttpServer() as App) - .get(`/personenkontext/rollen?rolleName=${faker.string.alpha()}&limit=25`) - .send(); - - expect(response.status).toBe(200); - expect(response.body).toBeInstanceOf(Object); - }); - }); - - describe('/GET schulstrukturknoten for personenkontext', () => { - it('should return all schulstrukturknoten for a personenkontext based on PersonenkontextAnlage', async () => { - const rolleName: string = faker.string.alpha({ length: 10 }); - const sskName: string = faker.company.name(); - const rolle: Rolle = await rolleRepo.save(createRolle(rolleFactory, { name: rolleName })); - const rolleId: string = rolle.id; - await organisationRepo.save(DoFactory.createOrganisation(false, { name: sskName })); - - const response: Response = await request(app.getHttpServer() as App) - .get(`/personenkontext/schulstrukturknoten?rolleId=${rolleId}&sskName=${sskName}&limit=25`) - .send(); - - expect(response.status).toBe(200); - expect(response.body).toBeInstanceOf(Object); - }); - - it('should return all schulstrukturknoten for a personenkontext based on PersonenkontextAnlage even when no sskName is provided', async () => { - const rolleName: string = faker.string.alpha({ length: 10 }); - const sskName: string = faker.company.name(); - const rolle: Rolle = await rolleRepo.save(createRolle(rolleFactory, { name: rolleName })); - const rolleId: string = rolle.id; - await organisationRepo.save(DoFactory.createOrganisation(false, { name: sskName })); - - const response: Response = await request(app.getHttpServer() as App) - .get(`/personenkontext/schulstrukturknoten?rolleId=${rolleId}&limit=25`) - .send(); - - expect(response.status).toBe(200); - expect(response.body).toBeInstanceOf(Object); - }); - - it('should return empty list', async () => { - const response: Response = await request(app.getHttpServer() as App) - .get( - `/personenkontext/schulstrukturknoten?rolleId=${faker.string.uuid()}&sskName=${faker.string.alpha()}&limit=25`, - ) - .send(); - - expect(response.status).toBe(200); - expect(response.body).toBeInstanceOf(Object); - }); - - it('should return empty list even when no sskName is provided', async () => { - const response: Response = await request(app.getHttpServer() as App) - .get(`/personenkontext/schulstrukturknoten?rolleId=${faker.string.uuid()}&limit=25`) - .send(); - - expect(response.status).toBe(200); - expect(response.body).toBeInstanceOf(Object); - }); - }); -}); diff --git a/src/modules/personenkontext/api/dbiam-personenkontext-filter.controller.ts b/src/modules/personenkontext/api/dbiam-personenkontext-filter.controller.ts deleted file mode 100644 index 10cd319bf..000000000 --- a/src/modules/personenkontext/api/dbiam-personenkontext-filter.controller.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Controller, Get, Inject, Query, UseFilters } from '@nestjs/common'; -import { - ApiBearerAuth, - ApiForbiddenResponse, - ApiInternalServerErrorResponse, - ApiOAuth2, - ApiOkResponse, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { SchulConnexValidationErrorFilter } from '../../../shared/error/schulconnex-validation-error.filter.js'; -import { FindPersonenkontextRollenBodyParams } from './param/find-personenkontext-rollen.body.params.js'; -import { FindPersonenkontextSchulstrukturknotenBodyParams } from './param/find-personenkontext-schulstrukturknoten.body.params.js'; -import { FindRollenResponse } from './response/find-rollen.response.js'; -import { FindSchulstrukturknotenResponse } from './response/find-schulstrukturknoten.response.js'; -import { PersonenkontextAnlage } from '../domain/personenkontext-anlage.js'; -import { Rolle } from '../../rolle/domain/rolle.js'; -import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; -import { OrganisationResponseLegacy } from '../../organisation/api/organisation.response.legacy.js'; -import { PersonenkontextAnlageFactory } from '../domain/personenkontext-anlage.factory.js'; -import { getMapperToken } from '@automapper/nestjs'; -import { Mapper } from '@automapper/core'; -import { Permissions } from '../../authentication/api/permissions.decorator.js'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; -import { AuthenticationExceptionFilter } from '../../authentication/api/authentication-exception-filter.js'; - -@UseFilters(SchulConnexValidationErrorFilter, new AuthenticationExceptionFilter()) -@ApiTags('personenkontext') -@ApiBearerAuth() -@ApiOAuth2(['openid']) -@Controller({ path: 'personenkontext' }) -export class DbiamPersonenkontextFilterController { - public constructor( - private readonly personenkontextAnlageFactory: PersonenkontextAnlageFactory, - @Inject(getMapperToken()) private readonly mapper: Mapper, - ) {} - - @Get('rollen') - @ApiOkResponse({ - description: 'The rollen for a personenkontext were successfully returned.', - type: FindRollenResponse, - }) - @ApiUnauthorizedResponse({ description: 'Not authorized to get available rolen for personenkontexte.' }) - @ApiForbiddenResponse({ description: 'Insufficient permission to get rollen for personenkontext.' }) - @ApiInternalServerErrorResponse({ description: 'Internal server error while getting rollen for personenkontexte.' }) - public async findRollen( - @Query() params: FindPersonenkontextRollenBodyParams, - @Permissions() permissions: PersonPermissions, - ): Promise { - const anlage: PersonenkontextAnlage = this.personenkontextAnlageFactory.createNew(); - const rollen: Rolle[] = await anlage.findAuthorizedRollen(permissions, params.rolleName, params.limit); - const response: FindRollenResponse = new FindRollenResponse(rollen, rollen.length); - - return response; - } - - @Get('schulstrukturknoten') - @ApiOkResponse({ - description: 'The schulstrukturknoten for a personenkontext were successfully returned.', - type: FindSchulstrukturknotenResponse, - }) - @ApiUnauthorizedResponse({ - description: 'Not authorized to get available schulstrukturknoten for personenkontexte.', - }) - @ApiForbiddenResponse({ description: 'Insufficient permission to get schulstrukturknoten for personenkontext.' }) - @ApiInternalServerErrorResponse({ - description: 'Internal server error while getting schulstrukturknoten for personenkontexte.', - }) - public async findSchulstrukturknoten( - @Query() params: FindPersonenkontextSchulstrukturknotenBodyParams, - @Permissions() permissions: PersonPermissions, - ): Promise { - const anlage: PersonenkontextAnlage = this.personenkontextAnlageFactory.createNew(); - const sskName: string = params.sskName ?? ''; - const ssks: OrganisationDo[] = await anlage.findSchulstrukturknoten( - permissions, - params.rolleId, - sskName, - params.limit, - true, - ); - const sskResponses: OrganisationResponseLegacy[] = this.mapper.mapArray( - ssks, - OrganisationDo, - OrganisationResponseLegacy, - ); - const response: FindSchulstrukturknotenResponse = new FindSchulstrukturknotenResponse( - sskResponses, - ssks.length, - ); - - return response; - } -} diff --git a/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.integration-spec.ts b/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.integration-spec.ts new file mode 100644 index 000000000..1de614391 --- /dev/null +++ b/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.integration-spec.ts @@ -0,0 +1,648 @@ +import { faker } from '@faker-js/faker'; +import { MikroORM } from '@mikro-orm/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'; +import { + ConfigTestModule, + DatabaseTestModule, + DoFactory, + KeycloakConfigTestModule, + MapperTestModule, +} from '../../../../test/utils/index.js'; +import { GlobalValidationPipe } from '../../../shared/validation/index.js'; +import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; +import { Rolle } from '../../rolle/domain/rolle.js'; +import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; +import { PersonenKontextApiModule } from '../personenkontext-api.module.js'; +import { RollenArt, RollenMerkmal, RollenSystemRecht } from '../../rolle/domain/rolle.enums.js'; +import { RolleFactory } from '../../rolle/domain/rolle.factory.js'; +import { PersonPermissionsRepo } from '../../authentication/domain/person-permission.repo.js'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { Observable } from 'rxjs'; +import { PassportUser } from '../../authentication/types/user.js'; +import { Request } from 'express'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { FindRollenResponse } from './response/find-rollen.response.js'; +import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; +import { PersonenkontextFactory } from '../domain/personenkontext.factory.js'; +import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.repo.js'; +import { PersonDo } from '../../person/domain/person.do.js'; +import { Personenkontext } from '../domain/personenkontext.js'; +import { DbiamUpdatePersonenkontexteBodyParams } from './param/dbiam-update-personenkontexte.body.params.js'; +import { PersonRepo } from '../../person/persistence/person.repo.js'; +import { PersonenkontexteUpdateError } from '../domain/error/personenkontexte-update.error.js'; +import { DBiamFindPersonenkontexteByPersonIdParams } from './param/dbiam-find-personenkontext-by-personid.params.js'; +import { PersonenkontextWorkflowAggregate } from '../domain/personenkontext-workflow.js'; +import { PersonenkontextWorkflowFactory } from '../domain/personenkontext-workflow.factory.js'; +import { FindDbiamPersonenkontextWorkflowBodyParams } from './param/dbiam-find-personenkontextworkflow-body.params.js'; +import { OrganisationsTyp } from '../../organisation/domain/organisation.enums.js'; +import { KeycloakAdministrationModule } from '../../keycloak-administration/keycloak-administration.module.js'; +import { KeycloakConfigModule } from '../../keycloak-administration/keycloak-config.module.js'; + +function createRolle(this: void, rolleFactory: RolleFactory, params: Partial> = {}): Rolle { + const rolle: Rolle = rolleFactory.createNew( + faker.string.alpha(), + faker.string.uuid(), + faker.helpers.enumValue(RollenArt), + [faker.helpers.enumValue(RollenMerkmal)], + [faker.helpers.enumValue(RollenSystemRecht)], + [], + ); + Object.assign(rolle, params); + + return rolle; +} + +function createPersonenkontext( + this: void, + personenkontextFactory: PersonenkontextFactory, + withId: WasPersisted, + params: Partial> = {}, +): Personenkontext { + const personenkontext: Personenkontext = personenkontextFactory.construct( + withId ? faker.string.uuid() : undefined, + withId ? faker.date.past() : undefined, + withId ? faker.date.recent() : undefined, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + + Object.assign(personenkontext, params); + + return personenkontext; +} + +describe('DbiamPersonenkontextWorkflowController Integration Test', () => { + let app: INestApplication; + let orm: MikroORM; + let organisationRepo: OrganisationRepo; + let rolleRepo: RolleRepo; + let rolleFactory: RolleFactory; + let personpermissionsRepoMock: DeepMocked; + let personRepo: PersonRepo; + let personenkontextRepo: DBiamPersonenkontextRepo; + let personenkontextFactory: PersonenkontextFactory; + let personenkontextWorkflowMock: DeepMocked; + let personenkontextWorkflowFactoryMock: DeepMocked; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + MapperTestModule, + ConfigTestModule, + DatabaseTestModule.forRoot({ isDatabaseRequired: true }), + PersonenKontextApiModule, + KeycloakAdministrationModule, + ], + providers: [ + { + provide: APP_PIPE, + useClass: GlobalValidationPipe, + }, + { + provide: PersonPermissionsRepo, + useValue: createMock(), + }, + { + provide: PersonenkontextWorkflowFactory, + useValue: createMock(), + }, + { + provide: PersonenkontextWorkflowAggregate, + useValue: createMock(), + }, + { + 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(); + }, + }, + }, + ], + }) + .overrideModule(KeycloakConfigModule) + .useModule(KeycloakConfigTestModule.forRoot({ isKeycloakRequired: true })) + .compile(); + + orm = module.get(MikroORM); + organisationRepo = module.get(OrganisationRepo); + rolleRepo = module.get(RolleRepo); + rolleFactory = module.get(RolleFactory); + personpermissionsRepoMock = module.get(PersonPermissionsRepo); + personRepo = module.get(PersonRepo); + personenkontextRepo = module.get(DBiamPersonenkontextRepo); + personenkontextFactory = module.get(PersonenkontextFactory); + personenkontextWorkflowMock = module.get(PersonenkontextWorkflowAggregate); + personenkontextWorkflowFactoryMock = createMock(); + + await DatabaseTestModule.setupDatabase(orm); + app = module.createNestApplication(); + await app.init(); + }, 10000000); + + afterAll(async () => { + await orm.close(); + await app.close(); + }); + + beforeEach(async () => { + await DatabaseTestModule.clearDatabase(orm); + }); + + describe('/POST create person with personenkontext', () => { + it('should return created person and personenkontext', async () => { + const organisation: OrganisationDo = await organisationRepo.save( + DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE }), + ); + const rolle: Rolle = await rolleRepo.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: organisation.id, + rollenart: RollenArt.LEHR, + }), + ); + + const personpermissions: DeepMocked = createMock(); + personpermissions.hasSystemrechtAtOrganisation.mockResolvedValueOnce(true); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const response: Response = await request(app.getHttpServer() as App) + .post('/personenkontext-workflow') + .send({ + familienname: faker.person.lastName(), + vorname: faker.person.firstName(), + organisationId: organisation.id, + rolleId: rolle.id, + }); + expect(response.status).toBe(201); + }); + + it('should return error with status-code=404 if organisation does NOT exist', async () => { + const rolle: Rolle = await rolleRepo.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: faker.string.uuid(), + rollenart: RollenArt.LEHR, + }), + ); + const permissions: DeepMocked = createMock(); + permissions.hasSystemrechtAtOrganisation.mockResolvedValueOnce(true); + permissions.canModifyPerson.mockResolvedValueOnce(true); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissions); + + const response: Response = await request(app.getHttpServer() as App) + .post('/personenkontext-workflow') + .send({ + familienname: faker.person.lastName(), + vorname: faker.person.firstName(), + organisationId: faker.string.uuid(), + rolleId: rolle.id, + }); + + expect(response.status).toBe(404); + }); + + it('should return error with status-code 400 if specification ROLLE_NUR_AN_PASSENDE_ORGANISATION is NOT satisfied', async () => { + const organisation: OrganisationDo = await organisationRepo.save( + DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE }), + ); + const rolle: Rolle = await rolleRepo.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: organisation.id, + rollenart: RollenArt.SYSADMIN, + }), + ); + + const personpermissions: DeepMocked = createMock(); + personpermissions.hasSystemrechtAtOrganisation.mockResolvedValueOnce(true); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const response: Response = await request(app.getHttpServer() as App) + .post('/personenkontext-workflow') + .send({ + familienname: faker.person.lastName(), + vorname: faker.person.firstName(), + organisationId: organisation.id, + rolleId: rolle.id, + }); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ + code: 400, + i18nKey: 'ROLLE_NUR_AN_PASSENDE_ORGANISATION', + }); + }); + + it('should return error with status-code 404 if user does NOT have permissions', async () => { + const organisation: OrganisationDo = await organisationRepo.save( + DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE }), + ); + const rolle: Rolle = await rolleRepo.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: organisation.id, + rollenart: RollenArt.LEHR, + }), + ); + + const personpermissions: DeepMocked = createMock(); + personpermissions.hasSystemrechtAtOrganisation.mockResolvedValueOnce(false); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const response: Response = await request(app.getHttpServer() as App) + .post('/personenkontext-workflow') + .send({ + familienname: faker.person.lastName(), + vorname: faker.person.firstName(), + organisationId: organisation.id, + rolleId: rolle.id, + }); + expect(response.status).toBe(404); + expect(response.body).toEqual({ + code: 404, + subcode: '01', + titel: 'Angefragte Entität existiert nicht', + beschreibung: 'Die angeforderte Entität existiert nicht', + }); + }); + }); + + describe('/GET processStep for personenkontext', () => { + it('should return selected organisation and all rollen', async () => { + const organisationName: string = faker.company.name(); + const organisation: OrganisationDo = await organisationRepo.save( + DoFactory.createOrganisation(false, { name: organisationName }), + ); + + const rolleName: string = faker.string.alpha({ length: 10 }); + const rolle: Rolle = await rolleRepo.save( + createRolle(rolleFactory, { + name: rolleName, + administeredBySchulstrukturknoten: organisation.id, + rollenart: RollenArt.LERN, + }), + ); + + const personpermissions: DeepMocked = createMock(); + personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const anlageMock: DeepMocked = + createMock({ + findAllSchulstrukturknoten: jest.fn().mockResolvedValue([]), + findRollenForOrganisation: jest.fn().mockResolvedValue(rolle), + }); + + personenkontextWorkflowFactoryMock.createNew.mockReturnValue(anlageMock); + + const response: Response = await request(app.getHttpServer() as App) + .get('/personenkontext-workflow/step') + .query({ organisationId: organisation.id }) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + }); + + it('should handle request with no organisationId', async () => { + const organisationName: string = faker.company.name(); + const randomName: string = faker.company.name(); + + const organisation: OrganisationDo = await organisationRepo.save( + DoFactory.createOrganisation(false, { name: organisationName }), + ); + const organisations: OrganisationDo[] = [organisation]; + + const personpermissions: DeepMocked = createMock(); + personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisation.id]); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + personenkontextWorkflowMock.findAllSchulstrukturknoten.mockResolvedValue(organisations); + + const response: Response = await request(app.getHttpServer() as App) + .get(`/personenkontext/step/organisationName?organisationName=${randomName}`) + .send(); + + expect(response.status).toEqual(404); + }); + it('should call findAllSchulstrukturknoten when no organisationId is provided', async () => { + const personpermissions: DeepMocked = createMock(); + personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([faker.string.uuid()]); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const anlageMock: DeepMocked = + createMock({ + findAllSchulstrukturknoten: jest.fn().mockResolvedValue('organisations'), + }); + + personenkontextWorkflowFactoryMock.createNew.mockReturnValue(anlageMock); + + const response: Response = await request(app.getHttpServer() as App) + .get('/personenkontext-workflow/step') + .send(); + + expect(response.status).toEqual(200); + }); + + it('should call findRollenForOrganisation when organisationId is provided', async () => { + const organisation: OrganisationDo = await organisationRepo.save( + DoFactory.createOrganisation(false, { name: faker.company.name() }), + ); + + const rolle: Rolle = await rolleRepo.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: organisation.id, + rollenart: RollenArt.LERN, + }), + ); + + const personpermissions: DeepMocked = createMock(); + personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const rollen: Rolle[] = [rolle]; + + const anlageMock: DeepMocked = + createMock({ + findRollenForOrganisation: jest.fn().mockResolvedValue(rollen), + }); + + personenkontextWorkflowFactoryMock.createNew.mockReturnValue(anlageMock); + + const response: Response = await request(app.getHttpServer() as App) + .get('/personenkontext-workflow/step') + .query({ organisationId: organisation.id }) + .send(); + + expect(response.status).toEqual(200); + }); + + it('should return empty organisations and empty roles if organisationId is provided but no roles nor orgas are found', async () => { + const organisationName: string = faker.company.name(); + const organisation: OrganisationDo = await organisationRepo.save( + DoFactory.createOrganisation(false, { name: organisationName }), + ); + + const personpermissions: DeepMocked = createMock(); + personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const response: Response = await request(app.getHttpServer() as App) + .get('/personenkontext-workflow/step') + .query({ organisationId: organisation.id }) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + }); + + it('should set canCommit to true if canCommit returns true', async () => { + const organisationId: string = faker.string.uuid(); + // Create and save an organisation and a rolle + const organisation: OrganisationDo = await organisationRepo.save( + DoFactory.createOrganisation(false, { id: organisationId, typ: OrganisationsTyp.LAND }), + ); + + const rolle: Rolle = await rolleRepo.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: organisationId, + rollenart: RollenArt.SYSADMIN, + }), + ); + const rolleId: string = rolle.id; + + organisationRepo.findById = jest.fn().mockResolvedValue(organisation); + rolleRepo.findById = jest.fn().mockResolvedValue(rolle); + + const personpermissions: DeepMocked = createMock(); + personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const params: FindDbiamPersonenkontextWorkflowBodyParams = { + organisationId, + rolleId, + organisationName: undefined, + rolleName: undefined, + limit: undefined, + }; + const rollen: Rolle[] = [rolle]; + + personenkontextWorkflowMock.findRollenForOrganisation.mockResolvedValue(rollen); + personenkontextWorkflowMock.canCommit.mockResolvedValue(true); + + const response: Response = await request(app.getHttpServer() as App) + .get('/personenkontext-workflow/step') + .query({ organisationId: params.organisationId, rolleId: params.rolleId }) + .send(); + + expect(response.status).toEqual(200); + }); + }); + describe('/PUT commit', () => { + describe('when sending no PKs', () => { + it('should delete and therefore return 200', async () => { + const person: PersonDo = await personRepo.save(DoFactory.createPerson(false)); + const rolle: Rolle = await rolleRepo.save(DoFactory.createRolle(false)); + const savedPK: Personenkontext = await personenkontextRepo.save( + createPersonenkontext(personenkontextFactory, false, { + personId: person.id, + rolleId: rolle.id, + updatedAt: new Date(), + }), + ); + const updatePKsRequest: DbiamUpdatePersonenkontexteBodyParams = + createMock({ + count: 1, + lastModified: savedPK.updatedAt, + personenkontexte: [], + }); + + const response: Response = await request(app.getHttpServer() as App) + .put(`/personenkontext-workflow/${person.id}`) + .send(updatePKsRequest); + + expect(response.status).toBe(200); + }); + }); + it('should throw BadRequestException if updateResult is an instance of PersonenkontexteUpdateError', async () => { + const params: DBiamFindPersonenkontexteByPersonIdParams = { personId: faker.string.uuid() }; + const bodyParams: DbiamUpdatePersonenkontexteBodyParams = { + count: 1, + lastModified: new Date(), + personenkontexte: [], + }; + const updateError: PersonenkontexteUpdateError = new PersonenkontexteUpdateError('Update error message'); + personenkontextWorkflowMock.commit.mockResolvedValue(updateError); + + const response: Response = await request(app.getHttpServer() as App) + .put(`/personenkontext-workflow/${params.personId}`) + .send(bodyParams); + + expect(response.status).toBe(400); + }); + + describe('when errors occur', () => { + it('should return error because the count is not matching', async () => { + const person: PersonDo = await personRepo.save(DoFactory.createPerson(false)); + const rolle: Rolle = await rolleRepo.save(DoFactory.createRolle(false)); + const savedPK: Personenkontext = await personenkontextRepo.save( + createPersonenkontext(personenkontextFactory, false, { + personId: person.id, + rolleId: rolle.id, + updatedAt: new Date(), + }), + ); + const updatePKsRequest: DbiamUpdatePersonenkontexteBodyParams = + createMock({ + count: 0, + lastModified: savedPK.updatedAt, + personenkontexte: [], + }); + + const response: Response = await request(app.getHttpServer() as App) + .put(`/personenkontext-workflow/${person.id}`) + .send(updatePKsRequest); + + expect(response.status).toBe(400); + }); + it('should throw BadRequestException if updateResult is an instance of PersonenkontexteUpdateError', async () => { + const params: DBiamFindPersonenkontexteByPersonIdParams = { personId: faker.string.uuid() }; + const bodyParams: DbiamUpdatePersonenkontexteBodyParams = { + count: 1, + lastModified: new Date(), + personenkontexte: [], + }; + const updateError: PersonenkontexteUpdateError = new PersonenkontexteUpdateError( + 'Update error message', + ); + personenkontextWorkflowMock.commit.mockResolvedValue(updateError); + + const response: Response = await request(app.getHttpServer() as App) + .put(`/personenkontext-workflow/${params.personId}`) + .send(bodyParams); + + expect(response.status).toBe(400); + }); + it('should rethrow generic errors', async () => { + const params: DBiamFindPersonenkontexteByPersonIdParams = { personId: faker.string.uuid() }; + const bodyParams: DbiamUpdatePersonenkontexteBodyParams = { + count: 0, + lastModified: new Date(), + personenkontexte: [], + }; + const genericError: Error = new Error('Generic error message'); + personenkontextWorkflowMock.commit.mockRejectedValue(genericError); + + const response: Response = await request(app.getHttpServer() as App) + .put(`/personenkontext-workflow/${params.personId}`) + .send(bodyParams); + + expect(response.status).toBe(500); + }); + }); + }); + + describe('/GET rollen for personenkontext', () => { + it('should return all rollen for a personenkontext without filter, if the user is Landesadmin', async () => { + const rolleName: string = faker.string.alpha({ length: 10 }); + await rolleRepo.save(createRolle(rolleFactory, { name: rolleName, rollenart: RollenArt.SYSADMIN })); + const schuladminRolleName: string = faker.string.alpha({ length: 10 }); + await rolleRepo.save(createRolle(rolleFactory, { name: schuladminRolleName, rollenart: RollenArt.LEIT })); + + const personpermissions: DeepMocked = createMock(); + personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationRepo.ROOT_ORGANISATION_ID]); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + + const response: Response = await request(app.getHttpServer() as App) + .get('/personenkontext-workflow/rollen') + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + expect(response.body).toEqual( + expect.objectContaining({ + total: 2, + }) as FindRollenResponse, + ); + }); + + it('should return all rollen for a personenkontext based on PersonenkontextAnlage', async () => { + const rolleName: string = faker.string.alpha({ length: 10 }); + await rolleRepo.save(createRolle(rolleFactory, { name: rolleName })); + const response: Response = await request(app.getHttpServer() as App) + .get(`/personenkontext-workflow/rollen?rolleName=${rolleName}&limit=25`) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + }); + + it('should return empty list', async () => { + const response: Response = await request(app.getHttpServer() as App) + .get(`/personenkontext-workflow/rollen?rolleName=${faker.string.alpha()}&limit=25`) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + }); + }); + + describe('/GET schulstrukturknoten for personenkontext', () => { + it('should return all schulstrukturknoten for a personenkontext based on PersonenkontextAnlage', async () => { + const rolleName: string = faker.string.alpha({ length: 10 }); + const sskName: string = faker.company.name(); + const rolle: Rolle = await rolleRepo.save(createRolle(rolleFactory, { name: rolleName })); + const rolleId: string = rolle.id; + await organisationRepo.save(DoFactory.createOrganisation(false, { name: sskName })); + + const response: Response = await request(app.getHttpServer() as App) + .get(`/personenkontext-workflow/schulstrukturknoten?rolleId=${rolleId}&sskName=${sskName}&limit=25`) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + }); + + it('should return all schulstrukturknoten for a personenkontext based on PersonenkontextAnlage even when no sskName is provided', async () => { + const rolleName: string = faker.string.alpha({ length: 10 }); + const sskName: string = faker.company.name(); + const rolle: Rolle = await rolleRepo.save(createRolle(rolleFactory, { name: rolleName })); + const rolleId: string = rolle.id; + await organisationRepo.save(DoFactory.createOrganisation(false, { name: sskName })); + + const response: Response = await request(app.getHttpServer() as App) + .get(`/personenkontext-workflow/schulstrukturknoten?rolleId=${rolleId}&limit=25`) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + }); + + it('should return empty list', async () => { + const response: Response = await request(app.getHttpServer() as App) + .get( + `/personenkontext-workflow/schulstrukturknoten?rolleId=${faker.string.uuid()}&sskName=${faker.string.alpha()}&limit=25`, + ) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + }); + + it('should return empty list even when no sskName is provided', async () => { + const response: Response = await request(app.getHttpServer() as App) + .get(`/personenkontext-workflow/schulstrukturknoten?rolleId=${faker.string.uuid()}&limit=25`) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + }); + }); +}); diff --git a/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.ts b/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.ts new file mode 100644 index 000000000..99461f862 --- /dev/null +++ b/src/modules/personenkontext/api/dbiam-personenkontext-workflow.controller.ts @@ -0,0 +1,264 @@ +import { + BadRequestException, + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Inject, + Param, + Post, + Put, + Query, + UseFilters, +} from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiConflictResponse, + ApiCreatedResponse, + ApiForbiddenResponse, + ApiInternalServerErrorResponse, + ApiOAuth2, + ApiOkResponse, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; +import { SchulConnexValidationErrorFilter } from '../../../shared/error/schulconnex-validation-error.filter.js'; +import { FindPersonenkontextRollenBodyParams } from './param/find-personenkontext-rollen.body.params.js'; +import { FindPersonenkontextSchulstrukturknotenBodyParams } from './param/find-personenkontext-schulstrukturknoten.body.params.js'; +import { FindRollenResponse } from './response/find-rollen.response.js'; +import { FindSchulstrukturknotenResponse } from './response/find-schulstrukturknoten.response.js'; +import { PersonenkontextWorkflowAggregate } from '../domain/personenkontext-workflow.js'; +import { Rolle } from '../../rolle/domain/rolle.js'; +import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; +import { OrganisationResponseLegacy } from '../../organisation/api/organisation.response.legacy.js'; +import { PersonenkontextWorkflowFactory } from '../domain/personenkontext-workflow.factory.js'; +import { getMapperToken } from '@automapper/nestjs'; +import { Mapper } from '@automapper/core'; +import { Permissions } from '../../authentication/api/permissions.decorator.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { FindDbiamPersonenkontextWorkflowBodyParams } from './param/dbiam-find-personenkontextworkflow-body.params.js'; +import { PersonenkontextWorkflowResponse } from './response/dbiam-personenkontext-workflow-response.js'; +import { PersonenkontexteUpdateError } from '../domain/error/personenkontexte-update.error.js'; +import { Personenkontext } from '../domain/personenkontext.js'; +import { DBiamFindPersonenkontexteByPersonIdParams } from './param/dbiam-find-personenkontext-by-personid.params.js'; +import { DbiamUpdatePersonenkontexteBodyParams } from './param/dbiam-update-personenkontexte.body.params.js'; +import { PersonenkontexteUpdateResponse } from './response/personenkontexte-update.response.js'; +import { DbiamPersonenkontexteUpdateError } from './dbiam-personenkontexte-update.error.js'; +import { DomainError } from '../../../shared/error/domain.error.js'; +import { DBiamPersonResponse } from './response/dbiam-person.response.js'; +import { DbiamPersonenkontextError } from './dbiam-personenkontext.error.js'; +import { DbiamCreatePersonWithContextBodyParams } from './param/dbiam-create-person-with-context.body.params.js'; +import { PersonPersonenkontext, PersonenkontextCreationService } from '../domain/personenkontext-creation.service.js'; +import { PersonenkontextCommitError } from '../domain/error/personenkontext-commit.error.js'; +import { PersonenkontextSpecificationError } from '../specification/error/personenkontext-specification.error.js'; +import { SchulConnexErrorMapper } from '../../../shared/error/schul-connex-error.mapper.js'; +import { PersonenkontextExceptionFilter } from './personenkontext-exception-filter.js'; + +@UseFilters(SchulConnexValidationErrorFilter, new PersonenkontextExceptionFilter()) +@ApiTags('personenkontext') +@ApiBearerAuth() +@ApiOAuth2(['openid']) +@Controller({ path: 'personenkontext-workflow' }) +export class DbiamPersonenkontextWorkflowController { + public constructor( + private readonly personenkontextWorkflowFactory: PersonenkontextWorkflowFactory, + private readonly personenkontextCreationService: PersonenkontextCreationService, + @Inject(getMapperToken()) private readonly mapper: Mapper, + ) {} + + @Get('step') + @ApiOkResponse({ + description: `Initialize or process data from the person creation form. + Valid combinations: + - Both organisationId and rolleId are undefined: Fetch all possible organisations. + - organisationId is provided, but rolleId is undefined: Fetch Rollen for the given organisation. + - Both organisationId and rolleId are provided: Check if the Rolle can be committed for the organisation. + Note: Providing rolleId without organisationId is invalid.`, + type: PersonenkontextWorkflowResponse, + }) + @ApiUnauthorizedResponse({ description: 'Not authorized to get available data for personenkontext.' }) + @ApiForbiddenResponse({ description: 'Insufficient permission to get data for personenkontext.' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error while getting data for personenkontext.' }) + public async processStep( + @Query() params: FindDbiamPersonenkontextWorkflowBodyParams, + @Permissions() permissions: PersonPermissions, + ): Promise { + // Creates a new instance of the workflow aggregate + const anlage: PersonenkontextWorkflowAggregate = this.personenkontextWorkflowFactory.createNew(); + + // Initializes the aggregate with the values of the selected organisation and rolle through the UI + // (Both values could be undefined when nothing was done yet) + anlage.initialize(params.organisationId, params.rolleId); + + // Find all possible SSKs (Possibly through name if the name was given) + const organisations: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + permissions, + params.organisationName, + params.limit, + ); + + // Find all possible roles under the selected Organisation + const rollen: Rolle[] = params.organisationId + ? await anlage.findRollenForOrganisation(permissions, params.rolleName, params.limit) + : []; + + const organisationsResponse: OrganisationResponseLegacy[] = this.mapper.mapArray( + organisations, + OrganisationDo, + OrganisationResponseLegacy, + ); + + // Determine canCommit status, by default it's always false unless both the rolle and orga are selected + let canCommit: boolean = false; + if (params.organisationId && params.rolleId) { + const commitResult: DomainError | boolean = await anlage.canCommit(permissions); + if (commitResult === true) { + canCommit = true; + } + } + + const response: PersonenkontextWorkflowResponse = new PersonenkontextWorkflowResponse( + organisationsResponse, + rollen, + canCommit, + params.organisationId, + params.rolleId, + ); + + return response; + } + + @Put(':personId') + @HttpCode(HttpStatus.OK) + @ApiOkResponse({ + description: + 'Add or remove personenkontexte as one operation. Returns the Personenkontexte existing after update.', + type: PersonenkontexteUpdateResponse, + }) + @ApiBadRequestResponse({ + description: 'The personenkontexte could not be updated, may due to unsatisfied specifications.', + type: DbiamPersonenkontexteUpdateError, + }) + @ApiConflictResponse({ description: 'Changes are conflicting with current state of personenkontexte.' }) + @ApiUnauthorizedResponse({ description: 'Not authorized to update personenkontexte.' }) + @ApiForbiddenResponse({ description: 'Insufficient permission to update personenkontexte.' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error while updating personenkontexte.' }) + public async commit( + @Param() params: DBiamFindPersonenkontexteByPersonIdParams, + @Body() bodyParams: DbiamUpdatePersonenkontexteBodyParams, + ): Promise { + const updateResult: Personenkontext[] | PersonenkontexteUpdateError = + await this.personenkontextWorkflowFactory + .createNew() + .commit(params.personId, bodyParams.lastModified, bodyParams.count, bodyParams.personenkontexte); + + if (updateResult instanceof DomainError) { + throw new BadRequestException(updateResult.message); + } + return new PersonenkontexteUpdateResponse(updateResult); + } + + @Get('rollen') + @ApiOkResponse({ + description: 'The rollen for a personenkontext were successfully returned.', + type: FindRollenResponse, + }) + @ApiUnauthorizedResponse({ description: 'Not authorized to get available rolen for personenkontexte.' }) + @ApiForbiddenResponse({ description: 'Insufficient permission to get rollen for personenkontext.' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error while getting rollen for personenkontexte.' }) + public async findRollen( + @Query() params: FindPersonenkontextRollenBodyParams, + @Permissions() permissions: PersonPermissions, + ): Promise { + const anlage: PersonenkontextWorkflowAggregate = this.personenkontextWorkflowFactory.createNew(); + const rollen: Rolle[] = await anlage.findAuthorizedRollen(permissions, params.rolleName, params.limit); + const response: FindRollenResponse = new FindRollenResponse(rollen, rollen.length); + + return response; + } + + @Get('schulstrukturknoten') + @ApiOkResponse({ + description: 'The schulstrukturknoten for a personenkontext were successfully returned.', + type: FindSchulstrukturknotenResponse, + }) + @ApiUnauthorizedResponse({ + description: 'Not authorized to get available schulstrukturknoten for personenkontexte.', + }) + @ApiForbiddenResponse({ description: 'Insufficient permission to get schulstrukturknoten for personenkontext.' }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error while getting schulstrukturknoten for personenkontexte.', + }) + public async findSchulstrukturknoten( + @Query() params: FindPersonenkontextSchulstrukturknotenBodyParams, + ): Promise { + const anlage: PersonenkontextWorkflowAggregate = this.personenkontextWorkflowFactory.createNew(); + const sskName: string = params.sskName ?? ''; + const ssks: OrganisationDo[] = await anlage.findSchulstrukturknoten( + params.rolleId, + sskName, + params.limit, + true, + ); + const sskResponses: OrganisationResponseLegacy[] = this.mapper.mapArray( + ssks, + OrganisationDo, + OrganisationResponseLegacy, + ); + const response: FindSchulstrukturknotenResponse = new FindSchulstrukturknotenResponse( + sskResponses, + ssks.length, + ); + + return response; + } + + @Post() + @HttpCode(HttpStatus.CREATED) + @ApiCreatedResponse({ + description: 'Person with Personenkontext was successfully created.', + type: DBiamPersonResponse, + }) + @ApiBadRequestResponse({ + description: 'The person and the personenkontext could not be created, may due to unsatisfied specifications.', + type: DbiamPersonenkontextError, + }) + @ApiUnauthorizedResponse({ description: 'Not authorized to create person with personenkontext.' }) + @ApiForbiddenResponse({ description: 'Insufficient permission to create person with personenkontext.' }) + @ApiForbiddenResponse({ description: 'Insufficient permissions to create the person with personenkontext.' }) + @ApiBadRequestResponse({ description: 'Request has wrong format.', type: DbiamPersonenkontextError }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error while creating person with personenkontext.', + }) + public async createPersonWithKontext( + @Body() params: DbiamCreatePersonWithContextBodyParams, + @Permissions() permissions: PersonPermissions, + ): Promise { + //Check all references & permissions then save person + const savedPersonWithPersonenkontext: PersonPersonenkontext | DomainError | PersonenkontextCommitError = + await this.personenkontextCreationService.createPersonWithPersonenkontext( + permissions, + params.vorname, + params.familienname, + params.organisationId, + params.rolleId, + ); + + if (savedPersonWithPersonenkontext instanceof PersonenkontextSpecificationError) { + throw savedPersonWithPersonenkontext; + } + + if (savedPersonWithPersonenkontext instanceof DomainError) { + throw SchulConnexErrorMapper.mapSchulConnexErrorToHttpException( + SchulConnexErrorMapper.mapDomainErrorToSchulConnexError(savedPersonWithPersonenkontext), + ); + } + + return new DBiamPersonResponse( + savedPersonWithPersonenkontext.person, + savedPersonWithPersonenkontext.personenkontext, + ); + } +} diff --git a/src/modules/personenkontext/api/dbiam-personenkontext.controller.integration.spec.ts b/src/modules/personenkontext/api/dbiam-personenkontext.controller.integration.spec.ts index 80fb79226..f5044985a 100644 --- a/src/modules/personenkontext/api/dbiam-personenkontext.controller.integration.spec.ts +++ b/src/modules/personenkontext/api/dbiam-personenkontext.controller.integration.spec.ts @@ -10,9 +10,9 @@ import request, { Response } from 'supertest'; import { App } from 'supertest/types.js'; import { ConfigTestModule, - DEFAULT_TIMEOUT_FOR_TESTCONTAINERS, DatabaseTestModule, DoFactory, + KeycloakConfigTestModule, MapperTestModule, } from '../../../../test/utils/index.js'; import { GlobalValidationPipe } from '../../../shared/validation/index.js'; @@ -32,6 +32,8 @@ import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.r import { RollenArt } from '../../rolle/domain/rolle.enums.js'; import { DbiamUpdatePersonenkontexteBodyParams } from './param/dbiam-update-personenkontexte.body.params.js'; import { PersonenKontextApiModule } from '../personenkontext-api.module.js'; +import { KeycloakConfigModule } from '../../keycloak-administration/keycloak-config.module.js'; +import { KeycloakAdministrationModule } from '../../keycloak-administration/keycloak-administration.module.js'; function createPersonenkontext( this: void, @@ -71,6 +73,7 @@ describe('dbiam Personenkontext API', () => { ConfigTestModule, DatabaseTestModule.forRoot({ isDatabaseRequired: true }), PersonenKontextApiModule, + KeycloakAdministrationModule, ], providers: [ { @@ -96,7 +99,10 @@ describe('dbiam Personenkontext API', () => { }, }, ], - }).compile(); + }) + .overrideModule(KeycloakConfigModule) + .useModule(KeycloakConfigTestModule.forRoot({ isKeycloakRequired: true })) + .compile(); orm = module.get(MikroORM); personenkontextRepo = module.get(DBiamPersonenkontextRepo); @@ -109,7 +115,7 @@ describe('dbiam Personenkontext API', () => { await DatabaseTestModule.setupDatabase(orm); app = module.createNestApplication(); await app.init(); - }, DEFAULT_TIMEOUT_FOR_TESTCONTAINERS); + }, 10000000); afterAll(async () => { await orm.close(); diff --git a/src/modules/person/api/dbiam-create-person-with-context.body.params.ts b/src/modules/personenkontext/api/param/dbiam-create-person-with-context.body.params.ts similarity index 79% rename from src/modules/person/api/dbiam-create-person-with-context.body.params.ts rename to src/modules/personenkontext/api/param/dbiam-create-person-with-context.body.params.ts index 2574ebe60..10b8f15c0 100644 --- a/src/modules/person/api/dbiam-create-person-with-context.body.params.ts +++ b/src/modules/personenkontext/api/param/dbiam-create-person-with-context.body.params.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, MinLength } from 'class-validator'; -import { OrganisationID, RolleID } from '../../../shared/types/aggregate-ids.types.js'; -import { IsDIN91379A } from '../../../shared/util/din-91379-validation.js'; +import { IsDIN91379A } from '../../../../shared/util/din-91379-validation.js'; +import { OrganisationID, RolleID } from '../../../../shared/types/aggregate-ids.types.js'; export class DbiamCreatePersonWithContextBodyParams { @IsDIN91379A() diff --git a/src/modules/personenkontext/api/param/dbiam-find-personenkontextworkflow-body.params.ts b/src/modules/personenkontext/api/param/dbiam-find-personenkontextworkflow-body.params.ts new file mode 100644 index 000000000..e7f0caec3 --- /dev/null +++ b/src/modules/personenkontext/api/param/dbiam-find-personenkontextworkflow-body.params.ts @@ -0,0 +1,50 @@ +import { IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class FindDbiamPersonenkontextWorkflowBodyParams { + @IsUUID() + @IsOptional() + @ApiProperty({ + description: 'ID of the organisation to filter the rollen later', + required: false, + nullable: true, + }) + public readonly organisationId?: string; + + @IsUUID() + @IsOptional() + @ApiProperty({ + description: 'ID of the rolle.', + required: false, + nullable: true, + }) + public readonly rolleId?: string; + + @IsString() + @IsOptional() + @ApiProperty({ + description: 'Rolle name used to filter for rollen in personenkontext.', + required: false, + nullable: true, + }) + public readonly rolleName?: string; + + @IsString() + @IsOptional() + @ApiProperty({ + description: 'Organisation/SSK name used to filter for schulstrukturknoten in personenkontext.', + required: false, + nullable: true, + }) + public readonly organisationName?: string; + + @IsNotEmpty() + @IsNumber() + @IsOptional() + @ApiProperty({ + description: 'The limit of items for the request.', + required: false, + nullable: false, + }) + public readonly limit?: number; +} diff --git a/src/modules/personenkontext/api/personenkontext.uc.spec.ts b/src/modules/personenkontext/api/personenkontext.uc.spec.ts index c6ef990a7..f7323c6e2 100644 --- a/src/modules/personenkontext/api/personenkontext.uc.spec.ts +++ b/src/modules/personenkontext/api/personenkontext.uc.spec.ts @@ -22,7 +22,6 @@ import { SchulConnexError } from '../../../shared/error/schul-connex.error.js'; import { DeletePersonenkontextDto } from './delete-personkontext.dto.js'; import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; -import { PersonenkontextAnlage } from '../domain/personenkontext-anlage.js'; import { RollenArt, RollenSystemRecht } from '../../rolle/domain/rolle.enums.js'; import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; import { Personenkontext } from '../domain/personenkontext.js'; @@ -99,10 +98,6 @@ describe('PersonenkontextUc', () => { provide: PersonenkontextService, useValue: createMock(), }, - { - provide: PersonenkontextAnlage, - useValue: createMock(), - }, { provide: ServiceProviderRepo, useValue: createMock(), diff --git a/src/modules/person/api/dbiam-person.response.ts b/src/modules/personenkontext/api/response/dbiam-person.response.ts similarity index 56% rename from src/modules/person/api/dbiam-person.response.ts rename to src/modules/personenkontext/api/response/dbiam-person.response.ts index 01fb95f4b..af6794172 100644 --- a/src/modules/person/api/dbiam-person.response.ts +++ b/src/modules/personenkontext/api/response/dbiam-person.response.ts @@ -1,10 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; - -import { PersonResponse } from './person.response.js'; -import { Personenkontext } from '../../personenkontext/domain/personenkontext.js'; -import { Person } from '../domain/person.js'; -import { PersonendatensatzResponse } from './personendatensatz.response.js'; -import { DBiamPersonenkontextResponse } from '../../personenkontext/api/response/dbiam-personenkontext.response.js'; +import { PersonResponse } from '../../../person/api/person.response.js'; +import { DBiamPersonenkontextResponse } from './dbiam-personenkontext.response.js'; +import { Person } from '../../../person/domain/person.js'; +import { Personenkontext } from '../../domain/personenkontext.js'; +import { PersonendatensatzResponse } from '../../../person/api/personendatensatz.response.js'; export class DBiamPersonResponse { @ApiProperty() diff --git a/src/modules/personenkontext/api/response/dbiam-personenkontext-workflow-response.ts b/src/modules/personenkontext/api/response/dbiam-personenkontext-workflow-response.ts new file mode 100644 index 000000000..af6aa1fb7 --- /dev/null +++ b/src/modules/personenkontext/api/response/dbiam-personenkontext-workflow-response.ts @@ -0,0 +1,65 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { OrganisationResponseLegacy } from '../../../organisation/api/organisation.response.legacy.js'; +import { RolleResponse } from '../../../rolle/api/rolle.response.js'; +import { Rolle } from '../../../rolle/domain/rolle.js'; + +export class PersonenkontextWorkflowResponse { + @ApiProperty({ + description: 'List of available organisations.', + type: [OrganisationResponseLegacy], + }) + public readonly organisations: OrganisationResponseLegacy[]; + + @ApiProperty({ + description: 'List of available roles.', + type: [RolleResponse], + }) + public readonly rollen: RolleResponse[]; + + @ApiProperty({ + description: 'Selected organisation.', + type: String, + nullable: true, + }) + public readonly selectedOrganisation?: string; + + @ApiProperty({ + description: 'Selected rolle.', + type: String, + nullable: true, + }) + public readonly selectedRolle?: string; + + @ApiProperty({ + description: 'Indicates whether the commit action can be performed.', + type: Boolean, + }) + public readonly canCommit: boolean; + + public constructor( + organisations: OrganisationResponseLegacy[], + rollen: Rolle[], + canCommit: boolean, + selectedOrganisation?: string, + selectedRole?: string, + ) { + this.organisations = organisations; + this.rollen = rollen.map((rolle: Rolle) => this.createRolleResponse(rolle)); + this.selectedOrganisation = selectedOrganisation; + this.selectedRolle = selectedRole; + this.canCommit = canCommit; + } + + private createRolleResponse(rolle: Rolle): RolleResponse { + return { + createdAt: rolle.createdAt, + updatedAt: rolle.updatedAt, + name: rolle.name, + id: rolle.id, + administeredBySchulstrukturknoten: rolle.administeredBySchulstrukturknoten, + rollenart: rolle.rollenart, + merkmale: rolle.merkmale, + systemrechte: rolle.systemrechte, + }; + } +} diff --git a/src/modules/personenkontext/domain/dbiam-personenkontext.factory.ts b/src/modules/personenkontext/domain/dbiam-personenkontext.factory.ts index cc923f929..f8c17415e 100644 --- a/src/modules/personenkontext/domain/dbiam-personenkontext.factory.ts +++ b/src/modules/personenkontext/domain/dbiam-personenkontext.factory.ts @@ -3,15 +3,15 @@ import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.r import { PersonenkontexteUpdate } from './personenkontexte-update.js'; import { DbiamPersonenkontextBodyParams } from '../api/param/dbiam-personenkontext.body.params.js'; import { PersonID } from '../../../shared/types/index.js'; -import { ClassLogger } from '../../../core/logging/class-logger.js'; +import { PersonenkontextFactory } from './personenkontext.factory.js'; import { EventService } from '../../../core/eventbus/index.js'; @Injectable() export class DbiamPersonenkontextFactory { public constructor( + private personenkontextFactory: PersonenkontextFactory, private readonly eventService: EventService, private readonly dBiamPersonenkontextRepo: DBiamPersonenkontextRepo, - private readonly logger: ClassLogger, ) {} public createNewPersonenkontexteUpdate( @@ -22,8 +22,8 @@ export class DbiamPersonenkontextFactory { ): PersonenkontexteUpdate { return PersonenkontexteUpdate.createNew( this.eventService, - this.logger, this.dBiamPersonenkontextRepo, + this.personenkontextFactory, personId, lastModified, count, diff --git a/src/modules/personenkontext/domain/error/personenkontext-commit.error.spec.ts b/src/modules/personenkontext/domain/error/personenkontext-commit.error.spec.ts new file mode 100644 index 000000000..3792a91a0 --- /dev/null +++ b/src/modules/personenkontext/domain/error/personenkontext-commit.error.spec.ts @@ -0,0 +1,26 @@ +import { PersonenkontextCommitError } from './personenkontext-commit.error.js'; + +describe('PersonenkontextCommitError', () => { + it('should create an error with the correct message and code', () => { + const message: string = 'An error occurred'; + const error: PersonenkontextCommitError = new PersonenkontextCommitError(message); + + expect(error).toBeInstanceOf(PersonenkontextCommitError); + expect(error.message).toBe(message); + expect(error.code).toBe('PERSONENKONTEXT_COULD_NOT_BE_COMMITED'); + expect(error.details).toBeUndefined(); + }); + + it('should create an error with details as an array', () => { + const message: string = 'An error occurred'; + const details: { + key: string; + }[] = [{ key: 'value' }]; + const error: PersonenkontextCommitError = new PersonenkontextCommitError(message, details); + + expect(error).toBeInstanceOf(PersonenkontextCommitError); + expect(error.message).toBe(message); + expect(error.code).toBe('PERSONENKONTEXT_COULD_NOT_BE_COMMITED'); + expect(error.details).toEqual(details); + }); +}); diff --git a/src/modules/personenkontext/domain/error/personenkontext-commit.error.ts b/src/modules/personenkontext/domain/error/personenkontext-commit.error.ts new file mode 100644 index 000000000..5268d226c --- /dev/null +++ b/src/modules/personenkontext/domain/error/personenkontext-commit.error.ts @@ -0,0 +1,10 @@ +import { DomainError } from '../../../../shared/error/index.js'; + +export class PersonenkontextCommitError extends DomainError { + public constructor( + public override readonly message: string, + details?: unknown[] | Record, + ) { + super(message, 'PERSONENKONTEXT_COULD_NOT_BE_COMMITED', details); + } +} diff --git a/src/modules/personenkontext/domain/error/personenkontexte-update.error.spec.ts b/src/modules/personenkontext/domain/error/personenkontexte-update.error.spec.ts new file mode 100644 index 000000000..afd665cbd --- /dev/null +++ b/src/modules/personenkontext/domain/error/personenkontexte-update.error.spec.ts @@ -0,0 +1,21 @@ +import { PersonenkontexteUpdateError } from './personenkontexte-update.error.js'; +import { DomainError } from '../../../../shared/error/index.js'; + +describe('PersonenkontexteUpdateError', () => { + it('should create an instance of PersonenkontexteUpdateError with message and details', () => { + const message: string = 'An error occurred while updating Personenkontexte.'; + const error: PersonenkontexteUpdateError = new PersonenkontexteUpdateError(message); + + expect(error).toBeInstanceOf(PersonenkontexteUpdateError); + expect(error).toBeInstanceOf(DomainError); + }); + + it('should create an instance of PersonenkontexteUpdateError with message only', () => { + const message: string = 'An error occurred while updating Personenkontexte.'; + + const error: PersonenkontexteUpdateError = new PersonenkontexteUpdateError(message); + + expect(error).toBeInstanceOf(PersonenkontexteUpdateError); + expect(error).toBeInstanceOf(DomainError); + }); +}); diff --git a/src/modules/personenkontext/domain/error/update-not-found.error.spec.ts b/src/modules/personenkontext/domain/error/update-not-found.error.spec.ts new file mode 100644 index 000000000..c33056bf0 --- /dev/null +++ b/src/modules/personenkontext/domain/error/update-not-found.error.spec.ts @@ -0,0 +1,36 @@ +import { UpdateNotFoundError } from './update-not-found.error.js'; +import { PersonID, OrganisationID, RolleID } from '../../../../shared/types/index.js'; +import { PersonenkontexteUpdateError } from './personenkontexte-update.error.js'; + +describe('UpdateNotFoundError', () => { + it('should create an instance of UpdateNotFoundError with personId, organisationId, rolleId and details', () => { + const personId: PersonID = '123'; + const organisationId: OrganisationID = '456'; + const rolleId: RolleID = '789'; + + const error: UpdateNotFoundError = new UpdateNotFoundError(personId, organisationId, rolleId); + + expect(error).toBeInstanceOf(UpdateNotFoundError); + expect(error).toBeInstanceOf(PersonenkontexteUpdateError); + expect(error.message).toBe( + `Personenkontexte could not be updated because Personenkontext personId:${personId}, organisationId:${organisationId}, rolleId:${rolleId} was not found.`, + ); + expect(error.code).toBe('ENTITIES_COULD_NOT_BE_UPDATED'); + }); + + it('should create an instance of UpdateNotFoundError with personId, organisationId, rolleId and no details', () => { + const personId: PersonID = '123'; + const organisationId: OrganisationID = '456'; + const rolleId: RolleID = '789'; + + const error: UpdateNotFoundError = new UpdateNotFoundError(personId, organisationId, rolleId); + + expect(error).toBeInstanceOf(UpdateNotFoundError); + expect(error).toBeInstanceOf(PersonenkontexteUpdateError); + expect(error.message).toBe( + `Personenkontexte could not be updated because Personenkontext personId:${personId}, organisationId:${organisationId}, rolleId:${rolleId} was not found.`, + ); + expect(error.details).toBeUndefined(); + expect(error.code).toBe('ENTITIES_COULD_NOT_BE_UPDATED'); + }); +}); diff --git a/src/modules/personenkontext/domain/personenkontext-anlage.factory.ts b/src/modules/personenkontext/domain/personenkontext-anlage.factory.ts deleted file mode 100644 index bff7de45d..000000000 --- a/src/modules/personenkontext/domain/personenkontext-anlage.factory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; -import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; -import { PersonenkontextAnlage } from './personenkontext-anlage.js'; - -@Injectable() -export class PersonenkontextAnlageFactory { - public constructor( - private readonly rolleRepo: RolleRepo, - private readonly organisationRepo: OrganisationRepo, - ) {} - - public createNew(): PersonenkontextAnlage { - return PersonenkontextAnlage.createNew(this.rolleRepo, this.organisationRepo); - } -} diff --git a/src/modules/personenkontext/domain/personenkontext-anlage.spec.ts b/src/modules/personenkontext/domain/personenkontext-anlage.spec.ts deleted file mode 100644 index 1c65e6d06..000000000 --- a/src/modules/personenkontext/domain/personenkontext-anlage.spec.ts +++ /dev/null @@ -1,623 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { PersonenkontextAnlage } from './personenkontext-anlage.js'; -import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; -import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; -import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; -import { DoFactory } from '../../../../test/utils/index.js'; -import { Personenkontext } from './personenkontext.js'; -import { Rolle } from '../../rolle/domain/rolle.js'; -import { faker } from '@faker-js/faker'; -import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.repo.js'; -import { PersonenkontextAnlageFactory } from './personenkontext-anlage.factory.js'; -import { RollenArt } from '../../rolle/domain/rolle.enums.js'; -import { OrganisationsTyp } from '../../organisation/domain/organisation.enums.js'; -import { PersonenkontextFactory } from './personenkontext.factory.js'; -import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; -import { PersonRepository } from '../../person/persistence/person.repository.js'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; - -function createPersonenkontext( - this: void, - factory: PersonenkontextFactory, - withId: WasPersisted, - params: Partial> = {}, -): Personenkontext { - const personenkontext: Personenkontext = factory.construct( - withId ? faker.string.uuid() : undefined, - withId ? faker.date.past() : undefined, - withId ? faker.date.recent() : undefined, - faker.string.uuid(), - faker.string.uuid(), - faker.string.uuid(), - ); - - Object.assign(personenkontext, params); - - return personenkontext; -} - -function createRolleOrganisationsPersonKontext( - factory: PersonenkontextFactory, -): [Rolle, OrganisationDo, OrganisationDo, OrganisationDo, Personenkontext] { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); - const parentOrganisation: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.TRAEGER, - }); - const childOrganisation: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.SCHULE, - }); - const childsChildOrganisation: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.KLASSE, - }); - childsChildOrganisation.administriertVon = childOrganisation.id; - childOrganisation.administriertVon = parentOrganisation.id; - const personenkontext: Personenkontext = createPersonenkontext(factory, true, { - rolleId: rolle.id, - organisationId: parentOrganisation.id, - }); - return [rolle, parentOrganisation, childOrganisation, childsChildOrganisation, personenkontext]; -} - -describe('PersonenkontextAnlage', () => { - const LIMIT: number = 25; - let module: TestingModule; - let rolleRepoMock: DeepMocked; - let organisationRepoMock: DeepMocked; - let dBiamPersonenkontextRepoMock: DeepMocked; - let personPermissionsMock: DeepMocked; - let anlage: PersonenkontextAnlage; - let personenkontextAnlageFactory: PersonenkontextAnlageFactory; - let personenkontextFactory: PersonenkontextFactory; - let personpermissionsMock: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - PersonenkontextAnlageFactory, - PersonenkontextFactory, - { - provide: RolleRepo, - useValue: createMock(), - }, - { - provide: OrganisationRepo, - useValue: createMock(), - }, - { - provide: OrganisationRepository, - useValue: createMock(), - }, - { - provide: PersonRepository, - useValue: createMock(), - }, - { - provide: DBiamPersonenkontextRepo, - useValue: createMock(), - }, - { - provide: PersonPermissions, - useValue: createMock(), - }, - ], - }).compile(); - rolleRepoMock = module.get(RolleRepo); - organisationRepoMock = module.get(OrganisationRepo); - dBiamPersonenkontextRepoMock = module.get(DBiamPersonenkontextRepo); - personPermissionsMock = createMock(); - personenkontextFactory = module.get(PersonenkontextFactory); - personenkontextAnlageFactory = module.get(PersonenkontextAnlageFactory); - personenkontextFactory = module.get(PersonenkontextFactory); - anlage = personenkontextAnlageFactory.createNew(); - personpermissionsMock = module.get(PersonPermissions); - }); - - afterAll(async () => { - await module.close(); - }); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should be defined', () => { - expect(anlage).toBeDefined(); - }); - - describe('findSchulstrukturknoten', () => { - it('should return list of schulstrukturknoten when parent-organisation is matching', async () => { - const [rolle, parentOrganisation, , , personenkontext]: [ - Rolle, - OrganisationDo, - OrganisationDo, - OrganisationDo, - Personenkontext, - ] = createRolleOrganisationsPersonKontext(personenkontextFactory); - const organisationen: OrganisationDo[] = [parentOrganisation]; - const personenkontexte: Personenkontext[] = [personenkontext]; - - organisationRepoMock.findByNameOrKennung.mockResolvedValue(organisationen); - dBiamPersonenkontextRepoMock.findByRolle.mockResolvedValue(personenkontexte); - - organisationRepoMock.findById.mockResolvedValue(parentOrganisation); - - const counted2: Counted> = [[], 1]; - organisationRepoMock.findBy.mockResolvedValueOnce(counted2); //mock call in findChildOrganisations, 2nd time (recursive) - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - parentOrganisation.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - }); - - describe('matching of parent or child SSK', () => { - it('should return list of schulstrukturknoten when child-organisation is matching', async () => { - const [rolle, parent, child, subchild]: [ - Rolle, - OrganisationDo, - OrganisationDo, - OrganisationDo, - Personenkontext, - ] = createRolleOrganisationsPersonKontext(personenkontextFactory); - - const foundByName: OrganisationDo[] = [child]; - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([child.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue(foundByName); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(parent); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([parent, child, subchild]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - child.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - }); - - it('should return list of schulstrukturknoten when child of child-organisation is matching with one results', async () => { - const [rolle, parent, child, childOfChild]: [ - Rolle, - OrganisationDo, - OrganisationDo, - OrganisationDo, - Personenkontext, - ] = createRolleOrganisationsPersonKontext(personenkontextFactory); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([child.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([child, childOfChild]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(parent); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([parent, child, childOfChild]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - child.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - }); - - it('should return list of schulstrukturknoten when a valid child with name exist', async () => { - const [rolle, parent, child]: [ - Rolle, - OrganisationDo, - OrganisationDo, - OrganisationDo, - Personenkontext, - ] = createRolleOrganisationsPersonKontext(personenkontextFactory); - - const foundByName: OrganisationDo[] = [child]; - const personenkontext: Personenkontext = createPersonenkontext(personenkontextFactory, true, { - rolleId: rolle.id, - organisationId: parent.id, - }); - const personenkontexte: Personenkontext[] = [personenkontext]; - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([child.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue(foundByName); - dBiamPersonenkontextRepoMock.findByRolle.mockResolvedValue(personenkontexte); - organisationRepoMock.findById.mockResolvedValue(undefined); //mock call to find parent in findSchulstrukturknoten - - const counted: Counted> = [foundByName, 1]; - organisationRepoMock.findBy.mockResolvedValue(counted); //mock call in findChildOrganisations - - await expect( - anlage.findSchulstrukturknoten(personPermissionsMock, rolle.id, child.name!, LIMIT), - ).resolves.not.toThrow(Error); - }); - }); - - it('should return empty list when no rolle could be found', async () => { - const rolle: Rolle = DoFactory.createRolle(true); - const organisation: OrganisationDo = DoFactory.createOrganisation(true); - const organisationen: OrganisationDo[] = [organisation]; - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisation.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue(organisationen); - rolleRepoMock.findById.mockResolvedValue(undefined); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - 'nonexistent', - LIMIT, - ); - - expect(result).toHaveLength(0); - }); - - it('should return empty list when no parent organisation could be found', async () => { - const rolle: Rolle = DoFactory.createRolle(true); - const organisation: OrganisationDo = DoFactory.createOrganisation(true); - const organisationen: OrganisationDo[] = [organisation]; - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisation.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue(organisationen); - rolleRepoMock.findById.mockResolvedValue(rolle); - organisationRepoMock.findById.mockResolvedValue(undefined); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - 'nonexistent', - LIMIT, - ); - - expect(result).toHaveLength(0); - }); - - it('should return empty list when no ssks could be found', async () => { - const rolle: Rolle = DoFactory.createRolle(true); - const personenkontext: Personenkontext = createPersonenkontext(personenkontextFactory, true); - const personenkontexte: Personenkontext[] = [personenkontext]; - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([faker.string.uuid()]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([]); - dBiamPersonenkontextRepoMock.findByRolle.mockResolvedValue(personenkontexte); - - const counted: Counted> = [[], 0]; - organisationRepoMock.findBy.mockResolvedValueOnce(counted); //mock call in findChildOrganisations, 2nd time (recursive) - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - 'nonexistent', - LIMIT, - ); - expect(result).toHaveLength(0); - }); - - describe('filter organisations by RollenArt', () => { - it('should return empty list, because orga as SCHULE does not match RollenArt SYSADMIN', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.SCHULE, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(0); - }); - - it('should return one element, because orga as LAND does match RollenArt SYSADMIN', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.LAND, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - expect(result).toContainEqual(organisationDo); - }); - - it('should return one element, because orga as ROOT does match RollenArt SYSADMIN', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.ROOT, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - expect(result).toContainEqual(organisationDo); - }); - - it('should return empty list, because orga as LAND does not match RollenArt LEIT', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.LAND, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(0); - }); - - it('should return one element, because orga as SCHULE does match RollenArt LEIT', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.SCHULE, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - expect(result).toContainEqual(organisationDo); - }); - - it('should return one element, because orga as SCHULE does match RollenArt LERN', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.SCHULE, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - expect(result).toContainEqual(organisationDo); - }); - - it('should return one element, because orga as KLASSE does match RollenArt LERN', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.KLASSE, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - expect(result).toContainEqual(organisationDo); - }); - - it('should return empty list, because orga as LAND does not match RollenArt LERN', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.LAND, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(0); - }); - - it('should return one element, because orga as SCHULE does match RollenArt LEHR', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.SCHULE, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(1); - expect(result).toContainEqual(organisationDo); - }); - - it('should return no element, because orga as KLASSE does not match RollenArt LEHR', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.KLASSE, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(0); - }); - - it('should return empty list, because orga as LAND does not match RollenArt LEHR', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.LAND, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - ); - expect(result).toHaveLength(0); - }); - - it('should not return klassen when excluded', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.KLASSE, - }); - - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue([organisationDo.id]); - organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); - rolleRepoMock.findById.mockResolvedValueOnce(rolle); - organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten - organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); - - const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( - personPermissionsMock, - rolle.id, - organisationDo.name!, - LIMIT, - true, - ); - expect(result).toHaveLength(0); - }); - }); - }); - - describe('findAuthorizedRollen', () => { - it('should return list of all rollen when they exist, if the user is Landesadmin', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); - const leitRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); - const lehrRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); - const lernRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); - - const rollen: Rolle[] = [rolle, leitRolle, lehrRolle, lernRolle]; - rolleRepoMock.find.mockResolvedValue(rollen); - - personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([ - organisationRepoMock.ROOT_ORGANISATION_ID, - ]); - - const result: Rolle[] = await anlage.findAuthorizedRollen(personpermissionsMock); - expect(result).toEqual(rollen); - }); - - it('should return list of all rollen when they exist Except Landesadmin, if the user is NOT Landesadmin', async () => { - const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); - const leitRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); - const lehrRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); - const lernRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); - - const rollen: Rolle[] = [rolle, leitRolle, lehrRolle, lernRolle]; - rolleRepoMock.find.mockResolvedValue(rollen); - - const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { - typ: OrganisationsTyp.SCHULE, - }); - const organisationMap: Map> = new Map(); - organisationMap.set(organisationDo.id, organisationDo); - organisationRepoMock.findByIds.mockResolvedValueOnce(organisationMap); - - personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationDo.id]); - - const result: Rolle[] = await anlage.findAuthorizedRollen(personpermissionsMock); - expect(result).not.toContain(rolle); - }); - - it('should return list of rollen when they exist', async () => { - const rolle: Rolle = DoFactory.createRolle(true); - rolleRepoMock.findByName.mockResolvedValue([rolle]); - - personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([ - organisationRepoMock.ROOT_ORGANISATION_ID, - ]); - - const result: Rolle[] = await anlage.findAuthorizedRollen(personpermissionsMock, rolle.name, LIMIT); - expect(result).toEqual([rolle]); - }); - - it('should return empty list when no rollen exist', async () => { - rolleRepoMock.findByName.mockResolvedValue(undefined); - - const result: Rolle[] = await anlage.findAuthorizedRollen( - personpermissionsMock, - 'nonexistent', - LIMIT, - ); - expect(result).toEqual([]); - }); - }); -}); diff --git a/src/modules/personenkontext/domain/personenkontext-anlage.ts b/src/modules/personenkontext/domain/personenkontext-anlage.ts deleted file mode 100644 index fb48529d0..000000000 --- a/src/modules/personenkontext/domain/personenkontext-anlage.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; -import { Rolle } from '../../rolle/domain/rolle.js'; -import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; -import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; -import { OrganisationsTyp } from '../../organisation/domain/organisation.enums.js'; -import { OrganisationMatchesRollenart } from '../specification/organisation-matches-rollenart.js'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; -import { OrganisationID } from '../../../shared/types/aggregate-ids.types.js'; -import { RollenSystemRecht } from '../../rolle/domain/rolle.enums.js'; - -export class PersonenkontextAnlage { - private constructor( - private readonly rolleRepo: RolleRepo, - private readonly organisationRepo: OrganisationRepo, - ) {} - - public static createNew(rolleRepo: RolleRepo, organisationRepo: OrganisationRepo): PersonenkontextAnlage { - return new PersonenkontextAnlage(rolleRepo, organisationRepo); - } - - public async findSchulstrukturknoten( - personPermissions: PersonPermissions, - rolleId: string, - sskName: string, - limit?: number, - excludeKlassen: boolean = false, - ): Promise[]> { - const orgsWithRecht: string[] = await personPermissions.getOrgIdsWithSystemrecht([ - RollenSystemRecht.PERSONEN_VERWALTEN, - ]); - - let ssks: Option[]> = await this.organisationRepo.findByNameOrKennung(sskName); - - //Landesadmin can view all roles, if not orgsWithRecht includes ROOT, filter the ssks - if (!orgsWithRecht.includes(this.organisationRepo.ROOT_ORGANISATION_ID)) { - ssks = ssks.filter((ssk: OrganisationDo) => orgsWithRecht.some((orgId: string) => ssk.id === orgId)); - } - - if (ssks.length === 0) return []; - - const rolleResult: Option> = await this.rolleRepo.findById(rolleId); - if (!rolleResult) return []; - - const allOrganisations: OrganisationDo[] = []; - - const parentOrganisation: Option> = await this.organisationRepo.findById( - rolleResult.administeredBySchulstrukturknoten, - ); - if (!parentOrganisation) return []; - allOrganisations.push(parentOrganisation); - - const childOrganisations: OrganisationDo[] = await this.organisationRepo.findChildOrgasForIds([ - rolleResult.administeredBySchulstrukturknoten, - ]); - allOrganisations.push(...childOrganisations); - - let orgas: OrganisationDo[] = ssks.filter((ssk: OrganisationDo) => - allOrganisations.some((organisation: OrganisationDo) => ssk.id === organisation.id), - ); - - if (excludeKlassen) { - orgas = orgas.filter((ssk: OrganisationDo) => ssk.typ !== OrganisationsTyp.KLASSE); - } - - const organisationMatchesRollenart: OrganisationMatchesRollenart = new OrganisationMatchesRollenart(); - orgas = orgas.filter((orga: OrganisationDo) => - organisationMatchesRollenart.isSatisfiedBy(orga, rolleResult), - ); - - return orgas.slice(0, limit); - } - - public async findAuthorizedRollen( - permissions: PersonPermissions, - rolleName?: string, - limit?: number, - ): Promise[]> { - let rollen: Option[]>; - - if (rolleName) { - rollen = await this.rolleRepo.findByName(rolleName, limit); - } else { - rollen = await this.rolleRepo.find(limit); - } - - if (!rollen) return []; - - const orgsWithRecht: OrganisationID[] = await permissions.getOrgIdsWithSystemrecht( - [RollenSystemRecht.PERSONEN_VERWALTEN], - true, - ); - - //Landesadmin can view all roles. - if (orgsWithRecht.includes(this.organisationRepo.ROOT_ORGANISATION_ID)) return rollen; - - const allowedRollen: Rolle[] = []; - const organisationMatchesRollenart: OrganisationMatchesRollenart = new OrganisationMatchesRollenart(); - (await this.organisationRepo.findByIds(orgsWithRecht)).forEach(function (orga: OrganisationDo) { - rollen.forEach(function (rolle: Rolle) { - if (organisationMatchesRollenart.isSatisfiedBy(orga, rolle) && !allowedRollen.includes(rolle)) { - allowedRollen.push(rolle); - } - }); - }); - - return allowedRollen; - } -} diff --git a/src/modules/personenkontext/domain/personenkontext-creation.service.spec.ts b/src/modules/personenkontext/domain/personenkontext-creation.service.spec.ts new file mode 100644 index 000000000..b4115b2ba --- /dev/null +++ b/src/modules/personenkontext/domain/personenkontext-creation.service.spec.ts @@ -0,0 +1,233 @@ +import { Mapper } from '@automapper/core'; +import { getMapperToken } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + DomainError, + EntityNotFoundError, + InvalidAttributeLengthError, + KeycloakClientError, + MissingPermissionsError, +} from '../../../shared/error/index.js'; +import { DoFactory } from '../../../../test/utils/do-factory.js'; +import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; +import { faker } from '@faker-js/faker'; +import { Organisation } from '../../organisation/domain/organisation.js'; +import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.repo.js'; +import { PersonenkontextFactory } from './personenkontext.factory.js'; +import { Rolle } from '../../rolle/domain/rolle.js'; +import { RollenArt } from '../../rolle/domain/rolle.enums.js'; +import { OrganisationsTyp } from '../../organisation/domain/organisation.enums.js'; +import { PersonenkontextWorkflowFactory } from './personenkontext-workflow.factory.js'; +import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; +import { DbiamPersonenkontextFactory } from './dbiam-personenkontext.factory.js'; +import { RolleNurAnPassendeOrganisationError } from '../specification/error/rolle-nur-an-passende-organisation.js'; +import { PersonenkontextCreationService, PersonPersonenkontext } from './personenkontext-creation.service.js'; +import { PersonRepository } from '../../person/persistence/person.repository.js'; +import { PersonFactory } from '../../person/domain/person.factory.js'; +import { Person } from '../../person/domain/person.js'; + +describe('PersonenkontextCreationService', () => { + let module: TestingModule; + let sut: PersonenkontextCreationService; + let rolleRepoMock: DeepMocked; + let organisationRepositoryMock: DeepMocked; + let personRepositoryMock: DeepMocked; + let personpermissionsMock: DeepMocked; + let personFactoryMock: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + PersonenkontextCreationService, + PersonenkontextWorkflowFactory, + { + provide: getMapperToken(), + useValue: createMock(), + }, + { + provide: RolleRepo, + useValue: createMock(), + }, + { + provide: OrganisationRepository, + useValue: createMock(), + }, + { + provide: PersonRepository, + useValue: createMock(), + }, + { + provide: DBiamPersonenkontextRepo, + useValue: createMock(), + }, + { + provide: PersonFactory, + useValue: createMock(), + }, + { + provide: PersonenkontextFactory, + useValue: createMock(), + }, + { + provide: PersonPermissions, + useValue: createMock(), + }, + { + provide: OrganisationRepo, + useValue: createMock(), + }, + { + provide: DbiamPersonenkontextFactory, + useValue: createMock(), + }, + ], + }).compile(); + sut = module.get(PersonenkontextCreationService); + rolleRepoMock = module.get(RolleRepo); + organisationRepositoryMock = module.get(OrganisationRepository); + personRepositoryMock = module.get(PersonRepository); + personpermissionsMock = module.get(PersonPermissions); + personFactoryMock = module.get(PersonFactory); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('createPersonWithPersonenkontext', () => { + it('should return DomainError if Person Aggregate ist invalid ', async () => { + personFactoryMock.createNew.mockResolvedValueOnce(new InvalidAttributeLengthError('name.vorname')); + rolleRepoMock.findById.mockResolvedValueOnce(DoFactory.createRolle(true)); + organisationRepositoryMock.findById.mockResolvedValueOnce(createMock>()); + + const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( + personpermissionsMock, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + expect(result).toBeInstanceOf(DomainError); + }); + + it('should return EntityNotFoundError if Organisation is not found', async () => { + personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); + rolleRepoMock.findById.mockResolvedValueOnce(DoFactory.createRolle(true)); + organisationRepositoryMock.findById.mockResolvedValueOnce(undefined); + + const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( + personpermissionsMock, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + expect(result).toBeInstanceOf(EntityNotFoundError); + }); + + it('should return EntityNotFoundError if Rolle is not found', async () => { + personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); + rolleRepoMock.findById.mockResolvedValueOnce(undefined); + organisationRepositoryMock.findById.mockResolvedValueOnce(createMock>()); + + const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( + personpermissionsMock, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + expect(result).toBeInstanceOf(EntityNotFoundError); + }); + + it('should return EntityNotFoundError if Rolle can NOT be assigned to organisation', async () => { + personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); + const rolleMock: DeepMocked> = createMock>(); + rolleMock.canBeAssignedToOrga.mockResolvedValueOnce(false); + rolleRepoMock.findById.mockResolvedValueOnce(rolleMock); + organisationRepositoryMock.findById.mockResolvedValueOnce(createMock>()); + + const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( + personpermissionsMock, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + expect(result).toBeInstanceOf(EntityNotFoundError); + }); + + it('should return RolleNurAnPassendeOrganisationError if Rolle does NOT match organisation', async () => { + personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); + const rolleMock: DeepMocked> = createMock>({ rollenart: RollenArt.SYSADMIN }); + rolleMock.canBeAssignedToOrga.mockResolvedValueOnce(true); + rolleRepoMock.findById.mockResolvedValueOnce(rolleMock); + organisationRepositoryMock.findById.mockResolvedValueOnce( + createMock>({ typ: OrganisationsTyp.SCHULE }), + ); + + const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( + personpermissionsMock, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + expect(result).toBeInstanceOf(RolleNurAnPassendeOrganisationError); + }); + + it('should return MissingPermissionsError if user does NOT have permissions', async () => { + personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); + const rolleMock: DeepMocked> = createMock>({ rollenart: RollenArt.SYSADMIN }); + rolleMock.canBeAssignedToOrga.mockResolvedValueOnce(true); + rolleRepoMock.findById.mockResolvedValueOnce(rolleMock); + organisationRepositoryMock.findById.mockResolvedValueOnce( + createMock>({ typ: OrganisationsTyp.LAND }), + ); + personpermissionsMock.hasSystemrechtAtOrganisation.mockResolvedValueOnce(false); + + const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( + personpermissionsMock, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + expect(result).toBeInstanceOf(MissingPermissionsError); + }); + + it('should return DomainError if Person cannot be saved in the DB', async () => { + personFactoryMock.createNew.mockResolvedValueOnce(createMock>()); + const rolleMock: DeepMocked> = createMock>({ rollenart: RollenArt.SYSADMIN }); + rolleMock.canBeAssignedToOrga.mockResolvedValueOnce(true); + rolleRepoMock.findById.mockResolvedValueOnce(rolleMock); + organisationRepositoryMock.findById.mockResolvedValueOnce( + createMock>({ typ: OrganisationsTyp.LAND }), + ); + personpermissionsMock.hasSystemrechtAtOrganisation.mockResolvedValueOnce(true); + personRepositoryMock.create.mockResolvedValueOnce( + new KeycloakClientError('Username or email already exists'), + ); + + const result: PersonPersonenkontext | DomainError = await sut.createPersonWithPersonenkontext( + personpermissionsMock, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + expect(result).toBeInstanceOf(DomainError); + }); + }); +}); diff --git a/src/modules/personenkontext/domain/personenkontext-creation.service.ts b/src/modules/personenkontext/domain/personenkontext-creation.service.ts new file mode 100644 index 000000000..2d8a722fb --- /dev/null +++ b/src/modules/personenkontext/domain/personenkontext-creation.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@nestjs/common'; +import { DomainError } from '../../../shared/error/index.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.repo.js'; +import { PersonenkontextFactory } from './personenkontext.factory.js'; +import { Personenkontext } from './personenkontext.js'; +import { PersonenkontextWorkflowFactory } from './personenkontext-workflow.factory.js'; +import { PersonenkontextWorkflowAggregate } from './personenkontext-workflow.js'; +import { Person } from '../../person/domain/person.js'; +import { PersonRepository } from '../../person/persistence/person.repository.js'; +import { PersonFactory } from '../../person/domain/person.factory.js'; + +export type PersonPersonenkontext = { + person: Person; + personenkontext: Personenkontext; +}; + +@Injectable() +export class PersonenkontextCreationService { + public constructor( + private readonly personRepository: PersonRepository, + private readonly personenkontextRepo: DBiamPersonenkontextRepo, + private readonly personFactory: PersonFactory, + private readonly personenkontextFactory: PersonenkontextFactory, + private readonly personenkontextWorkflowFactory: PersonenkontextWorkflowFactory, + ) {} + + public async createPersonWithPersonenkontext( + permissions: PersonPermissions, + vorname: string, + familienname: string, + organisationId: string, + rolleId: string, + ): Promise { + const person: Person | DomainError = await this.personFactory.createNew({ + vorname: vorname, + familienname: familienname, + }); + if (person instanceof DomainError) { + return person; + } + const anlage: PersonenkontextWorkflowAggregate = this.personenkontextWorkflowFactory.createNew(); + + anlage.initialize(organisationId, rolleId); + + // Check if permissions are enough to create the kontext + const canCommit: DomainError | boolean = await anlage.canCommit(permissions); + + if (canCommit instanceof DomainError) { + return canCommit; + } + //Save Person + const savedPerson: DomainError | Person = await this.personRepository.create(person); + if (savedPerson instanceof DomainError) { + return savedPerson; + } + + const personenkontext: Personenkontext = this.personenkontextFactory.createNew( + savedPerson.id, + organisationId, + rolleId, + ); + //Save Personenkontext + const savedPersonenkontext: Personenkontext = await this.personenkontextRepo.save(personenkontext); + return { + person: savedPerson, + personenkontext: savedPersonenkontext, + }; + } +} diff --git a/src/modules/personenkontext/domain/personenkontext-workflow.factory.ts b/src/modules/personenkontext/domain/personenkontext-workflow.factory.ts new file mode 100644 index 000000000..fc365297b --- /dev/null +++ b/src/modules/personenkontext/domain/personenkontext-workflow.factory.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; +import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; +import { PersonenkontextWorkflowAggregate } from './personenkontext-workflow.js'; +import { DbiamPersonenkontextFactory } from './dbiam-personenkontext.factory.js'; +import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; + +@Injectable() +export class PersonenkontextWorkflowFactory { + public constructor( + private readonly rolleRepo: RolleRepo, + private readonly organisationRepo: OrganisationRepo, + private readonly organisationRepository: OrganisationRepository, + private readonly dbiamPersonenkontextFactory: DbiamPersonenkontextFactory, + ) {} + + public createNew(): PersonenkontextWorkflowAggregate { + return PersonenkontextWorkflowAggregate.createNew( + this.rolleRepo, + this.organisationRepo, + this.organisationRepository, + this.dbiamPersonenkontextFactory, + ); + } +} diff --git a/src/modules/personenkontext/domain/personenkontext-workflow.spec.ts b/src/modules/personenkontext/domain/personenkontext-workflow.spec.ts new file mode 100644 index 000000000..cce6e947e --- /dev/null +++ b/src/modules/personenkontext/domain/personenkontext-workflow.spec.ts @@ -0,0 +1,1183 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { PersonenkontextWorkflowAggregate } from './personenkontext-workflow.js'; +import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; +import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; +import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; +import { DoFactory } from '../../../../test/utils/index.js'; +import { Personenkontext } from './personenkontext.js'; +import { Rolle } from '../../rolle/domain/rolle.js'; +import { faker } from '@faker-js/faker'; +import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.repo.js'; +import { PersonenkontextWorkflowFactory } from './personenkontext-workflow.factory.js'; +import { RollenArt } from '../../rolle/domain/rolle.enums.js'; +import { OrganisationsTyp } from '../../organisation/domain/organisation.enums.js'; +import { PersonenkontextFactory } from './personenkontext.factory.js'; +import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; +import { PersonRepository } from '../../person/persistence/person.repository.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { DbiamPersonenkontextFactory } from './dbiam-personenkontext.factory.js'; +import { DbiamPersonenkontextBodyParams } from '../api/param/dbiam-personenkontext.body.params.js'; +import { PersonenkontexteUpdateError } from './error/personenkontexte-update.error.js'; + +function createPersonenkontext( + this: void, + factory: PersonenkontextFactory, + withId: WasPersisted, + params: Partial> = {}, +): Personenkontext { + const personenkontext: Personenkontext = factory.construct( + withId ? faker.string.uuid() : undefined, + withId ? faker.date.past() : undefined, + withId ? faker.date.recent() : undefined, + faker.string.uuid(), + faker.string.uuid(), + faker.string.uuid(), + ); + + Object.assign(personenkontext, params); + + return personenkontext; +} + +function createRolleOrganisationsPersonKontext( + factory: PersonenkontextFactory, + anlage: PersonenkontextWorkflowAggregate, +): [Rolle, OrganisationDo, OrganisationDo, OrganisationDo, Personenkontext] { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); + const parentOrganisation: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.TRAEGER, + }); + const childOrganisation: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.SCHULE, + }); + const childsChildOrganisation: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.KLASSE, + }); + childsChildOrganisation.administriertVon = childOrganisation.id; + childOrganisation.administriertVon = parentOrganisation.id; + anlage.selectedOrganisationId = childOrganisation.id; + anlage.selectedRolleId = rolle.id; + const personenkontext: Personenkontext = createPersonenkontext(factory, true, { + rolleId: rolle.id, + organisationId: parentOrganisation.id, + }); + return [rolle, parentOrganisation, childOrganisation, childsChildOrganisation, personenkontext]; +} + +describe('PersonenkontextWorkflow', () => { + const LIMIT: number = 25; + let module: TestingModule; + let rolleRepoMock: DeepMocked; + let organisationRepoMock: DeepMocked; + let dBiamPersonenkontextRepoMock: DeepMocked; + let anlage: PersonenkontextWorkflowAggregate; + let personenkontextAnlageFactory: PersonenkontextWorkflowFactory; + let personenkontextFactory: PersonenkontextFactory; + let personpermissionsMock: DeepMocked; + let dbiamPersonenkontextFactoryMock: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + PersonenkontextWorkflowFactory, + PersonenkontextFactory, + { + provide: RolleRepo, + useValue: createMock(), + }, + { + provide: OrganisationRepo, + useValue: createMock(), + }, + { + provide: OrganisationRepository, + useValue: createMock(), + }, + { + provide: PersonRepository, + useValue: createMock(), + }, + { + provide: DBiamPersonenkontextRepo, + useValue: createMock(), + }, + { + provide: PersonPermissions, + useValue: createMock(), + }, + { + provide: DbiamPersonenkontextFactory, + useValue: createMock(), + }, + ], + }).compile(); + rolleRepoMock = module.get(RolleRepo); + organisationRepoMock = module.get(OrganisationRepo); + dBiamPersonenkontextRepoMock = module.get(DBiamPersonenkontextRepo); + dbiamPersonenkontextFactoryMock = module.get(DbiamPersonenkontextFactory); + personenkontextFactory = module.get(PersonenkontextFactory); + personenkontextAnlageFactory = module.get(PersonenkontextWorkflowFactory); + personenkontextFactory = module.get(PersonenkontextFactory); + anlage = personenkontextAnlageFactory.createNew(); + personpermissionsMock = module.get(PersonPermissions); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(anlage).toBeDefined(); + }); + + describe('initialize', () => { + it('should initialize the aggregate with the selected Organisation and Rolle', () => { + anlage.initialize('org-id', 'role-id'); + expect(anlage.selectedOrganisationId).toBe('org-id'); + expect(anlage.selectedRolleId).toBe('role-id'); + }); + }); + + describe('findAllSchulstrukturknoten', () => { + it('should return only the organisations that the admin has rights on', async () => { + const organisation: OrganisationDo = DoFactory.createOrganisation(true); + const organisations: OrganisationDo[] = [organisation]; + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue(organisations); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisation.id]); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + LIMIT, + ); + expect(result.length).toBe(1); + }); + + it('should return organisations based on name or kennung if provided', async () => { + const organisation: OrganisationDo = DoFactory.createOrganisation(true); + const organisations: OrganisationDo[] = [organisation]; + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue(organisations); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisation.id]); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + organisation.name, + LIMIT, + ); + expect(result.length).toBe(1); + }); + + it('should return an empty array if no organisations are found', async () => { + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + LIMIT, + ); + expect(result.length).toBe(0); + }); + + it('should sort organisations by name and kennung', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + kennung: 'K1', + name: 'Beta School', + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { + kennung: 'K2', + name: 'Alpha School', + }); + const org3: OrganisationDo = DoFactory.createOrganisation(true, { name: 'Gamma School' }); + const org4: OrganisationDo = DoFactory.createOrganisation(true, { kennung: 'K3' }); + const orgsWithRecht: string[] = [org1.id, org2.id, org3.id, org4.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([ + org1, + org2, + org3, + org4, + ]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toBe(4); + }); + + it('should sort organisations with only kennung defined', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { kennung: 'K2' }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { kennung: 'K1' }); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toBe(2); + }); + + it('should handle organisations with undefined kennung and name', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { kennung: 'K1' }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { name: 'Alpha School' }); + const org3: OrganisationDo = DoFactory.createOrganisation(true, {}); + const orgsWithRecht: string[] = [org1.id, org2.id, org3.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2, org3]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toBe(3); + }); + + it('should handle organisations with kennung but undefined name', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { kennung: 'K1', name: 'tootie' }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { name: undefined }); + const org3: OrganisationDo = DoFactory.createOrganisation(true, {}); + const orgsWithRecht: string[] = [org1.id, org2.id, org3.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2, org3]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toBe(3); + }); + + it('should handle organisations with name but undefined kennung', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + kennung: undefined, + name: 'rolle', + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { name: 'tootie' }); + const org3: OrganisationDo = DoFactory.createOrganisation(true, {}); + const orgsWithRecht: string[] = [org1.id, org2.id, org3.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2, org3]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toBe(3); + }); + it('should sort organisations with neither kennung nor name defined', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, {}); + const org2: OrganisationDo = DoFactory.createOrganisation(true, {}); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toBe(2); + }); + + it('should handle mixed cases of kennung and name', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + kennung: 'K2', + name: 'Beta School', + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { name: 'Alpha School' }); + const org3: OrganisationDo = DoFactory.createOrganisation(true, { kennung: 'K1' }); + const org4: OrganisationDo = DoFactory.createOrganisation(true, {}); + const orgsWithRecht: string[] = [org1.id, org2.id, org3.id, org4.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([ + org1, + org2, + org3, + org4, + ]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toBe(4); + }); + + it('should sort organisations with only name defined', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { name: 'Beta School' }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { name: 'Alpha School' }); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toBe(2); + }); + it('should handle organisations with neither kennung nor name defined and return them as equal', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + name: undefined, + kennung: undefined, + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { + name: undefined, + kennung: undefined, + }); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toEqual(2); + }); + it('should handle organisations with kennung defined but name undefined', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + name: undefined, + kennung: '123', + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { + name: undefined, + kennung: '123', + }); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toEqual(2); + }); + it('should handle organisations with kennung defined but name undefined', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + name: undefined, + kennung: undefined, + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { + name: undefined, + kennung: '123', + }); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toEqual(2); + }); + it('should handle organisations with name and kennung', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + name: 'Carl-Orff', + kennung: '123', + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { + name: 'Amalie', + kennung: '321', + }); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toEqual(2); + }); + it('should handle organisations with name and kennung', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + name: 'Carl-Orff', + kennung: undefined, + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { + name: 'Amalie', + kennung: '321', + }); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toEqual(2); + }); + it('should handle organisations with name and kennung', async () => { + const org1: OrganisationDo = DoFactory.createOrganisation(true, { + name: 'Carl-Orff', + kennung: '321', + }); + const org2: OrganisationDo = DoFactory.createOrganisation(true, { + name: 'Amalie', + kennung: undefined, + }); + const orgsWithRecht: string[] = [org1.id, org2.id]; + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([org1, org2]); + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + const result: OrganisationDo[] = await anlage.findAllSchulstrukturknoten( + personpermissionsMock, + undefined, + 10, + ); + + expect(result.length).toEqual(2); + }); + }); + + describe('findRollenForOrganisation', () => { + it('should return an empty array if no roles are found by name', async () => { + rolleRepoMock.findByName.mockResolvedValue(undefined); + + const result: Rolle[] = await anlage.findRollenForOrganisation( + createMock(), + 'rolle-name', + 10, + ); + + expect(result).toEqual([]); + }); + + it('should return an empty array if no organisations with system rights are found', async () => { + rolleRepoMock.find.mockResolvedValue([createMock>()]); + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValue([]); + + anlage.initialize('organisation-id'); + + const result: Rolle[] = await anlage.findRollenForOrganisation(permissions); + + expect(result).toEqual([]); + }); + + it('should return an empty array if the organisation is not found', async () => { + const rolle: DeepMocked> = createMock>(); + rolleRepoMock.find.mockResolvedValue([createMock>()]); + rolleRepoMock.find.mockResolvedValue([rolle]); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValue(['org-id']); + + organisationRepoMock.findById.mockResolvedValue(undefined); + + anlage.initialize('org-id'); + + const result: Rolle[] = await anlage.findRollenForOrganisation(permissions); + + expect(result).toEqual([]); + }); + + it('should return an empty array if user does not have permission to view roles for the organisation', async () => { + const rolle: DeepMocked> = createMock>(); + const organisation: DeepMocked> = createMock>(); + rolleRepoMock.find.mockResolvedValue([rolle]); + organisationRepoMock.findById.mockResolvedValue(organisation); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValue(['some-other-org-id']); + + anlage.initialize('organisation-id'); + + const result: Rolle[] = await anlage.findRollenForOrganisation(permissions); + + expect(result).toEqual([]); + }); + it('should add roles to allowedRollen if user has permissions', async () => { + const organisation: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.LAND, + }); + const childOrganisation: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.KLASSE, + }); + const rolle: Rolle = DoFactory.createRolle(true, { + rollenart: RollenArt.ORGADMIN, + name: 'Alpha', + }); + const rolle1: Rolle = DoFactory.createRolle(true, { + rollenart: RollenArt.ORGADMIN, + name: 'Beta', + }); + const rollen: Rolle[] = [rolle, rolle1]; + const orgsWithRecht: string[] = [organisation.id, childOrganisation.id]; + + organisationRepoMock.findById.mockResolvedValue(organisation); + organisationRepoMock.findChildOrgasForIds.mockResolvedValue([childOrganisation]); + organisationRepoMock.findByIds.mockResolvedValue( + new Map(orgsWithRecht.map((id: string) => [id, DoFactory.createOrganisation(true, { id })])), + ); + rolleRepoMock.find.mockResolvedValue(rollen); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + anlage.initialize(organisation.id); + + const result: Rolle[] = await anlage.findRollenForOrganisation(permissions); + + expect(result).toHaveLength(2); + }); + it('should handle empty roles array', async () => { + rolleRepoMock.find.mockResolvedValue([]); + + const organisation: OrganisationDo = DoFactory.createOrganisation(true); + organisationRepoMock.findById.mockResolvedValue(organisation); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisation.id]); + + anlage.initialize(organisation.id); + + const result: Rolle[] = await anlage.findRollenForOrganisation(permissions); + + expect(result).toHaveLength(0); + }); + it('should limit roles returned allowedRollen if limit is set', async () => { + const organisation: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.LAND, + }); + const childOrganisation: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.KLASSE, + }); + const rolle1: Rolle = DoFactory.createRolle(true, { + rollenart: RollenArt.ORGADMIN, + name: 'rolle1', + }); + const rolle2: Rolle = DoFactory.createRolle(true, { + rollenart: RollenArt.ORGADMIN, + name: 'rolle2', + }); + const rolle3: Rolle = DoFactory.createRolle(true, { + rollenart: RollenArt.ORGADMIN, + name: 'rolle3', + }); + const rolle4: Rolle = DoFactory.createRolle(true, { + rollenart: RollenArt.ORGADMIN, + name: 'rolle4', + }); + const rollen: Rolle[] = [rolle1, rolle2, rolle3, rolle4]; + const orgsWithRecht: string[] = [organisation.id, childOrganisation.id]; + + organisationRepoMock.findById.mockResolvedValue(organisation); + organisationRepoMock.findChildOrgasForIds.mockResolvedValue([childOrganisation]); + organisationRepoMock.findByIds.mockResolvedValue( + new Map(orgsWithRecht.map((id: string) => [id, DoFactory.createOrganisation(true, { id })])), + ); + rolleRepoMock.find.mockResolvedValue(rollen); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValue(orgsWithRecht); + + anlage.initialize(organisation.id); + + const result: Rolle[] = await anlage.findRollenForOrganisation(permissions, undefined, 2); + + expect(result).toHaveLength(2); + }); + }); + describe('commit', () => { + it('should successfully commit personenkontexte', async () => { + const personId: string = faker.string.uuid(); + const lastModified: Date = faker.date.recent(); + const count: number = 1; + const personenkontexte: DbiamPersonenkontextBodyParams[] = []; + + const personenkontext: Personenkontext = createMock>(); + const updateResult: Personenkontext[] = [personenkontext]; + + dbiamPersonenkontextFactoryMock.createNewPersonenkontexteUpdate.mockReturnValue({ + update: jest.fn().mockResolvedValue(updateResult), + } as never); + + const result: Personenkontext[] | PersonenkontexteUpdateError = await anlage.commit( + personId, + lastModified, + count, + personenkontexte, + ); + + expect(result).toEqual(updateResult); + }); + + it('should return an error if PersonenkontexteUpdateError is returned', async () => { + const personId: string = faker.string.uuid(); + const lastModified: Date = faker.date.recent(); + const count: number = 1; + const personenkontexte: DbiamPersonenkontextBodyParams[] = []; + + const updateError: PersonenkontexteUpdateError = new PersonenkontexteUpdateError('Error message'); + dbiamPersonenkontextFactoryMock.createNewPersonenkontexteUpdate.mockReturnValue({ + update: jest.fn().mockResolvedValue(updateError), + } as never); + + const result: PersonenkontexteUpdateError | Personenkontext[] = await anlage.commit( + personId, + lastModified, + count, + personenkontexte, + ); + + expect(result).toBeInstanceOf(PersonenkontexteUpdateError); + }); + }); + + it('should return an empty array if no personenkontexte are passed', async () => { + const personId: string = faker.string.uuid(); + const lastModified: Date = faker.date.recent(); + const count: number = 0; + const personenkontexte: DbiamPersonenkontextBodyParams[] = []; + + dbiamPersonenkontextFactoryMock.createNewPersonenkontexteUpdate.mockReturnValue({ + update: jest.fn().mockResolvedValue([]), + } as never); + + const result: Personenkontext[] | PersonenkontexteUpdateError = await anlage.commit( + personId, + lastModified, + count, + personenkontexte, + ); + + expect(result).toEqual([]); + }); + + describe('findSchulstrukturknoten', () => { + it('should return list of schulstrukturknoten when parent-organisation is matching', async () => { + const [rolle, parentOrganisation, , , personenkontext]: [ + Rolle, + OrganisationDo, + OrganisationDo, + OrganisationDo, + Personenkontext, + ] = createRolleOrganisationsPersonKontext(personenkontextFactory, anlage); + const organisationen: OrganisationDo[] = [parentOrganisation]; + const personenkontexte: Personenkontext[] = [personenkontext]; + + organisationRepoMock.findByNameOrKennung.mockResolvedValue(organisationen); + dBiamPersonenkontextRepoMock.findByRolle.mockResolvedValue(personenkontexte); + + organisationRepoMock.findById.mockResolvedValue(parentOrganisation); + + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([]); + + const counted2: Counted> = [[], 1]; + organisationRepoMock.findBy.mockResolvedValueOnce(counted2); //mock call in findChildOrganisations, 2nd time (recursive) + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + parentOrganisation.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + }); + + describe('matching of parent or child SSK', () => { + it('should return list of schulstrukturknoten when child-organisation is matching', async () => { + const [rolle, parent, child, subchild]: [ + Rolle, + OrganisationDo, + OrganisationDo, + OrganisationDo, + Personenkontext, + ] = createRolleOrganisationsPersonKontext(personenkontextFactory, anlage); + + const foundByName: OrganisationDo[] = [child]; + + organisationRepoMock.findByNameOrKennung.mockResolvedValue(foundByName); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(parent); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([parent, child, subchild]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + child.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + }); + + it('should return list of schulstrukturknoten when child of child-organisation is matching with one results', async () => { + const [rolle, parent, child, childOfChild]: [ + Rolle, + OrganisationDo, + OrganisationDo, + OrganisationDo, + Personenkontext, + ] = createRolleOrganisationsPersonKontext(personenkontextFactory, anlage); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([child, childOfChild]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(parent); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([parent, child, childOfChild]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + child.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + }); + + it('should return list of schulstrukturknoten when a valid child with name exist', async () => { + const [rolle, parent, child]: [ + Rolle, + OrganisationDo, + OrganisationDo, + OrganisationDo, + Personenkontext, + ] = createRolleOrganisationsPersonKontext(personenkontextFactory, anlage); + + const foundByName: OrganisationDo[] = [child]; + const personenkontext: Personenkontext = createPersonenkontext(personenkontextFactory, true, { + rolleId: rolle.id, + organisationId: parent.id, + }); + const personenkontexte: Personenkontext[] = [personenkontext]; + + organisationRepoMock.findByNameOrKennung.mockResolvedValue(foundByName); + dBiamPersonenkontextRepoMock.findByRolle.mockResolvedValue(personenkontexte); + + organisationRepoMock.findById.mockResolvedValue(undefined); //mock call to find parent in findSchulstrukturknoten + + const counted: Counted> = [foundByName, 1]; + organisationRepoMock.findBy.mockResolvedValue(counted); //mock call in findChildOrganisations + + await expect(anlage.findSchulstrukturknoten(rolle.id, child.name!, LIMIT)).resolves.not.toThrow(Error); + }); + }); + + it('should return empty list when no rolle could be found', async () => { + const rolle: Rolle = DoFactory.createRolle(true); + const organisationen: OrganisationDo[] = [DoFactory.createOrganisation(true)]; + + organisationRepoMock.findByNameOrKennung.mockResolvedValue(organisationen); + rolleRepoMock.findById.mockResolvedValue(undefined); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten(rolle.id, 'nonexistent', LIMIT); + + expect(result).toHaveLength(0); + }); + + it('should return empty list when no parent organisation could be found', async () => { + const rolle: Rolle = DoFactory.createRolle(true); + const organisationen: OrganisationDo[] = [DoFactory.createOrganisation(true)]; + + organisationRepoMock.findByNameOrKennung.mockResolvedValue(organisationen); + rolleRepoMock.findById.mockResolvedValue(rolle); + organisationRepoMock.findById.mockResolvedValue(undefined); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten(rolle.id, 'nonexistent', LIMIT); + + expect(result).toHaveLength(0); + }); + + it('should return empty list when no ssks could be found', async () => { + const rolle: Rolle = DoFactory.createRolle(true); + const personenkontext: Personenkontext = createPersonenkontext(personenkontextFactory, true); + const personenkontexte: Personenkontext[] = [personenkontext]; + organisationRepoMock.findByNameOrKennung.mockResolvedValue([]); + dBiamPersonenkontextRepoMock.findByRolle.mockResolvedValue(personenkontexte); + + const counted: Counted> = [[], 0]; + organisationRepoMock.findBy.mockResolvedValueOnce(counted); //mock call in findChildOrganisations, 2nd time (recursive) + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten(rolle.id, 'nonexistent', LIMIT); + expect(result).toHaveLength(0); + }); + + describe('filter organisations by RollenArt', () => { + it('should return empty list, because orga as SCHULE does not match RollenArt SYSADMIN', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.SCHULE, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(0); + }); + + it('should return one element, because orga as LAND does match RollenArt SYSADMIN', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.LAND, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + expect(result).toContainEqual(organisationDo); + }); + + it('should return one element, because orga as ROOT does match RollenArt SYSADMIN', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.ROOT, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + expect(result).toContainEqual(organisationDo); + }); + + it('should return empty list, because orga as LAND does not match RollenArt LEIT', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.LAND, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(0); + }); + + it('should return one element, because orga as SCHULE does match RollenArt LEIT', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.SCHULE, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + expect(result).toContainEqual(organisationDo); + }); + + it('should return one element, because orga as SCHULE does match RollenArt LERN', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.SCHULE, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + expect(result).toContainEqual(organisationDo); + }); + + it('should return one element, because orga as KLASSE does match RollenArt LERN', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.KLASSE, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + expect(result).toContainEqual(organisationDo); + }); + + it('should return empty list, because orga as LAND does not match RollenArt LERN', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.LAND, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(0); + }); + + it('should return one element, because orga as SCHULE does match RollenArt LEHR', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.SCHULE, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(1); + expect(result).toContainEqual(organisationDo); + }); + + it('should return no element, because orga as KLASSE does not match RollenArt LEHR', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.KLASSE, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(0); + }); + + it('should return empty list, because orga as LAND does not match RollenArt LEHR', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.LAND, + }); + + organisationRepoMock.findByNameOrKennung.mockResolvedValue([organisationDo]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + ); + expect(result).toHaveLength(0); + }); + + it('should not return klassen when excluded', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.KLASSE, + }); + + organisationRepoMock.findByNameOrKennungAndExcludeByOrganisationType.mockResolvedValue([]); + rolleRepoMock.findById.mockResolvedValueOnce(rolle); + organisationRepoMock.findById.mockResolvedValue(organisationDo); //mock call to find parent in findSchulstrukturknoten + organisationRepoMock.findChildOrgasForIds.mockResolvedValueOnce([organisationDo]); + + const result: OrganisationDo[] = await anlage.findSchulstrukturknoten( + rolle.id, + organisationDo.name!, + LIMIT, + true, + ); + expect(result).toHaveLength(0); + }); + }); + }); + + describe('findAuthorizedRollen', () => { + it('should return list of all rollen when they exist, if the user is Landesadmin', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); + const leitRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); + const lehrRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); + const lernRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); + + const rollen: Rolle[] = [rolle, leitRolle, lehrRolle, lernRolle]; + rolleRepoMock.find.mockResolvedValue(rollen); + + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([ + organisationRepoMock.ROOT_ORGANISATION_ID, + ]); + + const result: Rolle[] = await anlage.findAuthorizedRollen(personpermissionsMock); + expect(result).toEqual(rollen); + }); + + it('should return list of all rollen when they exist Except Landesadmin, if the user is NOT Landesadmin', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); + const leitRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); + const lehrRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); + const lernRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); + + const rollen: Rolle[] = [rolle, leitRolle, lehrRolle, lernRolle]; + rolleRepoMock.find.mockResolvedValue(rollen); + + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.SCHULE, + }); + const organisationMap: Map> = new Map(); + organisationMap.set(organisationDo.id, organisationDo); + organisationRepoMock.findByIds.mockResolvedValueOnce(organisationMap); + + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationDo.id]); + + const result: Rolle[] = await anlage.findAuthorizedRollen(personpermissionsMock); + expect(result).not.toContain(rolle); + }); + + it('should return list of rollen when they exist', async () => { + const rolle: Rolle = DoFactory.createRolle(true); + rolleRepoMock.findByName.mockResolvedValue([rolle]); + + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([ + organisationRepoMock.ROOT_ORGANISATION_ID, + ]); + + const result: Rolle[] = await anlage.findAuthorizedRollen(personpermissionsMock, rolle.name, LIMIT); + expect(result).toEqual([rolle]); + }); + + it('should return empty list when no rollen exist', async () => { + rolleRepoMock.findByName.mockResolvedValue(undefined); + + const result: Rolle[] = await anlage.findAuthorizedRollen( + personpermissionsMock, + 'nonexistent', + LIMIT, + ); + expect(result).toEqual([]); + }); + + it('should return list of limited rollen, if the user is Landesadmin and the limit is set', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); + const leitRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); + const lehrRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); + const lernRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); + + const rollen: Rolle[] = [rolle, leitRolle, lehrRolle, lernRolle]; + rolleRepoMock.find.mockResolvedValue(rollen); + + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([ + organisationRepoMock.ROOT_ORGANISATION_ID, + ]); + + const result: Rolle[] = await anlage.findAuthorizedRollen(personpermissionsMock, undefined, 2); + expect(result).toHaveLength(2); + }); + + it('should return list of limited allowedRollen, if the user is NOT Landesadmin and the limit is set', async () => { + const rolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.SYSADMIN }); + const leitRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEIT }); + const lehrRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LEHR }); + const lernRolle: Rolle = DoFactory.createRolle(true, { rollenart: RollenArt.LERN }); + + const rollen: Rolle[] = [rolle, leitRolle, lehrRolle, lernRolle]; + rolleRepoMock.find.mockResolvedValue(rollen); + + const organisationDo: OrganisationDo = DoFactory.createOrganisation(true, { + typ: OrganisationsTyp.SCHULE, + }); + const organisationMap: Map> = new Map(); + organisationMap.set(organisationDo.id, organisationDo); + organisationRepoMock.findByIds.mockResolvedValueOnce(organisationMap); + + personpermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationDo.id]); + + const result: Rolle[] = await anlage.findAuthorizedRollen(personpermissionsMock, undefined, 2); + expect(result).toHaveLength(2); + }); + }); +}); diff --git a/src/modules/personenkontext/domain/personenkontext-workflow.ts b/src/modules/personenkontext/domain/personenkontext-workflow.ts new file mode 100644 index 000000000..1001584ab --- /dev/null +++ b/src/modules/personenkontext/domain/personenkontext-workflow.ts @@ -0,0 +1,332 @@ +import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; +import { Rolle } from '../../rolle/domain/rolle.js'; +import { OrganisationDo } from '../../organisation/domain/organisation.do.js'; +import { OrganisationRepo } from '../../organisation/persistence/organisation.repo.js'; +import { OrganisationsTyp } from '../../organisation/domain/organisation.enums.js'; +import { OrganisationMatchesRollenart } from '../specification/organisation-matches-rollenart.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { OrganisationID } from '../../../shared/types/aggregate-ids.types.js'; +import { RollenSystemRecht } from '../../rolle/domain/rolle.enums.js'; +import { DomainError } from '../../../shared/error/domain.error.js'; +import { EntityNotFoundError } from '../../../shared/error/entity-not-found.error.js'; +import { RolleNurAnPassendeOrganisationError } from '../specification/error/rolle-nur-an-passende-organisation.js'; +import { MissingPermissionsError } from '../../../shared/error/missing-permissions.error.js'; +import { PersonenkontexteUpdateError } from './error/personenkontexte-update.error.js'; +import { Personenkontext } from './personenkontext.js'; +import { PersonenkontexteUpdate } from './personenkontexte-update.js'; +import { DbiamPersonenkontextFactory } from './dbiam-personenkontext.factory.js'; +import { DbiamPersonenkontextBodyParams } from '../api/param/dbiam-personenkontext.body.params.js'; +import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; + +export class PersonenkontextWorkflowAggregate { + public selectedOrganisationId?: string; + + public selectedRolleId?: string; + + private constructor( + private readonly rolleRepo: RolleRepo, + private readonly organisationRepo: OrganisationRepo, + private readonly organisationRepository: OrganisationRepository, + private readonly dbiamPersonenkontextFactory: DbiamPersonenkontextFactory, + ) {} + + public static createNew( + rolleRepo: RolleRepo, + organisationRepo: OrganisationRepo, + organisationRepository: OrganisationRepository, + dbiamPersonenkontextFactory: DbiamPersonenkontextFactory, + ): PersonenkontextWorkflowAggregate { + return new PersonenkontextWorkflowAggregate( + rolleRepo, + organisationRepo, + organisationRepository, + dbiamPersonenkontextFactory, + ); + } + + // Initialize the aggregate with the selected Organisation and Rolle + public initialize(organisationId?: string, rolleId?: string): void { + this.selectedOrganisationId = organisationId; + this.selectedRolleId = rolleId; + } + + // Finds all SSKs that the admin can see + public async findAllSchulstrukturknoten( + permissions: PersonPermissions, + organisationName: string | undefined, + limit?: number, + ): Promise[]> { + let allOrganisationsExceptKlassen: OrganisationDo[] = []; + // If the search string for organisation is present then search for Name or Kennung + + allOrganisationsExceptKlassen = await this.organisationRepo.findByNameOrKennungAndExcludeByOrganisationType( + OrganisationsTyp.KLASSE, + organisationName, + limit, + ); + + if (allOrganisationsExceptKlassen.length === 0) return []; + + const orgsWithRecht: OrganisationID[] = await permissions.getOrgIdsWithSystemrecht( + [RollenSystemRecht.PERSONEN_VERWALTEN], + true, + ); + // Return only the orgas that the admin have rights on + const filteredOrganisations: OrganisationDo[] = allOrganisationsExceptKlassen.filter( + (orga: OrganisationDo) => orgsWithRecht.includes(orga.id as OrganisationID), + ); + + // Sort the filtered organizations, handling undefined kennung and name + filteredOrganisations.sort((a: OrganisationDo, b: OrganisationDo) => { + if (a.name && b.name) { + const aTitle: string = a.kennung ? `${a.kennung} (${a.name})` : a.name; + const bTitle: string = b.kennung ? `${b.kennung} (${b.name})` : b.name; + return aTitle.localeCompare(bTitle, 'de', { numeric: true }); + } + // Ensure a return value for cases where name is not defined (Should never happen normally) + if (a.name) return -1; + if (b.name) return 1; + return 0; + }); + // Return only the orgas that the admin have rights on + return filteredOrganisations; + } + + public async findRollenForOrganisation( + permissions: PersonPermissions, + rolleName?: string, + limit?: number, + ): Promise[]> { + let rollen: Option[]>; + + if (rolleName) { + rollen = await this.rolleRepo.findByName(rolleName); + } else { + rollen = await this.rolleRepo.find(); + } + + if (!rollen) { + return []; + } + + // Retrieve all organisations that the admin has access to + const orgsWithRecht: OrganisationID[] = await permissions.getOrgIdsWithSystemrecht( + [RollenSystemRecht.PERSONEN_VERWALTEN], + true, + ); + + // If the admin has no right on any orga then return an empty array + if (!orgsWithRecht || orgsWithRecht.length === 0) { + return []; + } + + let organisation: Option>; + if (this.selectedOrganisationId) { + // The organisation that was selected and that will be the base for the returned roles + organisation = await this.organisationRepo.findById(this.selectedOrganisationId); + } + // If the organisation was not found with the provided selected Id then just return an array of empty orgas + if (!organisation) { + return []; + } + + let allowedRollen: Rolle[] = []; + // If the user has rights for this specific organization or any of its children, return the filtered roles + if (orgsWithRecht.includes(organisation.id)) { + const organisationMatchesRollenart: OrganisationMatchesRollenart = new OrganisationMatchesRollenart(); + rollen.forEach(function (rolle: Rolle) { + // Check here what kind of roles the admin can assign depending on the type of organisation + if (organisationMatchesRollenart.isSatisfiedBy(organisation, rolle) && !allowedRollen.includes(rolle)) { + allowedRollen.push(rolle); + } + }); + } + if (limit) { + allowedRollen = allowedRollen.slice(0, limit); + } + + // Sort the Roles by name + return allowedRollen.sort((a: Rolle, b: Rolle) => + a.name.localeCompare(b.name, 'de', { numeric: true }), + ); + } + + // Verifies if the selected rolle and organisation can together be assigned to a kontext + // Also verifies again if the organisationId is allowed to be assigned by the admin + public async canCommit(permissions: PersonPermissions): Promise { + if (this.selectedOrganisationId && this.selectedRolleId) { + const referenceCheckError: Option = await this.checkReferences( + this.selectedOrganisationId, + this.selectedRolleId, + ); + if (referenceCheckError) { + return referenceCheckError; + } + + const permissionCheckError: Option = await this.checkPermissions( + permissions, + this.selectedOrganisationId, + ); + if (permissionCheckError) { + return permissionCheckError; + } + } + + return true; + } + + // Takes in the list of personenkontexte and decides whether to add or delete the personenkontexte for a specific PersonId + // This will only be used during "bearbeiten". + public async commit( + personId: string, + lastModified: Date, + count: number, + personenkontexte: DbiamPersonenkontextBodyParams[], + ): Promise[] | PersonenkontexteUpdateError> { + const pkUpdate: PersonenkontexteUpdate = this.dbiamPersonenkontextFactory.createNewPersonenkontexteUpdate( + personId, + lastModified, + count, + personenkontexte, + ); + const updateResult: Personenkontext[] | PersonenkontexteUpdateError = await pkUpdate.update(); + + if (updateResult instanceof PersonenkontexteUpdateError) { + return updateResult; + } + return updateResult; + } + + // Checks if the rolle can be assigned to the target organisation + public async checkReferences(organisationId: string, rolleId: string): Promise> { + const [orga, rolle]: [Option>, Option>] = await Promise.all([ + this.organisationRepository.findById(organisationId), + this.rolleRepo.findById(rolleId), + ]); + + if (!orga) { + return new EntityNotFoundError('Organisation', organisationId); + } + + if (!rolle) { + return new EntityNotFoundError('Rolle', rolleId); + } + // Can rolle be assigned at target orga + const canAssignRolle: boolean = await rolle.canBeAssignedToOrga(organisationId); + if (!canAssignRolle) { + return new EntityNotFoundError('Rolle', rolleId); // Rolle does not exist for the chosen organisation + } + + //The aimed organisation needs to match the type of role to be assigned + const organisationMatchesRollenart: OrganisationMatchesRollenart = new OrganisationMatchesRollenart(); + if (!organisationMatchesRollenart.isSatisfiedBy(orga, rolle)) { + return new RolleNurAnPassendeOrganisationError(); + } + + return undefined; + } + + public async checkPermissions( + permissions: PersonPermissions, + organisationId: string, + ): Promise> { + // Check if logged in person has permission + const hasPermissionAtOrga: boolean = await permissions.hasSystemrechtAtOrganisation(organisationId, [ + RollenSystemRecht.PERSONEN_VERWALTEN, + ]); + + // Missing permission on orga + if (!hasPermissionAtOrga) { + return new MissingPermissionsError('Unauthorized to manage persons at the organisation'); + } + + return undefined; + } + + public async findSchulstrukturknoten( + rolleId: string, + sskName: string, + limit?: number, + excludeKlassen: boolean = false, + ): Promise[]> { + this.selectedRolleId = rolleId; + + let organisationsFoundByName: OrganisationDo[] = []; + + if (excludeKlassen) { + organisationsFoundByName = await this.organisationRepo.findByNameOrKennungAndExcludeByOrganisationType( + OrganisationsTyp.KLASSE, + sskName, + ); + } else { + organisationsFoundByName = await this.organisationRepo.findByNameOrKennung(sskName); + } + + if (organisationsFoundByName.length === 0) return []; + + const rolleResult: Option> = await this.rolleRepo.findById(rolleId); + if (!rolleResult) return []; + + const organisationsRoleIsAvalableIn: OrganisationDo[] = []; + + const parentOrganisation: Option> = await this.organisationRepo.findById( + rolleResult.administeredBySchulstrukturknoten, + ); + if (!parentOrganisation) return []; + organisationsRoleIsAvalableIn.push(parentOrganisation); + + const childOrganisations: OrganisationDo[] = await this.organisationRepo.findChildOrgasForIds([ + rolleResult.administeredBySchulstrukturknoten, + ]); + organisationsRoleIsAvalableIn.push(...childOrganisations); + + let orgas: OrganisationDo[] = organisationsFoundByName.filter((ssk: OrganisationDo) => + organisationsRoleIsAvalableIn.some((organisation: OrganisationDo) => ssk.id === organisation.id), + ); + + const organisationMatchesRollenart: OrganisationMatchesRollenart = new OrganisationMatchesRollenart(); + orgas = orgas.filter((orga: OrganisationDo) => + organisationMatchesRollenart.isSatisfiedBy(orga, rolleResult), + ); + + return orgas.slice(0, limit); + } + + public async findAuthorizedRollen( + permissions: PersonPermissions, + rolleName?: string, + limit?: number, + ): Promise[]> { + let rollen: Option[]>; + + if (rolleName) { + rollen = await this.rolleRepo.findByName(rolleName); + } else { + rollen = await this.rolleRepo.find(); + } + + if (!rollen) return []; + + const orgsWithRecht: OrganisationID[] = await permissions.getOrgIdsWithSystemrecht( + [RollenSystemRecht.PERSONEN_VERWALTEN], + true, + ); + + //Landesadmin can view all roles. + if (orgsWithRecht.includes(this.organisationRepo.ROOT_ORGANISATION_ID)) { + return limit ? rollen.slice(0, limit) : rollen; + } + + const allowedRollen: Rolle[] = []; + const organisationMatchesRollenart: OrganisationMatchesRollenart = new OrganisationMatchesRollenart(); + (await this.organisationRepo.findByIds(orgsWithRecht)).forEach(function (orga: OrganisationDo) { + rollen.forEach(function (rolle: Rolle) { + if (organisationMatchesRollenart.isSatisfiedBy(orga, rolle) && !allowedRollen.includes(rolle)) { + allowedRollen.push(rolle); + } + }); + }); + + return limit ? allowedRollen.slice(0, limit) : allowedRollen; + } +} diff --git a/src/modules/personenkontext/domain/personenkontexte-update.spec.ts b/src/modules/personenkontext/domain/personenkontexte-update.spec.ts index df02b7fdc..3e94e7ed1 100644 --- a/src/modules/personenkontext/domain/personenkontexte-update.spec.ts +++ b/src/modules/personenkontext/domain/personenkontexte-update.spec.ts @@ -8,11 +8,14 @@ import { PersonID } from '../../../shared/types/index.js'; import { DbiamPersonenkontextBodyParams } from '../api/param/dbiam-personenkontext.body.params.js'; import { ClassLogger } from '../../../core/logging/class-logger.js'; import { UpdatePersonIdMismatchError } from './error/update-person-id-mismatch.error.js'; -import { EntityNotFoundError } from '../../../shared/error/index.js'; import { Personenkontext } from './personenkontext.js'; import { UpdateCountError } from './error/update-count.error.js'; import { UpdateOutdatedError } from './error/update-outdated.error.js'; import { PersonenkontexteUpdateError } from './error/personenkontexte-update.error.js'; +import { PersonenkontextFactory } from './personenkontext.factory.js'; +import { PersonRepository } from '../../person/persistence/person.repository.js'; +import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; +import { RolleRepo } from '../../rolle/repo/rolle.repo.js'; import { EventService } from '../../../core/eventbus/index.js'; function createPKBodyParams(personId: PersonID): DbiamPersonenkontextBodyParams[] { @@ -54,8 +57,21 @@ describe('PersonenkontexteUpdate', () => { provide: DBiamPersonenkontextRepo, useValue: createMock(), }, + { + provide: PersonRepository, + useValue: createMock(), + }, + { + provide: OrganisationRepository, + useValue: createMock(), + }, + { + provide: RolleRepo, + useValue: createMock(), + }, EventService, DbiamPersonenkontextFactory, + PersonenkontextFactory, ], }).compile(); dBiamPersonenkontextRepoMock = module.get(DBiamPersonenkontextRepo); @@ -100,9 +116,9 @@ describe('PersonenkontexteUpdate', () => { }); describe('update', () => { - describe('when sent personenkontexte contain non-existing personkontext', () => { + describe('when sent personenkontexte contain new personenkontext', () => { beforeAll(() => { - const count: number = 2; + const count: number = 1; sut = dbiamPersonenkontextFactory.createNewPersonenkontexteUpdate( personId, @@ -114,9 +130,15 @@ describe('PersonenkontexteUpdate', () => { it('should return PersonenkontextSpecificationError', async () => { dBiamPersonenkontextRepoMock.find.mockResolvedValue(null); - const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk1); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(null); //mock pk2 is not found => therefore handled as new + dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1]); //mock pk1 is found as existing in DB + + dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1, pk2]); //mock the return values in the end of update method - expect(updateError).toBeInstanceOf(PersonenkontexteUpdateError); + const updateResult: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); + + expect(updateResult).toBeInstanceOf(Array); }); }); @@ -138,25 +160,6 @@ describe('PersonenkontexteUpdate', () => { }); }); - describe('when existing personenkontexte cannot be found', () => { - beforeAll(() => { - const count: number = 2; - sut = dbiamPersonenkontextFactory.createNewPersonenkontexteUpdate(personId, lastModified, count, [ - bodyParam1, - bodyParam2, - ]); - }); - - it('should return EntityNotFoundError', async () => { - dBiamPersonenkontextRepoMock.find.mockResolvedValue(pk1); - dBiamPersonenkontextRepoMock.find.mockResolvedValue(pk2); - dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([]); //mock: no existing pks are found - const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); - - expect(updateError).toBeInstanceOf(EntityNotFoundError); - }); - }); - describe('when existing personenkontexte amount does NOT match count', () => { beforeAll(() => { const count: number = 2; @@ -167,8 +170,8 @@ describe('PersonenkontexteUpdate', () => { }); it('should return UpdateCountError', async () => { - dBiamPersonenkontextRepoMock.find.mockResolvedValue(pk1); - dBiamPersonenkontextRepoMock.find.mockResolvedValue(pk2); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk1); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk2); dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1]); //mock: only one PK is found const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -187,8 +190,8 @@ describe('PersonenkontexteUpdate', () => { }); it('should return UpdateOutdatedError', async () => { - dBiamPersonenkontextRepoMock.find.mockResolvedValue(pk1); - dBiamPersonenkontextRepoMock.find.mockResolvedValue(pk2); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk1); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk2); dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1, pk2]); //mock: both PKs are found const updateError: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); @@ -197,7 +200,7 @@ describe('PersonenkontexteUpdate', () => { }); describe('when validate returns no errors', () => { - beforeAll(() => { + beforeEach(() => { const count: number = 2; sut = dbiamPersonenkontextFactory.createNewPersonenkontexteUpdate(personId, lastModified, count, [ bodyParam1, @@ -205,14 +208,15 @@ describe('PersonenkontexteUpdate', () => { ]); }); - it('should return null', async () => { - dBiamPersonenkontextRepoMock.find.mockResolvedValue(pk1); - dBiamPersonenkontextRepoMock.find.mockResolvedValue(pk2); + it('should return null asc order', async () => { + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk1); + dBiamPersonenkontextRepoMock.find.mockResolvedValueOnce(pk2); dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1, pk2]); //mock: both PKs are found dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1, pk2]); //mock: return the PKs found after update const updateResult: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); expect(updateResult).toBeInstanceOf(Array); + expect(dBiamPersonenkontextRepoMock.delete).toHaveBeenCalledTimes(0); }); // This test only test for right sorting by date of PKs, pk2 and pk1 are switched in retrieval order @@ -222,8 +226,8 @@ describe('PersonenkontexteUpdate', () => { dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk2, pk1]); //mock: both PKs are found dBiamPersonenkontextRepoMock.findByPerson.mockResolvedValueOnce([pk1, pk2]); //mock: return the PKs found after update + expect(dBiamPersonenkontextRepoMock.delete).toHaveBeenCalledTimes(0); const updateResult: Personenkontext[] | PersonenkontexteUpdateError = await sut.update(); - expect(updateResult).toBeInstanceOf(Array); }); }); diff --git a/src/modules/personenkontext/domain/personenkontexte-update.ts b/src/modules/personenkontext/domain/personenkontexte-update.ts index 3160cfa59..6e71f404f 100644 --- a/src/modules/personenkontext/domain/personenkontexte-update.ts +++ b/src/modules/personenkontext/domain/personenkontexte-update.ts @@ -2,21 +2,20 @@ import { DbiamPersonenkontextBodyParams } from '../api/param/dbiam-personenkonte import { DBiamPersonenkontextRepo } from '../persistence/dbiam-personenkontext.repo.js'; import { Personenkontext } from './personenkontext.js'; import { UpdateCountError } from './error/update-count.error.js'; -import { UpdateNotFoundError } from './error/update-not-found.error.js'; -import { EntityNotFoundError } from '../../../shared/error/index.js'; import { UpdateOutdatedError } from './error/update-outdated.error.js'; import { PersonID } from '../../../shared/types/index.js'; import { UpdatePersonIdMismatchError } from './error/update-person-id-mismatch.error.js'; -import { ClassLogger } from '../../../core/logging/class-logger.js'; import { PersonenkontexteUpdateError } from './error/personenkontexte-update.error.js'; +import { PersonenkontextFactory } from './personenkontext.factory.js'; import { EventService } from '../../../core/eventbus/index.js'; import { PersonenkontextDeletedEvent } from '../../../shared/events/personenkontext-deleted.event.js'; +import { PersonenkontextCreatedEvent } from '../../../shared/events/personenkontext-created.event.js'; export class PersonenkontexteUpdate { private constructor( private readonly eventService: EventService, - private readonly logger: ClassLogger, private readonly dBiamPersonenkontextRepo: DBiamPersonenkontextRepo, + private readonly personenkontextFactory: PersonenkontextFactory, private readonly personId: PersonID, private readonly lastModified: Date, private readonly count: number, @@ -25,8 +24,8 @@ export class PersonenkontexteUpdate { public static createNew( eventService: EventService, - logger: ClassLogger, dBiamPersonenkontextRepo: DBiamPersonenkontextRepo, + personenkontextFactory: PersonenkontextFactory, personId: PersonID, lastModified: Date, count: number, @@ -34,8 +33,8 @@ export class PersonenkontexteUpdate { ): PersonenkontexteUpdate { return new PersonenkontexteUpdate( eventService, - logger, dBiamPersonenkontextRepo, + personenkontextFactory, personId, lastModified, count, @@ -43,8 +42,8 @@ export class PersonenkontexteUpdate { ); } - private async getSentPersonenkontexte(): Promise[] | PersonenkontexteUpdateError> { - const personenKontexte: Personenkontext[] = []; + private async getSentPersonenkontexte(): Promise[] | PersonenkontexteUpdateError> { + const personenKontexte: Personenkontext[] = []; for (const pkBodyParam of this.dBiamPersonenkontextBodyParams) { if (pkBodyParam.personId != this.personId) { return new UpdatePersonIdMismatchError(); @@ -55,18 +54,21 @@ export class PersonenkontexteUpdate { pkBodyParam.rolleId, ); if (!pk) { - return new UpdateNotFoundError(pkBodyParam.personId, pkBodyParam.organisationId, pkBodyParam.rolleId); + const newPK: Personenkontext = this.personenkontextFactory.createNew( + pkBodyParam.personId, + pkBodyParam.organisationId, + pkBodyParam.rolleId, + ); + personenKontexte.push(newPK); + } else { + personenKontexte.push(pk); } - personenKontexte.push(pk); } return personenKontexte; } private validate(existingPKs: Personenkontext[]): Option { - if (existingPKs.length == 0) { - return new EntityNotFoundError(); - } if (existingPKs.length != this.count) { return new UpdateCountError(); } @@ -83,18 +85,7 @@ export class PersonenkontexteUpdate { return null; } - public async update(): Promise[] | PersonenkontexteUpdateError> { - const sentPKs: Personenkontext[] | PersonenkontexteUpdateError = await this.getSentPersonenkontexte(); - if (sentPKs instanceof PersonenkontexteUpdateError) { - return sentPKs; - } - - const existingPKs: Personenkontext[] = await this.dBiamPersonenkontextRepo.findByPerson(this.personId); - const validationError: Option = this.validate(existingPKs); - if (validationError) { - return validationError; - } - + private async delete(existingPKs: Personenkontext[], sentPKs: Personenkontext[]): Promise { for (const existingPK of existingPKs) { if ( !sentPKs.some( @@ -104,13 +95,47 @@ export class PersonenkontexteUpdate { pk.rolleId == existingPK.rolleId, ) ) { - this.logger.info(`DELETE ${existingPK.organisationId}`); await this.dBiamPersonenkontextRepo.delete(existingPK); this.eventService.publish( new PersonenkontextDeletedEvent(existingPK.personId, existingPK.organisationId, existingPK.rolleId), ); } } + } + + private async add(existingPKs: Personenkontext[], sentPKs: Personenkontext[]): Promise { + for (const sentPK of sentPKs) { + if ( + !existingPKs.some( + (existingPK: Personenkontext) => + existingPK.personId == sentPK.personId && + existingPK.organisationId == sentPK.organisationId && + existingPK.rolleId == sentPK.rolleId, + ) + ) { + await this.dBiamPersonenkontextRepo.save(sentPK); + this.eventService.publish( + new PersonenkontextCreatedEvent(sentPK.personId, sentPK.organisationId, sentPK.rolleId), + ); + } + } + } + + public async update(): Promise[] | PersonenkontexteUpdateError> { + const sentPKs: Personenkontext[] | PersonenkontexteUpdateError = await this.getSentPersonenkontexte(); + if (sentPKs instanceof PersonenkontexteUpdateError) { + return sentPKs; + } + + const existingPKs: Personenkontext[] = await this.dBiamPersonenkontextRepo.findByPerson(this.personId); + const validationError: Option = this.validate(existingPKs); + if (validationError) { + return validationError; + } + + await this.delete(existingPKs, sentPKs); + await this.add(existingPKs, sentPKs); + const existingPKsAfterUpdate: Personenkontext[] = await this.dBiamPersonenkontextRepo.findByPerson( this.personId, ); diff --git a/src/modules/personenkontext/personenkontext-api.module.ts b/src/modules/personenkontext/personenkontext-api.module.ts index 9d2cdafd5..52938fb1d 100644 --- a/src/modules/personenkontext/personenkontext-api.module.ts +++ b/src/modules/personenkontext/personenkontext-api.module.ts @@ -10,12 +10,13 @@ import { RolleModule } from '../rolle/rolle.module.js'; import { OrganisationModule } from '../organisation/organisation.module.js'; import { DBiamPersonenkontextRepo } from './persistence/dbiam-personenkontext.repo.js'; import { DBiamPersonenkontextController } from './api/dbiam-personenkontext.controller.js'; -import { DbiamPersonenkontextFilterController } from './api/dbiam-personenkontext-filter.controller.js'; -import { PersonenkontextAnlageFactory } from './domain/personenkontext-anlage.factory.js'; +import { DbiamPersonenkontextWorkflowController } from './api/dbiam-personenkontext-workflow.controller.js'; +import { PersonenkontextWorkflowFactory } from './domain/personenkontext-workflow.factory.js'; import { DBiamPersonenkontextService } from './domain/dbiam-personenkontext.service.js'; import { DbiamPersonenkontextFactory } from './domain/dbiam-personenkontext.factory.js'; import { EventModule } from '../../core/eventbus/index.js'; import { PersonenkontextFactory } from './domain/personenkontext.factory.js'; +import { PersonenkontextCreationService } from './domain/personenkontext-creation.service.js'; @Module({ imports: [ @@ -32,10 +33,11 @@ import { PersonenkontextFactory } from './domain/personenkontext.factory.js'; PersonenkontextRepo, PersonRepo, DBiamPersonenkontextRepo, - PersonenkontextAnlageFactory, + PersonenkontextWorkflowFactory, DbiamPersonenkontextFactory, PersonenkontextFactory, + PersonenkontextCreationService, ], - controllers: [PersonenkontextController, DBiamPersonenkontextController, DbiamPersonenkontextFilterController], + controllers: [PersonenkontextController, DBiamPersonenkontextController, DbiamPersonenkontextWorkflowController], }) export class PersonenKontextApiModule {} diff --git a/src/modules/rolle/api/rolle.controller.ts b/src/modules/rolle/api/rolle.controller.ts index 48f4256af..b691913b1 100644 --- a/src/modules/rolle/api/rolle.controller.ts +++ b/src/modules/rolle/api/rolle.controller.ts @@ -97,7 +97,6 @@ export class RolleController { return new PagedResponse(pagedRolleWithServiceProvidersResponse); } const serviceProviders: ServiceProvider[] = await this.serviceProviderRepo.find(); - const rollenWithServiceProvidersResponses: RolleWithServiceProvidersResponse[] = rollen.map( (r: Rolle) => { const sps: ServiceProvider[] = r.serviceProviderIds diff --git a/src/modules/rolle/domain/rolle.ts b/src/modules/rolle/domain/rolle.ts index d07248386..e6695a728 100644 --- a/src/modules/rolle/domain/rolle.ts +++ b/src/modules/rolle/domain/rolle.ts @@ -77,7 +77,6 @@ export class Rolle { public async canBeAssignedToOrga(orgaId: OrganisationID): Promise { if (orgaId === this.administeredBySchulstrukturknoten) return true; - const childOrgas: Organisation[] = await this.organisationRepo.findChildOrgasForIds([ this.administeredBySchulstrukturknoten, ]);