-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
529 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}, | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ReadGroupResponseBody, GroupResponse> { | ||
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<GroupResponse, DomainError> { | ||
return { | ||
ok: true, | ||
value: { | ||
name: body.readGroupResponse.group.description.descShort, | ||
type: body.readGroupResponse.group.groupType.typeValue.type, | ||
parentId: body.readGroupResponse.group.relationship.sourceId.identifier, | ||
}, | ||
}; | ||
} | ||
} |
243 changes: 243 additions & 0 deletions
243
src/modules/itslearning/itslearning-event-handler.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<OrganisationRepository>; | ||
let itsLearningServiceMock: DeepMocked<ItsLearningIMSESService>; | ||
let loggerMock: DeepMocked<ClassLogger>; | ||
|
||
let configRootOeffentlich: string; | ||
let configRootErsatz: string; | ||
|
||
beforeAll(async () => { | ||
module = await Test.createTestingModule({ | ||
imports: [LoggingTestModule, ConfigTestModule], | ||
providers: [ | ||
ItsLearningEventHandler, | ||
{ | ||
provide: ItsLearningIMSESService, | ||
useValue: createMock<ItsLearningIMSESService>(), | ||
}, | ||
{ | ||
provide: OrganisationRepository, | ||
useValue: createMock<OrganisationRepository>(), | ||
}, | ||
], | ||
}).compile(); | ||
|
||
sut = module.get(ItsLearningEventHandler); | ||
orgaRepoMock = module.get(OrganisationRepository); | ||
itsLearningServiceMock = module.get(ItsLearningIMSESService); | ||
loggerMock = module.get(ClassLogger); | ||
|
||
const config: ConfigService<ServerConfig> = module.get(ConfigService); | ||
configRootOeffentlich = config.getOrThrow<ItsLearningConfig>('ITSLEARNING').ROOT_OEFFENTLICH; | ||
configRootErsatz = config.getOrThrow<ItsLearningConfig>('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<Organisation<true>>({ | ||
id: orgaId, | ||
typ: OrganisationsTyp.SCHULE, | ||
name: schuleName, | ||
administriertVon: configRootOeffentlich, | ||
}), | ||
); | ||
orgaRepoMock.findById.mockResolvedValueOnce( | ||
createMock<Organisation<true>>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), | ||
); | ||
orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ | ||
createMock<Organisation<true>>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), | ||
createMock<Organisation<true>>({ 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<Organisation<true>>({ | ||
typ: OrganisationsTyp.SCHULE, | ||
name: schuleName, | ||
administriertVon: configRootOeffentlich, | ||
}), | ||
); | ||
orgaRepoMock.findById.mockResolvedValueOnce( | ||
createMock<Organisation<true>>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), | ||
); | ||
orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ | ||
createMock<Organisation<true>>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), | ||
createMock<Organisation<true>>({ 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<Organisation<true>>({ 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<Organisation<true>>({ | ||
typ: OrganisationsTyp.SCHULE, | ||
administriertVon: configRootOeffentlich, | ||
}), | ||
); | ||
orgaRepoMock.findById.mockResolvedValueOnce( | ||
createMock<Organisation<true>>({ id: configRootErsatz, typ: OrganisationsTyp.LAND }), | ||
); | ||
orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ | ||
createMock<Organisation<true>>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), | ||
createMock<Organisation<true>>({ 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<Organisation<true>>({ | ||
typ: OrganisationsTyp.SCHULE, | ||
name: schuleName, | ||
administriertVon: configRootOeffentlich, | ||
}), | ||
); | ||
orgaRepoMock.findById.mockResolvedValueOnce( | ||
createMock<Organisation<true>>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), | ||
); | ||
orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ | ||
createMock<Organisation<true>>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), | ||
createMock<Organisation<true>>({ id: configRootErsatz, typ: OrganisationsTyp.LAND }), | ||
]); | ||
itsLearningServiceMock.send.mockResolvedValueOnce({ ok: false, error: createMock() }); // ReadGroupAction | ||
itsLearningServiceMock.send.mockResolvedValueOnce({ | ||
ok: false, | ||
error: createMock<DomainError>({ 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<Organisation<true>>({ | ||
typ: OrganisationsTyp.SCHULE, | ||
administriertVon: configRootOeffentlich, | ||
name: undefined, | ||
}), | ||
); | ||
orgaRepoMock.findById.mockResolvedValueOnce( | ||
createMock<Organisation<true>>({ id: faker.string.uuid(), administriertVon: configRootOeffentlich }), | ||
); | ||
orgaRepoMock.findById.mockResolvedValueOnce(undefined); | ||
orgaRepoMock.findRootDirectChildren.mockResolvedValueOnce([ | ||
createMock<Organisation<true>>({ id: configRootOeffentlich, typ: OrganisationsTyp.LAND }), | ||
createMock<Organisation<true>>({ 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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.