From d79f56364af9784fa42114ead9bdbedaa26ca9df Mon Sep 17 00:00:00 2001 From: "Marvin Rode (Cap)" <127723478+marode-cap@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:45:00 +0200 Subject: [PATCH] SPSH-659 Send new organisations to itsLearning (#547) * send new organisations to itslearning --- .../dbildungs-iam-server/config/config.json | 6 +- config/config.json | 6 +- .../actions/create-group.action.ts | 2 +- .../actions/read-group.action.spec.ts | 47 ++++ .../itslearning/actions/read-group.action.ts | 66 +++++ .../itslearning-event-handler.spec.ts | 243 ++++++++++++++++++ .../itslearning/itslearning-event-handler.ts | 116 +++++++++ .../itslearning/itslearning.module.spec.ts | 4 +- src/modules/itslearning/itslearning.module.ts | 6 +- .../api/organisation.controller.spec.ts | 32 +-- .../api/organisation.controller.ts | 10 +- ...rganisation.repository.integration-spec.ts | 3 +- .../persistence/organisation.repository.ts | 15 +- src/shared/config/config.env.ts | 2 +- src/shared/config/config.loader.spec.ts | 8 +- src/shared/config/itslearning.config.ts | 12 +- test/config.test.json | 6 +- 17 files changed, 529 insertions(+), 55 deletions(-) create mode 100644 src/modules/itslearning/actions/read-group.action.spec.ts create mode 100644 src/modules/itslearning/actions/read-group.action.ts create mode 100644 src/modules/itslearning/itslearning-event-handler.spec.ts create mode 100644 src/modules/itslearning/itslearning-event-handler.ts diff --git a/charts/dbildungs-iam-server/config/config.json b/charts/dbildungs-iam-server/config/config.json index 8f22d4b63..018380300 100644 --- a/charts/dbildungs-iam-server/config/config.json +++ b/charts/dbildungs-iam-server/config/config.json @@ -47,9 +47,11 @@ "BACKEND_FOR_FRONTEND_MODULE_LOG_LEVEL": "debug" }, "ITSLEARNING": { - "ENABLED": false, + "ENABLED": "false", "ENDPOINT": "https://itslearning.example.com", "USERNAME": "username", - "PASSWORD": "password" + "PASSWORD": "password", + "ROOT_OEFFENTLICH": "oeffentlich", + "ROOT_ERSATZ": "ersatz" } } diff --git a/config/config.json b/config/config.json index 57eaabee5..11b81c9b9 100644 --- a/config/config.json +++ b/config/config.json @@ -55,9 +55,11 @@ "BACKEND_FOR_FRONTEND_MODULE_LOG_LEVEL": "debug" }, "ITSLEARNING": { - "ENABLED": false, + "ENABLED": "false", "ENDPOINT": "https://itslearning-test.example.com", "USERNAME": "username", - "PASSWORD": "password" + "PASSWORD": "password", + "ROOT_OEFFENTLICH": "oeffentlich", + "ROOT_ERSATZ": "ersatz" } } diff --git a/src/modules/itslearning/actions/create-group.action.ts b/src/modules/itslearning/actions/create-group.action.ts index b3b8404aa..cb026b219 100644 --- a/src/modules/itslearning/actions/create-group.action.ts +++ b/src/modules/itslearning/actions/create-group.action.ts @@ -7,7 +7,7 @@ export type CreateGroupParams = { id: string; name: string; - type: 'School' | 'Course' | 'CourseGroup'; + type: 'Unspecified' | 'Site' | 'School' | 'Course' | 'CourseGroup'; parentId: string; relationLabel?: string; diff --git a/src/modules/itslearning/actions/read-group.action.spec.ts b/src/modules/itslearning/actions/read-group.action.spec.ts new file mode 100644 index 000000000..620205877 --- /dev/null +++ b/src/modules/itslearning/actions/read-group.action.spec.ts @@ -0,0 +1,47 @@ +import { faker } from '@faker-js/faker'; +import { ReadGroupAction } from './read-group.action.js'; + +describe('ReadGroupAction', () => { + describe('buildRequest', () => { + it('should return object', () => { + const action: ReadGroupAction = new ReadGroupAction(faker.string.uuid()); + + expect(action.buildRequest()).toBeDefined(); + }); + }); + + describe('parseBody', () => { + it('should return result', () => { + const action: ReadGroupAction = new ReadGroupAction(faker.string.uuid()); + const name: string = faker.word.noun(); + const type: string = faker.word.noun(); + const parentId: string = faker.string.uuid(); + + expect( + action.parseBody({ + readGroupResponse: { + group: { + description: { descShort: name }, + groupType: { + scheme: '', + typeValue: { level: 1, type }, + }, + relationship: { + label: faker.word.noun(), + relation: 'parent', + sourceId: { identifier: parentId }, + }, + }, + }, + }), + ).toEqual({ + ok: true, + value: { + name, + type, + parentId, + }, + }); + }); + }); +}); diff --git a/src/modules/itslearning/actions/read-group.action.ts b/src/modules/itslearning/actions/read-group.action.ts new file mode 100644 index 000000000..ab96c1d1a --- /dev/null +++ b/src/modules/itslearning/actions/read-group.action.ts @@ -0,0 +1,66 @@ +import { DomainError } from '../../../shared/error/domain.error.js'; +import { IMS_COMMON_SCHEMA, IMS_GROUP_MAN_MESS_SCHEMA } from '../schemas.js'; +import { IMSESAction } from './base-action.js'; + +export type GroupResponse = { + name: string; + type: string; + parentId: string; +}; + +type ReadGroupResponseBody = { + readGroupResponse: { + group: { + groupType: { + scheme: string; + typeValue: { + type: string; + level: number; + }; + }; + relationship: { + relation: string; + sourceId: { + identifier: string; + }; + label: string; + }; + description: { + descShort: string; + descFull?: string; + }; + }; + }; +}; + +export class ReadGroupAction extends IMSESAction { + public override action: string = 'http://www.imsglobal.org/soap/gms/readGroup'; + + public constructor(private readonly id: string) { + super(); + } + + public override buildRequest(): object { + return { + 'ims:readGroupRequest': { + '@_xmlns:ims': IMS_GROUP_MAN_MESS_SCHEMA, + '@_xmlns:ims1': IMS_COMMON_SCHEMA, + + 'ims:sourcedId': { + 'ims1:identifier': this.id, + }, + }, + }; + } + + public override parseBody(body: ReadGroupResponseBody): Result { + return { + ok: true, + value: { + name: body.readGroupResponse.group.description.descShort, + type: body.readGroupResponse.group.groupType.typeValue.type, + parentId: body.readGroupResponse.group.relationship.sourceId.identifier, + }, + }; + } +} diff --git a/src/modules/itslearning/itslearning-event-handler.spec.ts b/src/modules/itslearning/itslearning-event-handler.spec.ts new file mode 100644 index 000000000..9bae3b3bd --- /dev/null +++ b/src/modules/itslearning/itslearning-event-handler.spec.ts @@ -0,0 +1,243 @@ +import { faker } from '@faker-js/faker'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigTestModule, LoggingTestModule } from '../../../test/utils/index.js'; +import { ClassLogger } from '../../core/logging/class-logger.js'; +import { SchuleCreatedEvent } from '../../shared/events/schule-created.event.js'; +import { OrganisationID } from '../../shared/types/index.js'; +import { OrganisationsTyp } from '../organisation/domain/organisation.enums.js'; +import { Organisation } from '../organisation/domain/organisation.js'; +import { OrganisationRepository } from '../organisation/persistence/organisation.repository.js'; +import { ItsLearningEventHandler } from './itslearning-event-handler.js'; +import { ItsLearningIMSESService } from './itslearning.service.js'; +import { ConfigService } from '@nestjs/config'; +import { ItsLearningConfig, ServerConfig } from '../../shared/config/index.js'; +import { CreateGroupAction } from './actions/create-group.action.js'; +import { DomainError } from '../../shared/error/domain.error.js'; + +describe('ItsLearning Event Handler', () => { + let module: TestingModule; + + let sut: ItsLearningEventHandler; + let orgaRepoMock: DeepMocked; + let itsLearningServiceMock: DeepMocked; + let loggerMock: DeepMocked; + + let configRootOeffentlich: string; + let configRootErsatz: string; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [LoggingTestModule, ConfigTestModule], + providers: [ + ItsLearningEventHandler, + { + provide: ItsLearningIMSESService, + useValue: createMock(), + }, + { + provide: OrganisationRepository, + useValue: createMock(), + }, + ], + }).compile(); + + sut = module.get(ItsLearningEventHandler); + orgaRepoMock = module.get(OrganisationRepository); + itsLearningServiceMock = module.get(ItsLearningIMSESService); + loggerMock = module.get(ClassLogger); + + const config: ConfigService = module.get(ConfigService); + configRootOeffentlich = config.getOrThrow('ITSLEARNING').ROOT_OEFFENTLICH; + configRootErsatz = config.getOrThrow('ITSLEARNING').ROOT_ERSATZ; + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + sut.ENABLED = true; + jest.resetAllMocks(); + }); + + describe('createSchuleEventHandler', () => { + it('should log on success', async () => { + const orgaId: OrganisationID = faker.string.uuid(); + const schuleName: OrganisationID = faker.word.noun(); + const oldParentId: OrganisationID = faker.string.uuid(); + const event: SchuleCreatedEvent = new SchuleCreatedEvent(orgaId); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ + id: orgaId, + typ: OrganisationsTyp.SCHULE, + name: schuleName, + administriertVon: configRootOeffentlich, + }), + ); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), + ); + orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ + createMock>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), + createMock>({ id: configRootErsatz, typ: OrganisationsTyp.LAND }), + ]); + itsLearningServiceMock.send.mockResolvedValueOnce({ ok: true, value: { parentId: oldParentId } }); // ReadGroupAction + itsLearningServiceMock.send.mockResolvedValueOnce({ + ok: true, + value: undefined, + }); // CreateGroupAction + + await sut.createSchuleEventHandler(event); + + expect(itsLearningServiceMock.send).toHaveBeenLastCalledWith(expect.any(CreateGroupAction)); + expect(loggerMock.info).toHaveBeenLastCalledWith(`Schule with ID ${orgaId} created.`); + }); + + it('should keep existing hierarchy', async () => { + const orgaId: OrganisationID = faker.string.uuid(); + const schuleName: OrganisationID = faker.word.noun(); + const oldParentId: OrganisationID = faker.string.uuid(); + const event: SchuleCreatedEvent = new SchuleCreatedEvent(orgaId); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ + typ: OrganisationsTyp.SCHULE, + name: schuleName, + administriertVon: configRootOeffentlich, + }), + ); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), + ); + orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ + createMock>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), + createMock>({ id: configRootErsatz, typ: OrganisationsTyp.LAND }), + ]); + itsLearningServiceMock.send.mockResolvedValueOnce({ ok: true, value: { parentId: oldParentId } }); // ReadGroupAction + itsLearningServiceMock.send.mockResolvedValueOnce({ + ok: true, + value: undefined, + }); // CreateGroupAction + + await sut.createSchuleEventHandler(event); + + expect(itsLearningServiceMock.send).toHaveBeenLastCalledWith(expect.any(CreateGroupAction)); + }); + + it('should skip event, if not enabled', async () => { + sut.ENABLED = false; + const event: SchuleCreatedEvent = new SchuleCreatedEvent(faker.string.uuid()); + + await sut.createSchuleEventHandler(event); + + expect(loggerMock.info).toHaveBeenCalledWith('Not enabled, ignoring event.'); + expect(orgaRepoMock.findById).not.toHaveBeenCalled(); + expect(itsLearningServiceMock.send).not.toHaveBeenCalled(); + }); + + it('should log error, if the organisation does not exist', async () => { + const orgaId: OrganisationID = faker.string.uuid(); + const event: SchuleCreatedEvent = new SchuleCreatedEvent(orgaId); + orgaRepoMock.findById.mockResolvedValueOnce(undefined); + + await sut.createSchuleEventHandler(event); + + expect(loggerMock.error).toHaveBeenCalledWith(`Organisation with id ${orgaId} could not be found!`); + expect(itsLearningServiceMock.send).not.toHaveBeenCalled(); + }); + + it('should skip event, if orga is not schule', async () => { + const orgaId: OrganisationID = faker.string.uuid(); + const event: SchuleCreatedEvent = new SchuleCreatedEvent(orgaId); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ typ: OrganisationsTyp.UNBEST }), + ); + + await sut.createSchuleEventHandler(event); + + expect(itsLearningServiceMock.send).not.toHaveBeenCalled(); + }); + + it('should skip event, if schule is ersatzschule', async () => { + const orgaId: OrganisationID = faker.string.uuid(); + const event: SchuleCreatedEvent = new SchuleCreatedEvent(orgaId); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ + typ: OrganisationsTyp.SCHULE, + administriertVon: configRootOeffentlich, + }), + ); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ id: configRootErsatz, typ: OrganisationsTyp.LAND }), + ); + orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ + createMock>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), + createMock>({ id: configRootErsatz, typ: OrganisationsTyp.LAND }), + ]); + + await sut.createSchuleEventHandler(event); + + expect(loggerMock.error).toHaveBeenCalledWith(`Ersatzschule, ignoring.`); + expect(itsLearningServiceMock.send).not.toHaveBeenCalled(); + }); + + it('should log error on failed creation', async () => { + const orgaId: OrganisationID = faker.string.uuid(); + const schuleName: OrganisationID = faker.word.noun(); + const event: SchuleCreatedEvent = new SchuleCreatedEvent(orgaId); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ + typ: OrganisationsTyp.SCHULE, + name: schuleName, + administriertVon: configRootOeffentlich, + }), + ); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), + ); + orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ + createMock>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), + createMock>({ id: configRootErsatz, typ: OrganisationsTyp.LAND }), + ]); + itsLearningServiceMock.send.mockResolvedValueOnce({ ok: false, error: createMock() }); // ReadGroupAction + itsLearningServiceMock.send.mockResolvedValueOnce({ + ok: false, + error: createMock({ message: 'Error' }), + }); // CreateGroupAction + + await sut.createSchuleEventHandler(event); + + expect(loggerMock.error).toHaveBeenLastCalledWith(`Could not create Schule in itsLearning: Error`); + }); + + it('should use "Öffentlich" as default, when no parent can be found', async () => { + const orgaId: OrganisationID = faker.string.uuid(); + const event: SchuleCreatedEvent = new SchuleCreatedEvent(orgaId); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ + typ: OrganisationsTyp.SCHULE, + administriertVon: configRootOeffentlich, + name: undefined, + }), + ); + orgaRepoMock.findById.mockResolvedValueOnce( + createMock>({ id: faker.string.uuid(), administriertVon: configRootOeffentlich }), + ); + orgaRepoMock.findById.mockResolvedValueOnce(undefined); + orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ + createMock>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), + createMock>({ id: configRootErsatz, typ: OrganisationsTyp.LAND }), + ]); + itsLearningServiceMock.send.mockResolvedValueOnce({ + ok: false, + error: createMock(), + }); // ReadGroupAction + itsLearningServiceMock.send.mockResolvedValueOnce({ + ok: true, + value: undefined, + }); // CreateGroupAction + + await sut.createSchuleEventHandler(event); + }); + }); +}); diff --git a/src/modules/itslearning/itslearning-event-handler.ts b/src/modules/itslearning/itslearning-event-handler.ts new file mode 100644 index 000000000..980c33a4b --- /dev/null +++ b/src/modules/itslearning/itslearning-event-handler.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +import { EventHandler } from '../../core/eventbus/decorators/event-handler.decorator.js'; +import { ClassLogger } from '../../core/logging/class-logger.js'; +import { ItsLearningConfig } from '../../shared/config/itslearning.config.js'; +import { ServerConfig } from '../../shared/config/server.config.js'; +import { DomainError } from '../../shared/error/index.js'; +import { SchuleCreatedEvent } from '../../shared/events/schule-created.event.js'; +import { OrganisationID } from '../../shared/types/aggregate-ids.types.js'; +import { OrganisationsTyp } from '../organisation/domain/organisation.enums.js'; +import { Organisation } from '../organisation/domain/organisation.js'; +import { OrganisationRepository } from '../organisation/persistence/organisation.repository.js'; +import { CreateGroupAction, CreateGroupParams } from './actions/create-group.action.js'; +import { GroupResponse, ReadGroupAction } from './actions/read-group.action.js'; +import { ItsLearningIMSESService } from './itslearning.service.js'; + +@Injectable() +export class ItsLearningEventHandler { + public ENABLED: boolean; + + private readonly ROOT_OEFFENTLICH: string; + + private readonly ROOT_ERSATZ: string; + + public constructor( + private readonly logger: ClassLogger, + private readonly itsLearningService: ItsLearningIMSESService, + private readonly organisationRepository: OrganisationRepository, + configService: ConfigService, + ) { + const itsLearningConfig: ItsLearningConfig = configService.getOrThrow('ITSLEARNING'); + + this.ENABLED = itsLearningConfig.ENABLED === 'true'; + + this.ROOT_OEFFENTLICH = itsLearningConfig.ROOT_OEFFENTLICH; + this.ROOT_ERSATZ = itsLearningConfig.ROOT_ERSATZ; + } + + @EventHandler(SchuleCreatedEvent) + public async createSchuleEventHandler(event: SchuleCreatedEvent): Promise { + this.logger.info(`Received CreateSchuleEvent, organisationId:${event.organisationId}`); + + if (!this.ENABLED) { + this.logger.info('Not enabled, ignoring event.'); + return; + } + + const organisation: Option> = await this.organisationRepository.findById( + event.organisationId, + ); + + if (!organisation) { + this.logger.error(`Organisation with id ${event.organisationId} could not be found!`); + return; + } + + if (organisation.typ === OrganisationsTyp.SCHULE) { + const parent: OrganisationID | undefined = await this.findParentId(organisation); + + if (parent === this.ROOT_ERSATZ) { + this.logger.error(`Ersatzschule, ignoring.`); + return; + } + + const params: CreateGroupParams = { + id: organisation.id, + name: organisation.name ?? 'Unbenannte Schule', + type: 'School', + parentId: parent, + }; + + { + // Check if school already exists in itsLearning + const readAction: ReadGroupAction = new ReadGroupAction(organisation.id); + const result: Result = await this.itsLearningService.send(readAction); + + if (result.ok) { + // School already exists, keep relationship + params.parentId = result.value.parentId; + } + } + + const action: CreateGroupAction = new CreateGroupAction(params); + + const result: Result = await this.itsLearningService.send(action); + + if (!result.ok) { + this.logger.error(`Could not create Schule in itsLearning: ${result.error.message}`); + } + + this.logger.info(`Schule with ID ${organisation.id} created.`); + } + } + + private async findParentId(organisation: Organisation): Promise { + const [oeffentlich, ersatz]: [Organisation | undefined, Organisation | undefined] = + await this.organisationRepository.findRootDirectChildren(); + + let parentOrgaId: OrganisationID | undefined = organisation.administriertVon; + + while (parentOrgaId) { + const result: Option> = await this.organisationRepository.findById(parentOrgaId); + + if (result?.id === oeffentlich?.id) { + return this.ROOT_OEFFENTLICH; + } else if (result?.id === ersatz?.id) { + return this.ROOT_ERSATZ; + } + + parentOrgaId = result?.administriertVon; + } + + return this.ROOT_OEFFENTLICH; + } +} diff --git a/src/modules/itslearning/itslearning.module.spec.ts b/src/modules/itslearning/itslearning.module.spec.ts index 242d01500..6decb69d1 100644 --- a/src/modules/itslearning/itslearning.module.spec.ts +++ b/src/modules/itslearning/itslearning.module.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigTestModule } from '../../../test/utils/index.js'; +import { ConfigTestModule, DatabaseTestModule, MapperTestModule } from '../../../test/utils/index.js'; import { ItsLearningModule } from './itslearning.module.js'; import { ItsLearningIMSESService } from './itslearning.service.js'; @@ -9,7 +9,7 @@ describe('ItsLearningModule', () => { beforeAll(async () => { module = await Test.createTestingModule({ - imports: [ConfigTestModule, ItsLearningModule], + imports: [ConfigTestModule, ItsLearningModule, MapperTestModule, DatabaseTestModule.forRoot()], }).compile(); }); diff --git a/src/modules/itslearning/itslearning.module.ts b/src/modules/itslearning/itslearning.module.ts index 593642adf..2470f592e 100644 --- a/src/modules/itslearning/itslearning.module.ts +++ b/src/modules/itslearning/itslearning.module.ts @@ -3,10 +3,12 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '../../core/logging/logger.module.js'; import { ItsLearningIMSESService } from './itslearning.service.js'; +import { ItsLearningEventHandler } from './itslearning-event-handler.js'; +import { OrganisationModule } from '../organisation/organisation.module.js'; @Module({ - imports: [LoggerModule.register(ItsLearningModule.name), HttpModule], - providers: [ItsLearningIMSESService], + imports: [LoggerModule.register(ItsLearningModule.name), HttpModule, OrganisationModule], + providers: [ItsLearningIMSESService, ItsLearningEventHandler], exports: [ItsLearningIMSESService], }) export class ItsLearningModule {} diff --git a/src/modules/organisation/api/organisation.controller.spec.ts b/src/modules/organisation/api/organisation.controller.spec.ts index 4966a7c8c..9c5c88e9a 100644 --- a/src/modules/organisation/api/organisation.controller.spec.ts +++ b/src/modules/organisation/api/organisation.controller.spec.ts @@ -355,7 +355,10 @@ describe('OrganisationController', () => { OrganisationsTyp.SCHULE, undefined, ); - const mockedRepoResponse: Organisation[] = [oeffentlich, ersatz]; + const mockedRepoResponse: [Organisation | undefined, Organisation | undefined] = [ + oeffentlich, + ersatz, + ]; organisationRepositoryMock.findRootDirectChildren.mockResolvedValue(mockedRepoResponse); @@ -369,33 +372,10 @@ describe('OrganisationController', () => { }); describe('when oeffentlich || ersatz could not be found', () => { it('should return an error', async () => { - const oeffentlich: Organisation = Organisation.construct( - faker.string.uuid(), - faker.date.past(), - faker.date.recent(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.numeric(), - 'Random Schule', - faker.lorem.word(), - faker.string.uuid(), - OrganisationsTyp.ROOT, + const mockedRepoResponse: [Organisation | undefined, Organisation | undefined] = [ undefined, - ); - const ersatz: Organisation = Organisation.construct( - faker.string.uuid(), - faker.date.past(), - faker.date.recent(), - faker.string.uuid(), - faker.string.uuid(), - faker.string.numeric(), - 'Random Schule 2', - faker.lorem.word(), - faker.string.uuid(), - OrganisationsTyp.SCHULE, undefined, - ); - const mockedRepoResponse: Organisation[] = [oeffentlich, ersatz]; + ]; organisationRepositoryMock.findRootDirectChildren.mockResolvedValue(mockedRepoResponse); diff --git a/src/modules/organisation/api/organisation.controller.ts b/src/modules/organisation/api/organisation.controller.ts index 3d8938172..68bc35c9c 100644 --- a/src/modules/organisation/api/organisation.controller.ts +++ b/src/modules/organisation/api/organisation.controller.ts @@ -135,13 +135,9 @@ export class OrganisationController { @ApiForbiddenResponse({ description: 'Insufficient permissions to get the organizations.' }) @ApiInternalServerErrorResponse({ description: 'Internal server error while getting the organization.' }) public async getRootChildren(): Promise { - const children: Organisation[] = await this.organisationRepository.findRootDirectChildren(); - const oeffentlich: Organisation | undefined = children.find((orga: Organisation) => - orga.name?.includes('Öffentliche'), - ); - const ersatz: Organisation | undefined = children.find((orga: Organisation) => - orga.name?.includes('Ersatz'), - ); + const [oeffentlich, ersatz]: [Organisation | undefined, Organisation | undefined] = + await this.organisationRepository.findRootDirectChildren(); + if (!oeffentlich || !ersatz) { throw SchulConnexErrorMapper.mapSchulConnexErrorToHttpException( SchulConnexErrorMapper.mapDomainErrorToSchulConnexError( diff --git a/src/modules/organisation/persistence/organisation.repository.integration-spec.ts b/src/modules/organisation/persistence/organisation.repository.integration-spec.ts index 9f1ab3684..ab82a0708 100644 --- a/src/modules/organisation/persistence/organisation.repository.integration-spec.ts +++ b/src/modules/organisation/persistence/organisation.repository.integration-spec.ts @@ -457,7 +457,8 @@ describe('OrganisationRepository', () => { describe('When Called', () => { it('should return flaged oeffentlich & ersatz root nodes', async () => { - const result: Organisation[] = await sut.findRootDirectChildren(); + const result: [Organisation | undefined, Organisation | undefined] = + await sut.findRootDirectChildren(); expect(result).toBeInstanceOf(Array); expect(result).toHaveLength(2); diff --git a/src/modules/organisation/persistence/organisation.repository.ts b/src/modules/organisation/persistence/organisation.repository.ts index 156b73d6c..9a0a168a1 100644 --- a/src/modules/organisation/persistence/organisation.repository.ts +++ b/src/modules/organisation/persistence/organisation.repository.ts @@ -107,15 +107,22 @@ export class OrganisationRepository { return rawResult.map(mapEntityToAggregate); } - public async findRootDirectChildren(): Promise[]> { + public async findRootDirectChildren(): Promise< + [oeffentlich: Organisation | undefined, ersatz: Organisation | undefined] + > { const scope: OrganisationScope = new OrganisationScope().findAdministrierteVon(this.ROOT_ORGANISATION_ID); const [entities]: Counted = await scope.executeQuery(this.em); - const organisations: Organisation[] = entities.map((entity: OrganisationEntity) => - mapEntityToAggregate(entity), + + const oeffentlich: OrganisationEntity | undefined = entities.find((entity: OrganisationEntity) => + entity.name?.includes('Öffentliche'), + ); + + const ersatz: OrganisationEntity | undefined = entities.find((entity: OrganisationEntity) => + entity.name?.includes('Ersatz'), ); - return organisations; + return [oeffentlich && mapEntityToAggregate(oeffentlich), ersatz && mapEntityToAggregate(ersatz)]; } public async findById(id: string): Promise>> { diff --git a/src/shared/config/config.env.ts b/src/shared/config/config.env.ts index c9736453b..3bd52d226 100644 --- a/src/shared/config/config.env.ts +++ b/src/shared/config/config.env.ts @@ -31,7 +31,7 @@ export default (): { HOSTNAME: process.env['BACKEND_HOSTNAME'], }, ITSLEARNING: { - ENABLED: process.env['ITSLEARNING_ENABLED']?.toLowerCase() === 'true', + ENABLED: process.env['ITSLEARNING_ENABLED']?.toLowerCase() as 'true' | 'false', ENDPOINT: process.env['ITSLEARNING_ENDPOINT'], USERNAME: process.env['ITSLEARNING_USERNAME'], PASSWORD: process.env['ITSLEARNING_PASSWORD'], diff --git a/src/shared/config/config.loader.spec.ts b/src/shared/config/config.loader.spec.ts index 9dbac4c29..bab2f7c01 100644 --- a/src/shared/config/config.loader.spec.ts +++ b/src/shared/config/config.loader.spec.ts @@ -46,9 +46,11 @@ describe('configloader', () => { DEFAULT_LOG_LEVEL: 'debug', }, ITSLEARNING: { - ENABLED: true, + ENABLED: 'true', ENDPOINT: 'http://itslearning', USERNAME: 'username', + ROOT_OEFFENTLICH: 'oeffentlich', + ROOT_ERSATZ: 'ersatz', }, }; @@ -120,10 +122,12 @@ describe('configloader', () => { DEFAULT_LOG_LEVEL: 'debug', }, ITSLEARNING: { - ENABLED: true, + ENABLED: 'true', ENDPOINT: 'http://itslearning', USERNAME: 'username', PASSWORD: 'password', + ROOT_OEFFENTLICH: 'oeffentlich', + ROOT_ERSATZ: 'ersatz', }, }; diff --git a/src/shared/config/itslearning.config.ts b/src/shared/config/itslearning.config.ts index 684911739..d027912ee 100644 --- a/src/shared/config/itslearning.config.ts +++ b/src/shared/config/itslearning.config.ts @@ -1,8 +1,8 @@ -import { IsBoolean, IsString } from 'class-validator'; +import { IsBooleanString, IsString } from 'class-validator'; export class ItsLearningConfig { - @IsBoolean() - public readonly ENABLED!: boolean; + @IsBooleanString() + public readonly ENABLED!: 'true' | 'false'; @IsString() public readonly ENDPOINT!: string; @@ -12,4 +12,10 @@ export class ItsLearningConfig { @IsString() public readonly PASSWORD!: string; + + @IsString() + public readonly ROOT_OEFFENTLICH!: string; + + @IsString() + public readonly ROOT_ERSATZ!: string; } diff --git a/test/config.test.json b/test/config.test.json index fa3b760e6..bcded2a3d 100644 --- a/test/config.test.json +++ b/test/config.test.json @@ -44,9 +44,11 @@ "DEFAULT_LOG_LEVEL": "info" }, "ITSLEARNING": { - "ENABLED": false, + "ENABLED": "false", "ENDPOINT": "https://itslearning-test.example.com", "USERNAME": "username", - "PASSWORD": "password" + "PASSWORD": "password", + "ROOT_OEFFENTLICH": "oeffentlich", + "ROOT_ERSATZ": "ersatz" } }