diff --git a/src/modules/organisation/api/organisation.controller.spec.ts b/src/modules/organisation/api/organisation.controller.spec.ts index 0851687e0..b16b8b5cd 100644 --- a/src/modules/organisation/api/organisation.controller.spec.ts +++ b/src/modules/organisation/api/organisation.controller.spec.ts @@ -88,6 +88,7 @@ describe('OrganisationController', () => { }); describe('createOrganisation', () => { + const permissionsMock: PersonPermissions = createMock(); describe('when usecase returns a DTO', () => { it('should not throw an error', async () => { const params: CreateOrganisationBodyParams = { @@ -101,7 +102,7 @@ describe('OrganisationController', () => { const returnedValue: Organisation = DoFactory.createOrganisation(true); organisationServiceMock.createOrganisation.mockResolvedValueOnce({ ok: true, value: returnedValue }); - await expect(organisationController.createOrganisation(params)).resolves.not.toThrow(); + await expect(organisationController.createOrganisation(permissionsMock, params)).resolves.not.toThrow(); expect(organisationServiceMock.createOrganisation).toHaveBeenCalledTimes(1); }); }); @@ -153,7 +154,7 @@ describe('OrganisationController', () => { organisationRepositoryMock.findRootDirectChildren.mockResolvedValue(mockedRepoResponse); try { - await organisationController.createOrganisation(params); + await organisationController.createOrganisation(permissionsMock, params); fail('Expected error was not thrown'); } catch (error) { @@ -202,7 +203,7 @@ describe('OrganisationController', () => { error: new OrganisationSpecificationError('error', undefined), }); await expect( - organisationController.createOrganisation({} as CreateOrganisationBodyParams), + organisationController.createOrganisation(permissionsMock, {} as CreateOrganisationBodyParams), ).rejects.toThrow(OrganisationSpecificationError); expect(organisationServiceMock.createOrganisation).toHaveBeenCalledTimes(1); }); @@ -249,7 +250,7 @@ describe('OrganisationController', () => { error: {} as EntityNotFoundError, }); await expect( - organisationController.createOrganisation({} as CreateOrganisationBodyParams), + organisationController.createOrganisation(permissionsMock, {} as CreateOrganisationBodyParams), ).rejects.toThrow(HttpException); expect(organisationServiceMock.createOrganisation).toHaveBeenCalledTimes(1); }); @@ -257,6 +258,7 @@ describe('OrganisationController', () => { }); describe('updateOrganisation', () => { + const permissionsMock: DeepMocked = createMock(); describe('when usecase returns a DTO', () => { it('should not throw an error', async () => { const params: OrganisationByIdParams = { @@ -276,7 +278,9 @@ describe('OrganisationController', () => { const returnedValue: Organisation = DoFactory.createOrganisation(true); organisationServiceMock.updateOrganisation.mockResolvedValue({ ok: true, value: returnedValue }); - await expect(organisationController.updateOrganisation(params, body)).resolves.not.toThrow(); + await expect( + organisationController.updateOrganisation(params, body, permissionsMock), + ).resolves.not.toThrow(); expect(organisationServiceMock.updateOrganisation).toHaveBeenCalledTimes(1); }); }); @@ -291,6 +295,7 @@ describe('OrganisationController', () => { organisationController.updateOrganisation( { organisationId: faker.string.uuid() } as OrganisationByIdParams, {} as UpdateOrganisationBodyParams, + permissionsMock, ), ).rejects.toThrow(OrganisationSpecificationError); expect(organisationServiceMock.updateOrganisation).toHaveBeenCalledTimes(1); @@ -307,6 +312,7 @@ describe('OrganisationController', () => { organisationController.updateOrganisation( { organisationId: faker.string.uuid() } as OrganisationByIdParams, {} as UpdateOrganisationBodyParams, + permissionsMock, ), ).rejects.toThrow(HttpException); expect(organisationServiceMock.updateOrganisation).toHaveBeenCalledTimes(1); @@ -320,6 +326,7 @@ describe('OrganisationController', () => { organisationController.updateOrganisation( { organisationId: organisationId } as OrganisationByIdParams, {} as UpdateOrganisationBodyParams, + permissionsMock, ), ).rejects.toThrow(new NotFoundException(`Organisation with ID ${organisationId} not found`)); expect(organisationRepositoryMock.findById).toHaveBeenCalledTimes(1); @@ -719,6 +726,7 @@ describe('OrganisationController', () => { }); describe('updateOrganisationName', () => { + const permissionsMock: PersonPermissions = createMock(); describe('when usecase succeeds', () => { it('should not throw an error', async () => { const oeffentlich: Organisation = Organisation.construct( @@ -745,7 +753,9 @@ describe('OrganisationController', () => { organisationRepositoryMock.updateKlassenname.mockResolvedValueOnce(oeffentlich); - await expect(organisationController.updateOrganisationName(params, body)).resolves.not.toThrow(); + await expect( + organisationController.updateOrganisationName(params, body, permissionsMock), + ).resolves.not.toThrow(); }); }); @@ -760,9 +770,9 @@ describe('OrganisationController', () => { }; organisationRepositoryMock.updateKlassenname.mockResolvedValueOnce(new NameRequiredForKlasseError()); - await expect(organisationController.updateOrganisationName(params, body)).rejects.toThrow( - NameRequiredForKlasseError, - ); + await expect( + organisationController.updateOrganisationName(params, body, permissionsMock), + ).rejects.toThrow(NameRequiredForKlasseError); }); }); @@ -778,14 +788,15 @@ describe('OrganisationController', () => { organisationRepositoryMock.updateKlassenname.mockResolvedValueOnce(new EntityNotFoundError()); - await expect(organisationController.updateOrganisationName(params, body)).rejects.toThrow( - HttpException, - ); + await expect( + organisationController.updateOrganisationName(params, body, permissionsMock), + ).rejects.toThrow(HttpException); }); }); }); describe('updateOrganisationName', () => { + const permissionsMock: PersonPermissions = createMock(); describe('when usecase succeeds', () => { it('should not throw an error', async () => { const oeffentlich: Organisation = Organisation.construct( @@ -812,7 +823,9 @@ describe('OrganisationController', () => { organisationRepositoryMock.updateKlassenname.mockResolvedValueOnce(oeffentlich); - await expect(organisationController.updateOrganisationName(params, body)).resolves.not.toThrow(); + await expect( + organisationController.updateOrganisationName(params, body, permissionsMock), + ).resolves.not.toThrow(); }); }); @@ -827,9 +840,9 @@ describe('OrganisationController', () => { }; organisationRepositoryMock.updateKlassenname.mockResolvedValueOnce(new NameRequiredForKlasseError()); - await expect(organisationController.updateOrganisationName(params, body)).rejects.toThrow( - NameRequiredForKlasseError, - ); + await expect( + organisationController.updateOrganisationName(params, body, permissionsMock), + ).rejects.toThrow(NameRequiredForKlasseError); }); }); @@ -845,9 +858,9 @@ describe('OrganisationController', () => { organisationRepositoryMock.updateKlassenname.mockResolvedValueOnce(new EntityNotFoundError()); - await expect(organisationController.updateOrganisationName(params, body)).rejects.toThrow( - HttpException, - ); + await expect( + organisationController.updateOrganisationName(params, body, permissionsMock), + ).rejects.toThrow(HttpException); }); }); }); diff --git a/src/modules/organisation/api/organisation.controller.ts b/src/modules/organisation/api/organisation.controller.ts index 45b9437a6..c3ecb2b0d 100644 --- a/src/modules/organisation/api/organisation.controller.ts +++ b/src/modules/organisation/api/organisation.controller.ts @@ -85,7 +85,10 @@ export class OrganisationController { @ApiUnauthorizedResponse({ description: 'Not authorized to create the organisation.' }) @ApiForbiddenResponse({ description: 'Not permitted to create the organisation.' }) @ApiInternalServerErrorResponse({ description: 'Internal server error while creating the organisation.' }) - public async createOrganisation(@Body() params: CreateOrganisationBodyParams): Promise { + public async createOrganisation( + @Permissions() permissions: PersonPermissions, + @Body() params: CreateOrganisationBodyParams, + ): Promise { const [oeffentlich]: [Organisation | undefined, Organisation | undefined] = await this.organisationRepository.findRootDirectChildren(); @@ -106,6 +109,7 @@ export class OrganisationController { } const result: Result, DomainError> = await this.organisationService.createOrganisation( organisation, + permissions, ); if (!result.ok) { if (result.error instanceof OrganisationSpecificationError) { @@ -133,6 +137,7 @@ export class OrganisationController { public async updateOrganisation( @Param() params: OrganisationByIdParams, @Body() body: UpdateOrganisationBodyParams, + @Permissions() permissions: PersonPermissions, ): Promise { const existingOrganisation: Option> = await this.organisationRepository.findById( params.organisationId, @@ -156,6 +161,7 @@ export class OrganisationController { const result: Result, DomainError> = await this.organisationService.updateOrganisation( existingOrganisation, + permissions, ); if (result.ok) { @@ -423,12 +429,18 @@ export class OrganisationController { @ApiBadRequestResponse({ description: 'The input was not valid.', type: DbiamOrganisationError }) @ApiNotFoundResponse({ description: 'The organisation that should be deleted does not exist.' }) @ApiUnauthorizedResponse({ description: 'Not authorized to delete the organisation.' }) - public async deleteKlasse(@Param() params: OrganisationByIdParams): Promise { + public async deleteKlasse( + @Param() params: OrganisationByIdParams, + @Permissions() permissions: PersonPermissions, + ): Promise { if (await this.dBiamPersonenkontextRepo.isOrganisationAlreadyAssigned(params.organisationId)) { throw new OrganisationIstBereitsZugewiesenError(); } - const result: Option = await this.organisationRepository.deleteKlasse(params.organisationId); + const result: Option = await this.organisationRepository.deleteKlasse( + params.organisationId, + permissions, + ); if (result instanceof DomainError) { throw SchulConnexErrorMapper.mapSchulConnexErrorToHttpException( SchulConnexErrorMapper.mapDomainErrorToSchulConnexError(result), @@ -450,18 +462,19 @@ export class OrganisationController { public async updateOrganisationName( @Param() params: OrganisationByIdParams, @Body() body: OrganisationByNameBodyParams, + @Permissions() permissions: PersonPermissions, ): Promise { const result: DomainError | Organisation = await this.organisationRepository.updateKlassenname( params.organisationId, body.name, body.version, + permissions, ); if (result instanceof DomainError) { if (result instanceof OrganisationSpecificationError) { throw result; } - throw SchulConnexErrorMapper.mapSchulConnexErrorToHttpException( SchulConnexErrorMapper.mapDomainErrorToSchulConnexError(result), ); diff --git a/src/modules/organisation/domain/organisation-service-specification.spec.ts b/src/modules/organisation/domain/organisation-service-specification.spec.ts index 06fc9f53c..b9c52bd1d 100644 --- a/src/modules/organisation/domain/organisation-service-specification.spec.ts +++ b/src/modules/organisation/domain/organisation-service-specification.spec.ts @@ -18,6 +18,8 @@ import { EventModule } from '../../../core/eventbus/index.js'; import { Organisation } from './organisation.js'; import { OrganisationsTyp } from './organisation.enums.js'; import { OrganisationRepository } from '../persistence/organisation.repository.js'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; describe('OrganisationServiceSpecificationTest', () => { let module: TestingModule; @@ -35,6 +37,7 @@ describe('OrganisationServiceSpecificationTest', () => { DatabaseTestModule.forRoot({ isDatabaseRequired: true }), MapperTestModule, EventModule, + LoggingTestModule, ], providers: [OrganisationService, OrganisationRepository, OrganisationPersistenceMapperProfile], }).compile(); @@ -74,6 +77,7 @@ describe('OrganisationServiceSpecificationTest', () => { }); describe('create', () => { + const permissionsMock: DeepMocked = createMock(); it('should return DomainError, when KlasseNurVonSchuleAdministriert specificaton is not satisfied and type is KLASSE', async () => { const klasseDo: Organisation = DoFactory.createOrganisation(false, { name: 'Klasse', @@ -84,6 +88,7 @@ describe('OrganisationServiceSpecificationTest', () => { const result: Result, DomainError> = await organisationService.createOrganisation( klasseDo, + permissionsMock, ); expect(result).toEqual>>({ @@ -113,7 +118,10 @@ describe('OrganisationServiceSpecificationTest', () => { zugehoerigZu: schule.id, typ: OrganisationsTyp.KLASSE, }); - const result: Result> = await organisationService.createOrganisation(weitereKlasseDo); + const result: Result> = await organisationService.createOrganisation( + weitereKlasseDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -122,6 +130,7 @@ describe('OrganisationServiceSpecificationTest', () => { }); }); describe('update', () => { + const permissionsMock: DeepMocked = createMock(); it('should return DomainError, when klasse specifications are not satisfied and type is klasse', async () => { const klasse: Organisation = DoFactory.createOrganisation(false, { name: 'klasse', @@ -133,6 +142,7 @@ describe('OrganisationServiceSpecificationTest', () => { const result: Result, DomainError> = await organisationService.updateOrganisation( klassePersisted, + permissionsMock, ); expect(result).toEqual>>({ diff --git a/src/modules/organisation/domain/organisation.service.spec.ts b/src/modules/organisation/domain/organisation.service.spec.ts index 02bbc1a38..89bd5c7fa 100644 --- a/src/modules/organisation/domain/organisation.service.spec.ts +++ b/src/modules/organisation/domain/organisation.service.spec.ts @@ -21,6 +21,9 @@ import { NameForOrganisationWithTrailingSpaceError } from '../specification/erro import { KennungForOrganisationWithTrailingSpaceError } from '../specification/error/kennung-with-trailing-space.error.js'; import { EmailAdressOnOrganisationTypError } from '../specification/error/email-adress-on-organisation-typ-error.js'; import { KlasseWithoutNumberOrLetterError } from '../specification/error/klasse-without-number-or-letter.error.js'; +import { LoggingTestModule } from '../../../../test/utils/logging-test.module.js'; +import { PersonenkontextRolleFields, PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { KlasseNurVonSchuleAdministriertError } from '../specification/error/klasse-nur-von-schule-administriert.error.js'; describe('OrganisationService', () => { let module: TestingModule; @@ -30,7 +33,7 @@ describe('OrganisationService', () => { beforeAll(async () => { module = await Test.createTestingModule({ - imports: [ConfigTestModule], + imports: [ConfigTestModule, LoggingTestModule], providers: [ OrganisationService, { @@ -61,23 +64,106 @@ describe('OrganisationService', () => { }); describe('createOrganisation', () => { + const permissionsMock: DeepMocked = createMock(); + const organisationUser: Organisation = DoFactory.createOrganisation(true); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: organisationUser.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; it('should create an organisation', async () => { const organisation: Organisation = DoFactory.createOrganisation(false); organisationRepositoryMock.save.mockResolvedValue(organisation as unknown as Organisation); mapperMock.map.mockReturnValue(organisation as unknown as Dictionary); - const result: Result> = await organisationService.createOrganisation(organisation); + const result: Result> = await organisationService.createOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: true, value: organisation as unknown as Organisation, }); }); + it('should create a Schule and log its creation', async () => { + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + organisationRepositoryMock.findById.mockResolvedValueOnce(organisationUser); + const schule: Organisation = DoFactory.createOrganisation(false); + schule.typ = OrganisationsTyp.SCHULE; + organisationRepositoryMock.findBy.mockResolvedValueOnce([[], 0]); + organisationRepositoryMock.save.mockResolvedValue(schule as unknown as Organisation); + mapperMock.map.mockReturnValue(schule as unknown as Dictionary); + + const result: Result> = await organisationService.createOrganisation( + schule, + permissionsMock, + ); + + expect(result).toEqual>>({ + ok: true, + value: schule as unknown as Organisation, + }); + }); + + it('should create a Klasse and log its creation', async () => { + const schule: Organisation = DoFactory.createOrganisation(true); + const klasse: Organisation = DoFactory.createOrganisation(false); + schule.typ = OrganisationsTyp.SCHULE; + klasse.typ = OrganisationsTyp.KLASSE; + klasse.administriertVon = schule.id; + klasse.zugehoerigZu = schule.id; + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.save.mockResolvedValue(klasse as unknown as Organisation); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + mapperMock.map.mockReturnValue(klasse as unknown as Dictionary); + + const result: Result> = await organisationService.createOrganisation( + klasse, + permissionsMock, + ); + + expect(result).toEqual>>({ + ok: true, + value: klasse as unknown as Organisation, + }); + }); + + it('should fail to create a Klasse and log the creation attempt', async () => { + const schule: Organisation = DoFactory.createOrganisation(true); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + const klasse: Organisation = DoFactory.createOrganisation(false); + klasse.typ = OrganisationsTyp.KLASSE; + klasse.zugehoerigZu = schule.id; + klasse.administriertVon = schule.id; + organisationRepositoryMock.exists.mockResolvedValue(true); + organisationRepositoryMock.exists.mockResolvedValue(true); + organisationRepositoryMock.save.mockResolvedValue(klasse as unknown as Organisation); + mapperMock.map.mockReturnValue(klasse as unknown as Dictionary); + + const result: Result> = await organisationService.createOrganisation( + klasse, + permissionsMock, + ); + + expect(result).toEqual>>({ + ok: false, + error: new KlasseNurVonSchuleAdministriertError(), + }); + }); + it('should return a domain error if first parent organisation does not exist', async () => { const organisation: Organisation = DoFactory.createOrganisation(false); organisation.administriertVon = faker.string.uuid(); organisationRepositoryMock.exists.mockResolvedValueOnce(false); - const result: Result> = await organisationService.createOrganisation(organisation); + const result: Result> = await organisationService.createOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -90,7 +176,10 @@ describe('OrganisationService', () => { organisation.zugehoerigZu = faker.string.uuid(); organisationRepositoryMock.exists.mockResolvedValueOnce(false); - const result: Result> = await organisationService.createOrganisation(organisation); + const result: Result> = await organisationService.createOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -99,6 +188,9 @@ describe('OrganisationService', () => { }); it('should return a domain error if kennung is not set and type is schule', async () => { + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + organisationRepositoryMock.findById.mockResolvedValue(organisationUser); + const organisation: Organisation = DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE, kennung: undefined, @@ -106,7 +198,10 @@ describe('OrganisationService', () => { organisationRepositoryMock.save.mockResolvedValue(organisation as unknown as Organisation); mapperMock.map.mockReturnValue(organisation as unknown as Dictionary); - const result: Result> = await organisationService.createOrganisation(organisation); + const result: Result> = await organisationService.createOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -115,6 +210,9 @@ describe('OrganisationService', () => { }); it('should return a domain error if name is not set and type is schule', async () => { + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + organisationRepositoryMock.findById.mockResolvedValue(organisationUser); + const organisation: Organisation = DoFactory.createOrganisation(false, { typ: OrganisationsTyp.SCHULE, kennung: '1234567', @@ -123,7 +221,10 @@ describe('OrganisationService', () => { organisationRepositoryMock.save.mockResolvedValue(organisation as unknown as Organisation); mapperMock.map.mockReturnValue(organisation as unknown as Dictionary); - const result: Result> = await organisationService.createOrganisation(organisation); + const result: Result> = await organisationService.createOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -140,7 +241,10 @@ describe('OrganisationService', () => { organisationRepositoryMock.save.mockResolvedValue(organisation as unknown as Organisation); mapperMock.map.mockReturnValue(organisation as unknown as Dictionary); - const result: Result> = await organisationService.createOrganisation(organisation); + const result: Result> = await organisationService.createOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -149,6 +253,9 @@ describe('OrganisationService', () => { }); it('should return a domain error if kennung is not unique and type is schule', async () => { + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + organisationRepositoryMock.findById.mockResolvedValue(organisationUser); + const name: string = faker.string.alpha(); const kennung: string = faker.string.numeric({ length: 7 }); const organisation: Organisation = DoFactory.createOrganisation(false, { @@ -170,7 +277,10 @@ describe('OrganisationService', () => { organisationRepositoryMock.save.mockResolvedValue(organisation as unknown as Organisation); mapperMock.map.mockReturnValue(organisation as unknown as Dictionary); - const result: Result> = await organisationService.createOrganisation(organisation); + const result: Result> = await organisationService.createOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -181,7 +291,10 @@ describe('OrganisationService', () => { it('should return a domain error', async () => { const organisation: Organisation = DoFactory.createOrganisation(false); organisation.id = faker.string.uuid(); - const result: Result> = await organisationService.createOrganisation(organisation); + const result: Result> = await organisationService.createOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new EntityCouldNotBeCreated(`Organization could not be created`), @@ -191,7 +304,10 @@ describe('OrganisationService', () => { it('should return domain error if name contains trailing space', async () => { const organisationDo: Organisation = DoFactory.createOrganisation(false, { name: ' name' }); organisationRepositoryMock.exists.mockResolvedValueOnce(true).mockResolvedValueOnce(true); - const result: Result> = await organisationService.createOrganisation(organisationDo); + const result: Result> = await organisationService.createOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new NameForOrganisationWithTrailingSpaceError(), @@ -201,7 +317,10 @@ describe('OrganisationService', () => { it('should return domain error if kennung contains trailing space', async () => { const organisationDo: Organisation = DoFactory.createOrganisation(false, { kennung: ' ' }); organisationRepositoryMock.exists.mockResolvedValueOnce(true).mockResolvedValueOnce(true); - const result: Result> = await organisationService.createOrganisation(organisationDo); + const result: Result> = await organisationService.createOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new KennungForOrganisationWithTrailingSpaceError(), @@ -211,7 +330,10 @@ describe('OrganisationService', () => { it('should return domain error if name contains trailing space', async () => { const organisationDo: Organisation = DoFactory.createOrganisation(false, { name: ' name' }); organisationRepositoryMock.exists.mockResolvedValueOnce(true).mockResolvedValueOnce(true); - const result: Result> = await organisationService.createOrganisation(organisationDo); + const result: Result> = await organisationService.createOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new NameForOrganisationWithTrailingSpaceError(), @@ -221,7 +343,10 @@ describe('OrganisationService', () => { it('should return domain error if kennung contains trailing space', async () => { const organisationDo: Organisation = DoFactory.createOrganisation(false, { kennung: ' ' }); organisationRepositoryMock.exists.mockResolvedValueOnce(true).mockResolvedValueOnce(true); - const result: Result> = await organisationService.createOrganisation(organisationDo); + const result: Result> = await organisationService.createOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new KennungForOrganisationWithTrailingSpaceError(), @@ -234,7 +359,10 @@ describe('OrganisationService', () => { typ: OrganisationsTyp.KLASSE, }); organisationRepositoryMock.exists.mockResolvedValueOnce(true).mockResolvedValueOnce(true); - const result: Result> = await organisationService.createOrganisation(organisationDo); + const result: Result> = await organisationService.createOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new KlasseWithoutNumberOrLetterError(), @@ -243,21 +371,106 @@ describe('OrganisationService', () => { }); describe('updateOrganisation', () => { + const permissionsMock: DeepMocked = createMock(); + const organisationUser: Organisation = DoFactory.createOrganisation(true); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: organisationUser.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; it('should update an organisation', async () => { const organisation: Organisation = DoFactory.createOrganisation(true); organisationRepositoryMock.save.mockResolvedValue(organisation as unknown as Organisation); - const result: Result> = await organisationService.updateOrganisation(organisation); + organisationRepositoryMock.findById.mockResolvedValue(organisation); + const result: Result> = await organisationService.updateOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: true, value: organisation as unknown as Organisation, }); }); + it('should update a Schule and log the update', async () => { + const schule: Organisation = DoFactory.createOrganisation(true); + schule.typ = OrganisationsTyp.SCHULE; + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.findBy.mockResolvedValueOnce([[], 0]); + organisationRepositoryMock.save.mockResolvedValue(schule as unknown as Organisation); + mapperMock.map.mockReturnValue(schule as unknown as Dictionary); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + organisationRepositoryMock.findById.mockResolvedValue(organisationUser); + + const result: Result> = await organisationService.updateOrganisation( + schule, + permissionsMock, + ); + + expect(result).toEqual>>({ + ok: true, + value: schule as unknown as Organisation, + }); + }); + + it('should update a Klasse and log the update', async () => { + const schule: Organisation = DoFactory.createOrganisation(true); + const klasse: Organisation = DoFactory.createOrganisation(true); + schule.typ = OrganisationsTyp.SCHULE; + klasse.typ = OrganisationsTyp.KLASSE; + klasse.administriertVon = schule.id; + klasse.zugehoerigZu = schule.id; + organisationRepositoryMock.findById.mockResolvedValueOnce(klasse); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + organisationRepositoryMock.findChildOrgasForIds.mockResolvedValueOnce([]); + organisationRepositoryMock.save.mockResolvedValue(klasse as unknown as Organisation); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + mapperMock.map.mockReturnValue(klasse as unknown as Dictionary); + + const result: Result> = await organisationService.updateOrganisation( + klasse, + permissionsMock, + ); + + expect(result).toEqual>>({ + ok: true, + value: klasse as unknown as Organisation, + }); + }); + + it('should fail to update a Klasse and log the update attempt', async () => { + const schule: Organisation = DoFactory.createOrganisation(true); + const klasse: Organisation = DoFactory.createOrganisation(true); + klasse.typ = OrganisationsTyp.KLASSE; + klasse.zugehoerigZu = schule.id; + organisationRepositoryMock.findById.mockResolvedValueOnce(klasse); + organisationRepositoryMock.save.mockResolvedValue(klasse as unknown as Organisation); + organisationRepositoryMock.findById.mockResolvedValueOnce(schule); + mapperMock.map.mockReturnValue(klasse as unknown as Dictionary); + + const result: Result> = await organisationService.updateOrganisation( + klasse, + permissionsMock, + ); + + expect(result).toEqual>>({ + ok: false, + error: new KlasseNurVonSchuleAdministriertError(klasse.id), + }); + }); + it('should return a domain error', async () => { const organisation: Organisation = DoFactory.createOrganisation(true); organisation.id = ''; organisationRepositoryMock.findById.mockResolvedValue({} as Option>); - const result: Result> = await organisationService.updateOrganisation(organisation); + const result: Result> = await organisationService.updateOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new EntityCouldNotBeUpdated(`Organization could not be updated`, organisation.id), @@ -269,9 +482,14 @@ describe('OrganisationService', () => { typ: OrganisationsTyp.SCHULE, kennung: undefined, }); - organisationRepositoryMock.findById.mockResolvedValue(organisation as unknown as Organisation); + organisationRepositoryMock.findById.mockResolvedValueOnce(organisation as unknown as Organisation); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + organisationRepositoryMock.findById.mockResolvedValueOnce(organisationUser); - const result: Result> = await organisationService.updateOrganisation(organisation); + const result: Result> = await organisationService.updateOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -280,6 +498,9 @@ describe('OrganisationService', () => { }); it('should return a domain error if name is not set and type is schule', async () => { + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + organisationRepositoryMock.findById.mockResolvedValue(organisationUser); + const organisation: Organisation = DoFactory.createOrganisation(true, { typ: OrganisationsTyp.SCHULE, kennung: '1234567', @@ -287,7 +508,10 @@ describe('OrganisationService', () => { }); organisationRepositoryMock.findById.mockResolvedValue(organisation as unknown as Organisation); - const result: Result> = await organisationService.updateOrganisation(organisation); + const result: Result> = await organisationService.updateOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -303,7 +527,10 @@ describe('OrganisationService', () => { }); organisationRepositoryMock.findById.mockResolvedValue(organisation as unknown as Organisation); - const result: Result> = await organisationService.updateOrganisation(organisation); + const result: Result> = await organisationService.updateOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -312,6 +539,9 @@ describe('OrganisationService', () => { }); it('should return a domain error if kennung is not unique and type is schule', async () => { + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + organisationRepositoryMock.findById.mockResolvedValue(organisationUser); + const name: string = faker.string.alpha(); const kennung: string = faker.string.numeric({ length: 7 }); const organisation: Organisation = DoFactory.createOrganisation(true, { @@ -325,7 +555,10 @@ describe('OrganisationService', () => { organisationRepositoryMock.save.mockResolvedValue(organisation as unknown as Organisation); mapperMock.map.mockReturnValue(organisation as unknown as Dictionary); - const result: Result> = await organisationService.updateOrganisation(organisation); + const result: Result> = await organisationService.updateOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, @@ -336,7 +569,10 @@ describe('OrganisationService', () => { it('should return a domain error when organisation cannot be found on update', async () => { const organisation: Organisation = DoFactory.createOrganisation(true); organisationRepositoryMock.findById.mockResolvedValue(undefined); - const result: Result> = await organisationService.updateOrganisation(organisation); + const result: Result> = await organisationService.updateOrganisation( + organisation, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new EntityNotFoundError('Organisation', organisation.id), @@ -346,7 +582,10 @@ describe('OrganisationService', () => { it('should return domain error if name contains trailing space', async () => { const organisationDo: Organisation = DoFactory.createOrganisation(true, { name: ' ' }); organisationRepositoryMock.findById.mockResolvedValueOnce(DoFactory.createOrganisation(true)); - const result: Result> = await organisationService.updateOrganisation(organisationDo); + const result: Result> = await organisationService.updateOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new NameForOrganisationWithTrailingSpaceError(), @@ -356,7 +595,10 @@ describe('OrganisationService', () => { it('should return domain error if kennung contains trailing space', async () => { const organisationDo: Organisation = DoFactory.createOrganisation(true, { kennung: 'kennung ' }); organisationRepositoryMock.findById.mockResolvedValueOnce(DoFactory.createOrganisation(true)); - const result: Result> = await organisationService.updateOrganisation(organisationDo); + const result: Result> = await organisationService.updateOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new KennungForOrganisationWithTrailingSpaceError(), @@ -366,7 +608,10 @@ describe('OrganisationService', () => { it('should return domain error if name contains trailing space', async () => { const organisationDo: Organisation = DoFactory.createOrganisation(true, { name: ' ' }); organisationRepositoryMock.findById.mockResolvedValueOnce(DoFactory.createOrganisation(true)); - const result: Result> = await organisationService.updateOrganisation(organisationDo); + const result: Result> = await organisationService.updateOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new NameForOrganisationWithTrailingSpaceError(), @@ -376,7 +621,10 @@ describe('OrganisationService', () => { it('should return domain error if kennung contains trailing space', async () => { const organisationDo: Organisation = DoFactory.createOrganisation(true, { kennung: 'kennung ' }); organisationRepositoryMock.findById.mockResolvedValueOnce(DoFactory.createOrganisation(true)); - const result: Result> = await organisationService.updateOrganisation(organisationDo); + const result: Result> = await organisationService.updateOrganisation( + organisationDo, + permissionsMock, + ); expect(result).toEqual>>({ ok: false, error: new KennungForOrganisationWithTrailingSpaceError(), diff --git a/src/modules/organisation/domain/organisation.service.ts b/src/modules/organisation/domain/organisation.service.ts index 7df9bce2b..26c6d1fff 100644 --- a/src/modules/organisation/domain/organisation.service.ts +++ b/src/modules/organisation/domain/organisation.service.ts @@ -37,107 +37,223 @@ import { EmailAdressOnOrganisationTyp } from '../specification/email-on-organisa import { EmailAdressOnOrganisationTypError } from '../specification/error/email-adress-on-organisation-typ-error.js'; import { OrganisationsTyp } from './organisation.enums.js'; import { KlasseWithoutNumberOrLetterError } from '../specification/error/klasse-without-number-or-letter.error.js'; +import { ClassLogger } from '../../../core/logging/class-logger.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; @Injectable() export class OrganisationService { - public constructor(private readonly organisationRepo: OrganisationRepository) {} + public constructor( + private readonly logger: ClassLogger, + private readonly organisationRepo: OrganisationRepository, + ) {} + + private async logCreation( + permissions: PersonPermissions, + organisation: Organisation, + error?: Error, + ): Promise { + if (organisation.typ === OrganisationsTyp.KLASSE) { + if (organisation.zugehoerigZu) { + const school: Option> = await this.organisationRepo.findById( + organisation.zugehoerigZu, + ); + const schoolName: string = school?.name ?? 'SCHOOL_NOT_FOUND'; + if (error) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine neue Klasse ${organisation.name} (${schoolName}) anzulegen. Fehler: ${error.message}`, + ); + } else { + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat eine neue Klasse angelegt: ${organisation.name} (${schoolName}).`, + ); + } + } + } + if (organisation.typ === OrganisationsTyp.SCHULE) { + if (error) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine neue Schule ${organisation.name} anzulegen. Fehler: ${error.message}`, + ); + } else { + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat eine neue Schule angelegt: ${organisation.name}.`, + ); + } + } + } + + private async logUpdate( + permissions: PersonPermissions, + organisation: Organisation, + error?: Error, + ): Promise { + if (organisation.typ === OrganisationsTyp.KLASSE) { + if (organisation.zugehoerigZu) { + const school: Option> = await this.organisationRepo.findById( + organisation.zugehoerigZu, + ); + let schoolName: string = 'SCHOOL_NOT_FOUND'; + if (school) if (school.name) schoolName = school.name; + + if (error) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine Klasse ${organisation.name} (${schoolName}) zu verändern. Fehler: ${error.message}`, + ); + } else { + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat eine Klasse geändert: ${organisation.name} (${schoolName}).`, + ); + } + } + } + if (organisation.typ === OrganisationsTyp.SCHULE) { + if (error) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine Schule ${organisation.name} zu verändern. Fehler: ${error.message}`, + ); + } else { + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat eine Schule geändert: ${organisation.name}.`, + ); + } + } + } public async createOrganisation( organisationDo: Organisation, + permissions: PersonPermissions, ): Promise, DomainError>> { if (organisationDo.administriertVon && !(await this.organisationRepo.exists(organisationDo.administriertVon))) { + const error: DomainError = new EntityNotFoundError('Organisation', organisationDo.administriertVon); + await this.logCreation(permissions, organisationDo, error); return { ok: false, - error: new EntityNotFoundError('Organisation', organisationDo.administriertVon), + error: error, }; } if (organisationDo.zugehoerigZu && !(await this.organisationRepo.exists(organisationDo.zugehoerigZu))) { + const error: DomainError = new EntityNotFoundError('Organisation', organisationDo.zugehoerigZu); + await this.logCreation(permissions, organisationDo, error); return { ok: false, - error: new EntityNotFoundError('Organisation', organisationDo.zugehoerigZu), + error: error, }; } const validationFieldnamesResult: void | DomainError = this.validateFieldNames(organisationDo); if (validationFieldnamesResult) { - return { ok: false, error: validationFieldnamesResult }; + const error: DomainError = validationFieldnamesResult; + await this.logCreation(permissions, organisationDo, error); + return { ok: false, error: error }; } let validationResult: Result = await this.validateKennungRequiredForSchule(organisationDo); if (!validationResult.ok) { - return { ok: false, error: validationResult.error }; + const error: DomainError = validationResult.error; + await this.logCreation(permissions, organisationDo, error); + return { ok: false, error: error }; } validationResult = await this.validateNameRequiredForSchule(organisationDo); if (!validationResult.ok) { - return { ok: false, error: validationResult.error }; + const error: DomainError = validationResult.error; + await this.logCreation(permissions, organisationDo, error); + return { ok: false, error: error }; } validationResult = await this.validateSchuleKennungUnique(organisationDo); if (!validationResult.ok) { - return { ok: false, error: validationResult.error }; + const error: DomainError = validationResult.error; + await this.logCreation(permissions, organisationDo, error); + return { ok: false, error: error }; } validationResult = await this.validateEmailAdressOnOrganisationTyp(organisationDo); if (!validationResult.ok) { - return { ok: false, error: validationResult.error }; + const error: DomainError = validationResult.error; + await this.logCreation(permissions, organisationDo, error); + return { ok: false, error: error }; } const validateKlassen: Result = await this.validateKlassenSpecifications(organisationDo); if (!validateKlassen.ok) { - return { ok: false, error: validateKlassen.error }; + const error: DomainError = validateKlassen.error; + await this.logCreation(permissions, organisationDo, error); + return { ok: false, error: error }; } const organisation: Organisation | OrganisationSpecificationError = await this.organisationRepo.save(organisationDo); if (organisation instanceof Organisation) { + await this.logCreation(permissions, organisation); return { ok: true, value: organisation }; } - return { ok: false, error: new EntityCouldNotBeCreated(`Organization could not be created`) }; + + const error: DomainError = new EntityCouldNotBeCreated(`Organization could not be created`); + await this.logCreation(permissions, organisationDo, error); + return { ok: false, error: error }; } public async updateOrganisation( organisationDo: Organisation, + permissions: PersonPermissions, ): Promise, DomainError>> { const storedOrganisation: Option> = await this.organisationRepo.findById(organisationDo.id); if (!storedOrganisation) { - return { ok: false, error: new EntityNotFoundError('Organisation', organisationDo.id) }; + const error: DomainError = new EntityNotFoundError('Organisation', organisationDo.id); + await this.logUpdate(permissions, organisationDo, error); + return { ok: false, error: error }; } const validationFieldnamesResult: void | DomainError = this.validateFieldNames(organisationDo); if (validationFieldnamesResult) { - return { ok: false, error: validationFieldnamesResult }; + const error: DomainError = validationFieldnamesResult; + await this.logUpdate(permissions, organisationDo, error); + return { ok: false, error: error }; } let validationResult: Result = await this.validateKennungRequiredForSchule(organisationDo); if (!validationResult.ok) { - return { ok: false, error: validationResult.error }; + const error: DomainError = validationResult.error; + await this.logUpdate(permissions, organisationDo, error); + return { ok: false, error: error }; } validationResult = await this.validateNameRequiredForSchule(organisationDo); if (!validationResult.ok) { - return { ok: false, error: validationResult.error }; + const error: DomainError = validationResult.error; + await this.logUpdate(permissions, organisationDo, error); + return { ok: false, error: error }; } validationResult = await this.validateSchuleKennungUnique(organisationDo); if (!validationResult.ok) { - return { ok: false, error: validationResult.error }; + const error: DomainError = validationResult.error; + await this.logUpdate(permissions, organisationDo, error); + return { ok: false, error: error }; } validationResult = await this.validateEmailAdressOnOrganisationTyp(organisationDo); if (!validationResult.ok) { - return { ok: false, error: validationResult.error }; + const error: DomainError = validationResult.error; + await this.logUpdate(permissions, organisationDo, error); + return { ok: false, error: error }; } const validateKlassen: Result = await this.validateKlassenSpecifications(organisationDo); if (!validateKlassen.ok) { - return { ok: false, error: validateKlassen.error }; + const error: DomainError = validateKlassen.error; + await this.logUpdate(permissions, organisationDo, error); + return { ok: false, error: error }; } const organisation: Organisation | OrganisationSpecificationError = await this.organisationRepo.save(organisationDo); if (organisation instanceof Organisation) { + await this.logUpdate(permissions, organisation); return { ok: true, value: organisation }; } + const error: DomainError = new EntityCouldNotBeUpdated(`Organization could not be updated`, organisationDo.id); + await this.logUpdate(permissions, organisationDo, error); return { ok: false, - error: new EntityCouldNotBeUpdated(`Organization could not be updated`, organisationDo.id), + error: error, }; } diff --git a/src/modules/organisation/persistence/organisation.repository.integration-spec.ts b/src/modules/organisation/persistence/organisation.repository.integration-spec.ts index c3cf8cb93..dd708d3cc 100644 --- a/src/modules/organisation/persistence/organisation.repository.integration-spec.ts +++ b/src/modules/organisation/persistence/organisation.repository.integration-spec.ts @@ -790,12 +790,13 @@ describe('OrganisationRepository', () => { describe('deleteKlasse', () => { describe('when all validations succeed', () => { it('should succeed', async () => { + const permissionsMock: PersonPermissions = createMock(); const organisation: Organisation = DoFactory.createOrganisationAggregate(false, { typ: OrganisationsTyp.KLASSE, }); const savedOrganisaiton: Organisation = await sut.save(organisation); - await sut.deleteKlasse(savedOrganisaiton.id); + await sut.deleteKlasse(savedOrganisaiton.id, permissionsMock); const exists: boolean = await sut.exists(savedOrganisaiton.id); expect(exists).toBe(false); @@ -804,27 +805,30 @@ describe('OrganisationRepository', () => { describe('when organisation does not exist', () => { it('should return EntityNotFoundError', async () => { + const permissionsMock: DeepMocked = createMock(); const id: string = faker.string.uuid(); - const result: Option = await sut.deleteKlasse(id); + const result: Option = await sut.deleteKlasse(id, permissionsMock); expect(result).toEqual(new EntityNotFoundError('Organisation', id)); }); }); describe('when organisation is not a Klasse', () => { it('should return EntityCouldNotBeUpdated', async () => { + const permissionsMock: DeepMocked = createMock(); const organisation: Organisation = DoFactory.createOrganisationAggregate(false, { typ: OrganisationsTyp.SONSTIGE, name: 'test', }); const savedOrganisaiton: Organisation = await sut.save(organisation); - const result: Option = await sut.deleteKlasse(savedOrganisaiton.id); + const result: Option = await sut.deleteKlasse(savedOrganisaiton.id, permissionsMock); expect(result).toBeInstanceOf(EntityCouldNotBeUpdated); }); }); }); describe('updateKlassenname', () => { + const permissionsMock: PersonPermissions = createMock(); describe('when organisation does not exist', () => { it('should return EntityNotFoundError', async () => { const id: string = faker.string.uuid(); @@ -832,6 +836,7 @@ describe('OrganisationRepository', () => { id, faker.company.name(), faker.number.int(), + permissionsMock, ); expect(result).toEqual(new EntityNotFoundError('Organisation', id)); @@ -850,6 +855,7 @@ describe('OrganisationRepository', () => { savedOrganisaiton.id, faker.company.name(), faker.number.int(), + permissionsMock, ); expect(result).toBeInstanceOf(EntityCouldNotBeUpdated); @@ -867,6 +873,7 @@ describe('OrganisationRepository', () => { savedOrganisaiton.id, '', faker.number.int(), + permissionsMock, ); expect(result).toBeInstanceOf(OrganisationSpecificationError); @@ -910,6 +917,7 @@ describe('OrganisationRepository', () => { organisationEntity2.id, 'newName', 1, + permissionsMock, ); expect(result).not.toBeInstanceOf(DomainError); @@ -945,7 +953,7 @@ describe('OrganisationRepository', () => { // Simulate concurrent updates: // 1. First update - await sut.updateKlassenname(organisationEntity2.id, 'newName1', 1); + await sut.updateKlassenname(organisationEntity2.id, 'newName1', 1, permissionsMock); // 2. Try second update with original version (should fail) await expect(async () => { @@ -953,6 +961,7 @@ describe('OrganisationRepository', () => { organisationEntity2.id, 'newName2', 1, // This is now outdated because previous update incremented it + permissionsMock, ); }).rejects.toThrow(OrganisationUpdateOutdatedError); }); @@ -984,6 +993,7 @@ describe('OrganisationRepository', () => { organisationEntity2.id, 'name', 1, + permissionsMock, ); expect(result).not.toBeInstanceOf(DomainError); diff --git a/src/modules/organisation/persistence/organisation.repository.ts b/src/modules/organisation/persistence/organisation.repository.ts index f03843e58..3b7474853 100644 --- a/src/modules/organisation/persistence/organisation.repository.ts +++ b/src/modules/organisation/persistence/organisation.repository.ts @@ -415,7 +415,7 @@ export class OrganisationRepository { return [organisations, total + entitiesForIds.length - duplicates, pageTotal]; } - public async deleteKlasse(id: OrganisationID): Promise> { + public async deleteKlasse(id: OrganisationID, permissions: PersonPermissions): Promise> { const organisationEntity: Option = await this.em.findOne(OrganisationEntity, { id }); if (!organisationEntity) { return new EntityNotFoundError('Organisation', id); @@ -428,6 +428,15 @@ export class OrganisationRepository { await this.em.removeAndFlush(organisationEntity); this.eventService.publish(new KlasseDeletedEvent(organisationEntity.id)); + let schoolName: string = 'SCHOOL_NOT_FOUND'; + if (organisationEntity.zugehoerigZu) { + const school: Option> = await this.findById(organisationEntity.zugehoerigZu); + schoolName = school?.name ?? 'SCHOOL_NOT_FOUND'; + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat eine Klasse entfernt: ${organisationEntity.name} (${schoolName}).`, + ); + } + return; } @@ -435,6 +444,7 @@ export class OrganisationRepository { id: string, newName: string, version: number, + permissions: PersonPermissions, ): Promise> { const organisationFound: Option> = await this.findById(id); @@ -444,6 +454,11 @@ export class OrganisationRepository { if (organisationFound.typ !== OrganisationsTyp.KLASSE) { return new EntityCouldNotBeUpdated('Organisation', id, ['Only the name of Klassen can be updated.']); } + let schoolName: string = 'SCHOOL_NOT_FOUND'; + if (organisationFound.zugehoerigZu) { + const school: Option> = await this.findById(organisationFound.zugehoerigZu); + schoolName = school?.name ?? 'SCHOOL_NOT_FOUND'; + } //Specifications: it needs to be clarified how the specifications can be checked using DDD principles { if (organisationFound.name !== newName) { @@ -452,6 +467,9 @@ export class OrganisationRepository { await organisationFound.checkKlasseSpecifications(this); if (specificationError) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht den Namen einer Klasse ${organisationFound.name} (${schoolName}) zu verändern. Fehler: ${specificationError.message}`, + ); return specificationError; } } @@ -460,7 +478,9 @@ export class OrganisationRepository { const organisationEntity: Organisation | OrganisationSpecificationError = await this.save(organisationFound); this.eventService.publish(new KlasseUpdatedEvent(id, newName, organisationFound.administriertVon)); - + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat den Namen einer Klasse geändert: ${organisationFound.name} (${schoolName}).`, + ); return organisationEntity; } diff --git a/src/modules/organisation/specification/organisation-specification-mock-test.spec.ts b/src/modules/organisation/specification/organisation-specification-mock-test.spec.ts index 2cd6a85fa..8c6c480f0 100644 --- a/src/modules/organisation/specification/organisation-specification-mock-test.spec.ts +++ b/src/modules/organisation/specification/organisation-specification-mock-test.spec.ts @@ -1,5 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigTestModule, DatabaseTestModule, DoFactory, MapperTestModule } from '../../../../test/utils/index.js'; +import { + ConfigTestModule, + DatabaseTestModule, + DoFactory, + LoggingTestModule, + MapperTestModule, +} from '../../../../test/utils/index.js'; import { OrganisationPersistenceMapperProfile } from '../persistence/organisation-persistence.mapper.profile.js'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { OrganisationsTyp } from '../domain/organisation.enums.js'; @@ -17,7 +23,12 @@ describe('OrganisationSpecificationMockedRepoTest', () => { beforeAll(async () => { module = await Test.createTestingModule({ - imports: [ConfigTestModule, DatabaseTestModule.forRoot({ isDatabaseRequired: false }), MapperTestModule], + imports: [ + ConfigTestModule, + DatabaseTestModule.forRoot({ isDatabaseRequired: false }), + MapperTestModule, + LoggingTestModule, + ], providers: [ OrganisationPersistenceMapperProfile, { diff --git a/src/modules/organisation/specification/organisation-specifications-test.spec.ts b/src/modules/organisation/specification/organisation-specifications-test.spec.ts index 860ab4ea4..15bff41ea 100644 --- a/src/modules/organisation/specification/organisation-specifications-test.spec.ts +++ b/src/modules/organisation/specification/organisation-specifications-test.spec.ts @@ -39,6 +39,7 @@ describe('OrganisationSpecificationTests', () => { DatabaseTestModule.forRoot({ isDatabaseRequired: true }), MapperTestModule, EventModule, + LoggingTestModule, ], providers: [OrganisationPersistenceMapperProfile, OrganisationRepository], }).compile(); diff --git a/src/modules/person/api/personenuebersicht/dbiam-personuebersicht.controller.integration.spec.ts b/src/modules/person/api/personenuebersicht/dbiam-personuebersicht.controller.integration.spec.ts index 02f366742..59c8de33a 100644 --- a/src/modules/person/api/personenuebersicht/dbiam-personuebersicht.controller.integration.spec.ts +++ b/src/modules/person/api/personenuebersicht/dbiam-personuebersicht.controller.integration.spec.ts @@ -76,6 +76,7 @@ describe('Personenuebersicht API', () => { ConfigTestModule, DatabaseTestModule.forRoot({ isDatabaseRequired: true }), MapperTestModule, + LoggingTestModule, ], providers: [ { diff --git a/src/modules/person/api/personenuebersicht/dbiam-personuebersicht.controller.mocked.spec.ts b/src/modules/person/api/personenuebersicht/dbiam-personuebersicht.controller.mocked.spec.ts index 4c843ff7e..c3aef747d 100644 --- a/src/modules/person/api/personenuebersicht/dbiam-personuebersicht.controller.mocked.spec.ts +++ b/src/modules/person/api/personenuebersicht/dbiam-personuebersicht.controller.mocked.spec.ts @@ -5,6 +5,7 @@ import { DatabaseTestModule, DEFAULT_TIMEOUT_FOR_TESTCONTAINERS, DoFactory, + LoggingTestModule, MapperTestModule, } from '../../../../../test/utils/index.js'; import { ServiceProviderRepo } from '../../../service-provider/repo/service-provider.repo.js'; @@ -83,6 +84,7 @@ describe('Personenuebersicht API Mocked', () => { ConfigTestModule, DatabaseTestModule.forRoot({ isDatabaseRequired: false }), MapperTestModule, + LoggingTestModule, ], providers: [ServiceProviderRepo, RolleFactory, OrganisationRepository], }) diff --git a/src/modules/person/persistence/person.scope.integration-spec.ts b/src/modules/person/persistence/person.scope.integration-spec.ts index 9f3c42112..5a5343ea3 100644 --- a/src/modules/person/persistence/person.scope.integration-spec.ts +++ b/src/modules/person/persistence/person.scope.integration-spec.ts @@ -6,8 +6,8 @@ import { DEFAULT_TIMEOUT_FOR_TESTCONTAINERS, DatabaseTestModule, DoFactory, - MapperTestModule, LoggingTestModule, + MapperTestModule, } from '../../../../test/utils/index.js'; import { ScopeOrder } from '../../../shared/persistence/scope.enums.js'; import { PersonEntity } from './person.entity.js'; @@ -55,6 +55,7 @@ describe('PersonScope', () => { DatabaseTestModule.forRoot({ isDatabaseRequired: true }), MapperTestModule, PersonenKontextModule, + LoggingTestModule, ], providers: [ DBiamPersonenkontextRepoInternal, diff --git a/src/modules/personenkontext/persistence/personenkontext.repo.integration-spec.ts b/src/modules/personenkontext/persistence/personenkontext.repo.integration-spec.ts index eeac6f29d..288ffc190 100644 --- a/src/modules/personenkontext/persistence/personenkontext.repo.integration-spec.ts +++ b/src/modules/personenkontext/persistence/personenkontext.repo.integration-spec.ts @@ -5,8 +5,8 @@ import { DEFAULT_TIMEOUT_FOR_TESTCONTAINERS, DatabaseTestModule, DoFactory, - MapperTestModule, LoggingTestModule, + MapperTestModule, } from '../../../../test/utils/index.js'; import { PersonenkontextDo } from '../domain/personenkontext.do.js'; import { PersonPersistenceMapperProfile } from '../../person/persistence/person-persistence.mapper.profile.js'; @@ -42,6 +42,7 @@ describe('PersonenkontextRepo', () => { DatabaseTestModule.forRoot({ isDatabaseRequired: true }), MapperTestModule, EventModule, + LoggingTestModule, ], providers: [ PersonPersistenceMapperProfile, diff --git a/src/modules/personenkontext/persistence/personenkontext.scope.integration-spec.ts b/src/modules/personenkontext/persistence/personenkontext.scope.integration-spec.ts index 4e32d54ca..8802735c7 100644 --- a/src/modules/personenkontext/persistence/personenkontext.scope.integration-spec.ts +++ b/src/modules/personenkontext/persistence/personenkontext.scope.integration-spec.ts @@ -8,8 +8,8 @@ import { DEFAULT_TIMEOUT_FOR_TESTCONTAINERS, DatabaseTestModule, DoFactory, - MapperTestModule, LoggingTestModule, + MapperTestModule, } from '../../../../test/utils/index.js'; import { ScopeOrder } from '../../../shared/persistence/scope.enums.js'; import { PersonenkontextDo } from '../domain/personenkontext.do.js'; @@ -47,6 +47,7 @@ describe('PersonenkontextScope', () => { DatabaseTestModule.forRoot({ isDatabaseRequired: true }), MapperTestModule, EventModule, + LoggingTestModule, ], providers: [ PersonPersistenceMapperProfile, diff --git a/src/modules/rolle/api/rolle.controller.integration-spec.ts b/src/modules/rolle/api/rolle.controller.integration-spec.ts index a3fdbaa8a..7b92b71aa 100644 --- a/src/modules/rolle/api/rolle.controller.integration-spec.ts +++ b/src/modules/rolle/api/rolle.controller.integration-spec.ts @@ -39,7 +39,7 @@ import { DBiamPersonenkontextRepoInternal } from '../../personenkontext/persiste import { PersonRepository } from '../../person/persistence/person.repository.js'; import { KeycloakUserService } from '../../keycloak-administration/domain/keycloak-user.service.js'; -import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { PersonenkontextRolleFields, PersonPermissions } from '../../authentication/domain/person-permissions.js'; import { Person } from '../../person/domain/person.js'; import { DomainError } from '../../../shared/error/domain.error.js'; import { PersonFactory } from '../../person/domain/person.factory.js'; @@ -48,6 +48,8 @@ import { RolleServiceProviderBodyParams } from './rolle-service-provider.body.pa import { generatePassword } from '../../../shared/util/password-generator.js'; import { StepUpGuard } from '../../authentication/api/steup-up.guard.js'; import { DbiamRolleError } from './dbiam-rolle.error.js'; +import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; +import { Organisation } from '../../organisation/domain/organisation.js'; describe('Rolle API', () => { let app: INestApplication; @@ -56,10 +58,11 @@ describe('Rolle API', () => { let rolleRepo: RolleRepo; let personRepo: PersonRepository; let serviceProviderRepo: ServiceProviderRepo; + let organisationRepo: OrganisationRepository; let dBiamPersonenkontextRepoInternal: DBiamPersonenkontextRepoInternal; let personpermissionsRepoMock: DeepMocked; - let personPermissionsMock: DeepMocked; let personFactory: PersonFactory; + let permissionsMock: DeepMocked; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -118,6 +121,7 @@ describe('Rolle API', () => { rolleRepo = module.get(RolleRepo); personRepo = module.get(PersonRepository); serviceProviderRepo = module.get(ServiceProviderRepo); + organisationRepo = module.get(OrganisationRepository); personFactory = module.get(PersonFactory); const stepUpGuard: StepUpGuard = module.get(StepUpGuard); @@ -125,10 +129,6 @@ describe('Rolle API', () => { dBiamPersonenkontextRepoInternal = module.get(DBiamPersonenkontextRepoInternal); personpermissionsRepoMock = module.get(PersonPermissionsRepo); - - personPermissionsMock = createMock(); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personPermissionsMock); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); await DatabaseTestModule.setupDatabase(module.get(MikroORM)); app = module.createNestApplication(); await app.init(); @@ -140,11 +140,26 @@ describe('Rolle API', () => { }); beforeEach(async () => { + permissionsMock = createMock(); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); await DatabaseTestModule.clearDatabase(orm); }); describe('/POST rolle', () => { it('should return created rolle', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); @@ -167,6 +182,18 @@ describe('Rolle API', () => { }); it('should save rolle to db', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); @@ -187,6 +214,18 @@ describe('Rolle API', () => { }); it('should fail if the organisation does not exist', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const params: CreateRolleBodyParams = { name: faker.person.jobTitle(), administeredBySchulstrukturknoten: faker.string.uuid(), @@ -203,6 +242,18 @@ describe('Rolle API', () => { }); it('should fail if rollenart is invalid', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); @@ -222,6 +273,18 @@ describe('Rolle API', () => { }); it('should fail if merkmal is invalid', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); @@ -241,6 +304,18 @@ describe('Rolle API', () => { }); it('should fail if merkmale are not unique', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); @@ -306,7 +381,7 @@ describe('Rolle API', () => { return r.administeredBySchulstrukturknoten; }); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds }); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds }); const response: Response = await request(app.getHttpServer() as App) .get('/rolle') @@ -336,7 +411,7 @@ describe('Rolle API', () => { await rolleRepo.save(DoFactory.createRolle(false)); if (testRolle instanceof DomainError) throw Error(); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [testRolle.administeredBySchulstrukturknoten], }); @@ -375,7 +450,7 @@ describe('Rolle API', () => { return r.administeredBySchulstrukturknoten; }); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds }); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds }); const response: Response = await request(app.getHttpServer() as App) .get('/rolle') @@ -430,7 +505,7 @@ describe('Rolle API', () => { return r.administeredBySchulstrukturknoten; }); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds }); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds }); const response: Response = await request(app.getHttpServer() as App) .get('/rolle') @@ -449,7 +524,7 @@ describe('Rolle API', () => { const rolle: Rolle | DomainError = await rolleRepo.save(DoFactory.createRolle(false)); if (rolle instanceof DomainError) throw Error(); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [rolle.administeredBySchulstrukturknoten], }); @@ -471,7 +546,7 @@ describe('Rolle API', () => { ); if (rolle instanceof DomainError) throw Error(); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [rolle.administeredBySchulstrukturknoten], }); @@ -505,7 +580,7 @@ describe('Rolle API', () => { ); if (rolle instanceof DomainError) throw Error(); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [rolle.administeredBySchulstrukturknoten], }); @@ -718,6 +793,18 @@ describe('Rolle API', () => { describe('/PUT rolle', () => { it('should return updated rolle', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); await em.findOneOrFail(OrganisationEntity, { id: organisation.id }); @@ -730,10 +817,7 @@ describe('Rolle API', () => { ); if (rolle instanceof DomainError) throw Error(); - personPermissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ - all: false, - orgaIds: [organisation.id], - }); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [organisation.id] }); const serviceProvider: ServiceProvider = await serviceProviderRepo.save( DoFactory.createServiceProvider(false), @@ -763,6 +847,18 @@ describe('Rolle API', () => { }); it('should fail if the rolle does not exist', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const params: UpdateRolleBodyParams = { name: faker.person.jobTitle(), merkmale: [faker.helpers.enumValue(RollenMerkmal)], @@ -779,6 +875,15 @@ describe('Rolle API', () => { }); it('should return error with status-code 404 if user does NOT have permissions', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); await em.findOneOrFail(OrganisationEntity, { id: organisation.id }); @@ -789,11 +894,12 @@ describe('Rolle API', () => { rollenart: RollenArt.LEHR, }), ); + if (rolle instanceof DomainError) throw Error(); - const personpermissions: DeepMocked = createMock(); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ all: false, orgaIds: [] }); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); const params: UpdateRolleBodyParams = { name: faker.person.jobTitle(), @@ -817,6 +923,15 @@ describe('Rolle API', () => { }); it('should return error with status-code 404 if rolle is technical', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); await em.findOneOrFail(OrganisationEntity, { id: organisation.id }); @@ -828,14 +943,12 @@ describe('Rolle API', () => { istTechnisch: true, }), ); + if (rolle instanceof DomainError) throw Error(); - const personpermissions: DeepMocked = createMock(); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ - all: false, - orgaIds: [organisation.id], - }); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ all: false, orgaIds: [organisation.id] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); const params: UpdateRolleBodyParams = { name: faker.person.jobTitle(), @@ -860,6 +973,15 @@ describe('Rolle API', () => { describe('Update Merkmale', () => { it('should return 400 if rolle is already assigned', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + const personData: Person | DomainError = await personFactory.createNew({ vorname: faker.person.firstName(), familienname: faker.person.lastName(), @@ -896,12 +1018,9 @@ describe('Rolle API', () => { }), ); - const personpermissions: DeepMocked = createMock(); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ - all: false, - orgaIds: [organisation.id], - }); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [organisation.id] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); const params: UpdateRolleBodyParams = { name: faker.person.jobTitle(), @@ -924,6 +1043,15 @@ describe('Rolle API', () => { }); it('should return error if new name has trailing space', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); @@ -949,6 +1077,10 @@ describe('Rolle API', () => { version: 1, }; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [organisation.id] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const response: Response = await request(app.getHttpServer() as App) .put(`/rolle/${rolle.id}`) .send(params); @@ -964,6 +1096,18 @@ describe('Rolle API', () => { describe('/DELETE rolleId', () => { describe('should return error', () => { it('if rolle does NOT exist', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValue({ all: false, orgaIds: [] }); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + const response: Response = await request(app.getHttpServer() as App) .delete(`/rolle/${faker.string.uuid()}`) .send(); @@ -972,6 +1116,15 @@ describe('Rolle API', () => { }); it('if rolle is already assigned to a Personenkontext', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + const personData: Person | DomainError = await personFactory.createNew({ vorname: faker.person.firstName(), familienname: faker.person.lastName(), @@ -1005,12 +1158,14 @@ describe('Rolle API', () => { organisationId: organisation.id, }), ); - const personpermissions: DeepMocked = createMock(); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ + + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ all: false, orgaIds: [organisation.id], }); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); const response: Response = await request(app.getHttpServer() as App) .delete(`/rolle/${rolle.id}`) @@ -1024,6 +1179,15 @@ describe('Rolle API', () => { }); it('if user does NOT have permissions', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); await em.findOneOrFail(OrganisationEntity, { id: organisation.id }); @@ -1036,12 +1200,13 @@ describe('Rolle API', () => { ); if (rolle instanceof DomainError) throw Error(); - const personpermissions: DeepMocked = createMock(); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ all: false, orgaIds: [], }); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); const response: Response = await request(app.getHttpServer() as App) .delete(`/rolle/${rolle.id}`) @@ -1057,6 +1222,15 @@ describe('Rolle API', () => { }); it('if rolle is technical', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); await em.findOneOrFail(OrganisationEntity, { id: organisation.id }); @@ -1070,12 +1244,13 @@ describe('Rolle API', () => { ); if (rolle instanceof DomainError) throw Error(); - const personpermissions: DeepMocked = createMock(); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ all: false, orgaIds: [organisation.id], }); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); const response: Response = await request(app.getHttpServer() as App) .delete(`/rolle/${rolle.id}`) @@ -1093,6 +1268,15 @@ describe('Rolle API', () => { describe('should succeed', () => { it('if all conditions are passed', async () => { + const userOrganisation: Organisation = DoFactory.createOrganisation(false); + const savedUserOrganisation: Organisation = await organisationRepo.save(userOrganisation); + const personenkontextewithRolesMock: PersonenkontextRolleFields[] = [ + { + organisationsId: savedUserOrganisation.id, + rolle: { systemrechte: [], serviceProviderIds: [] }, + }, + ]; + const organisation: OrganisationEntity = new OrganisationEntity(); await em.persistAndFlush(organisation); await em.findOneOrFail(OrganisationEntity, { id: organisation.id }); @@ -1110,12 +1294,13 @@ describe('Rolle API', () => { ); if (rolle instanceof DomainError) throw Error(); - const personpermissions: DeepMocked = createMock(); - personpermissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ + permissionsMock.getOrgIdsWithSystemrecht.mockResolvedValueOnce({ all: false, orgaIds: [organisation.id], }); - personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(personpermissions); + permissionsMock.getPersonenkontextewithRoles.mockResolvedValue(personenkontextewithRolesMock); + + personpermissionsRepoMock.loadPersonPermissions.mockResolvedValue(permissionsMock); const response: Response = await request(app.getHttpServer() as App) .delete(`/rolle/${rolle.id}`) diff --git a/src/modules/rolle/api/rolle.controller.spec.ts b/src/modules/rolle/api/rolle.controller.spec.ts index 05fa9614e..c1b50d592 100644 --- a/src/modules/rolle/api/rolle.controller.spec.ts +++ b/src/modules/rolle/api/rolle.controller.spec.ts @@ -1,7 +1,12 @@ import { faker } from '@faker-js/faker'; import { APP_PIPE } from '@nestjs/core'; import { Test, TestingModule } from '@nestjs/testing'; -import { DEFAULT_TIMEOUT_FOR_TESTCONTAINERS, DoFactory, MapperTestModule } from '../../../../test/utils/index.js'; +import { + DEFAULT_TIMEOUT_FOR_TESTCONTAINERS, + DoFactory, + LoggingTestModule, + MapperTestModule, +} from '../../../../test/utils/index.js'; import { GlobalValidationPipe } from '../../../shared/validation/global-validation.pipe.js'; import { RolleRepo } from '../repo/rolle.repo.js'; import { RolleFactory } from '../domain/rolle.factory.js'; @@ -19,6 +24,7 @@ import { RollenArt, RollenMerkmal, RollenSystemRecht } from '../domain/rolle.enu import { NameForRolleWithTrailingSpaceError } from '../domain/name-with-trailing-space.error.js'; import { Organisation } from '../../organisation/domain/organisation.js'; import { RolleServiceProviderBodyParams } from './rolle-service-provider.body.params.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; describe('Rolle API with mocked ServiceProviderRepo', () => { let rolleRepoMock: DeepMocked; @@ -28,7 +34,7 @@ describe('Rolle API with mocked ServiceProviderRepo', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [MapperTestModule], + imports: [MapperTestModule, LoggingTestModule], providers: [ { provide: APP_PIPE, @@ -103,6 +109,7 @@ describe('Rolle API with mocked ServiceProviderRepo', () => { describe('/GET rolle mocked Rolle-repo', () => { describe('createRolle', () => { + const permissionsMock: PersonPermissions = createMock(); it('should throw an HTTP exception when rolleFactory.createNew returns DomainError', async () => { const createRolleParams: CreateRolleBodyParams = { name: ' SuS', @@ -118,7 +125,7 @@ describe('Rolle API with mocked ServiceProviderRepo', () => { value: organisation, }); - await expect(rolleController.createRolle(createRolleParams)).rejects.toThrow( + await expect(rolleController.createRolle(createRolleParams, permissionsMock)).rejects.toThrow( NameForRolleWithTrailingSpaceError, ); }); diff --git a/src/modules/rolle/api/rolle.controller.ts b/src/modules/rolle/api/rolle.controller.ts index 11bdb92b7..daf3dca26 100644 --- a/src/modules/rolle/api/rolle.controller.ts +++ b/src/modules/rolle/api/rolle.controller.ts @@ -62,6 +62,7 @@ import { OrganisationRepository } from '../../organisation/persistence/organisat import { Organisation } from '../../organisation/domain/organisation.js'; import { RolleServiceProviderBodyParams } from './rolle-service-provider.body.params.js'; import { StepUpGuard } from '../../authentication/api/steup-up.guard.js'; +import { ClassLogger } from '../../../core/logging/class-logger.js'; @UseFilters(new SchulConnexValidationErrorFilter(), new RolleExceptionFilter(), new AuthenticationExceptionFilter()) @ApiTags('rolle') @@ -76,6 +77,7 @@ export class RolleController { private readonly serviceProviderRepo: ServiceProviderRepo, private readonly dBiamPersonenkontextRepo: DBiamPersonenkontextRepo, private readonly organisationRepository: OrganisationRepository, + private readonly logger: ClassLogger, ) {} @Get() @@ -181,11 +183,17 @@ export class RolleController { @ApiUnauthorizedResponse({ description: 'Not authorized to create the rolle.' }) @ApiForbiddenResponse({ description: 'Insufficient permissions to create the rolle.' }) @ApiInternalServerErrorResponse({ description: 'Internal server error while creating the rolle.' }) - public async createRolle(@Body() params: CreateRolleBodyParams): Promise { + public async createRolle( + @Body() params: CreateRolleBodyParams, + @Permissions() permissions: PersonPermissions, + ): Promise { const orgResult: Result, DomainError> = await this.orgService.findOrganisationById( params.administeredBySchulstrukturknoten, ); if (!orgResult.ok) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine neue Rolle ${params.name} anzulegen. Fehler: ${orgResult.error.message}`, + ); throw SchulConnexErrorMapper.mapSchulConnexErrorToHttpException( SchulConnexErrorMapper.mapDomainErrorToSchulConnexError(orgResult.error), ); @@ -202,12 +210,21 @@ export class RolleController { ); if (rolle instanceof DomainError) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine neue Rolle ${params.name} anzulegen. Fehler: ${rolle.message}`, + ); throw rolle; } const result: Rolle | DomainError = await this.rolleRepo.save(rolle); if (result instanceof DomainError) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine neue Rolle ${params.name} anzulegen. Fehler: ${result.message}.`, + ); throw result; } + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat eine neue Rolle angelegt: ${result.name}.`, + ); return new RolleResponse(result); } @@ -366,6 +383,9 @@ export class RolleController { @Body() params: UpdateRolleBodyParams, @Permissions() permissions: PersonPermissions, ): Promise { + const rolle: Option> = await this.rolleRepo.findById(findRolleByIdParams.rolleId); + const rolleName: string = rolle?.name ?? 'ROLLE_NOT_FOUND'; + const isAlreadyAssigned: boolean = await this.dBiamPersonenkontextRepo.isRolleAlreadyAssigned( findRolleByIdParams.rolleId, ); @@ -382,14 +402,23 @@ export class RolleController { if (result instanceof DomainError) { if (result instanceof RolleDomainError) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine Rolle ${params.name} zu bearbeiten. Fehler: ${result.message}`, + ); throw result; } - + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht eine Rolle ${params.name} zu bearbeiten. Fehler: ${result.message}`, + ); throw SchulConnexErrorMapper.mapSchulConnexErrorToHttpException( SchulConnexErrorMapper.mapDomainErrorToSchulConnexError(result), ); } + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat eine Rolle bearbeitet: ${rolleName}.`, + ); + return this.returnRolleWithServiceProvidersResponse(result); } @@ -405,19 +434,31 @@ export class RolleController { @Param() findRolleByIdParams: FindRolleByIdParams, @Permissions() permissions: PersonPermissions, ): Promise { + const rolle: Option> = await this.rolleRepo.findById(findRolleByIdParams.rolleId); + const rolleName: string = rolle?.name ?? 'ROLLE_NOT_FOUND'; + const result: Option = await this.rolleRepo.deleteAuthorized( findRolleByIdParams.rolleId, permissions, ); if (result instanceof DomainError) { if (result instanceof RolleDomainError) { + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht die Rolle ${rolleName} zu entfernen. Fehler: ${result.message}`, + ); throw result; } - + this.logger.error( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat versucht die Rolle ${rolleName} zu entfernen. Fehler: ${result.message}`, + ); throw SchulConnexErrorMapper.mapSchulConnexErrorToHttpException( SchulConnexErrorMapper.mapDomainErrorToSchulConnexError(result), ); } + + this.logger.info( + `Admin ${permissions.personFields.username} (${permissions.personFields.id}) hat eine Rolle entfernt: ${rolleName}.`, + ); } private async returnRolleWithServiceProvidersResponse(