Skip to content

Commit

Permalink
Merge branch 'main' into SPSH-708
Browse files Browse the repository at this point in the history
  • Loading branch information
YoussefBouch authored Jun 26, 2024
2 parents 93ab5bb + d79f563 commit cbeb83b
Show file tree
Hide file tree
Showing 17 changed files with 529 additions and 55 deletions.
6 changes: 4 additions & 2 deletions charts/dbildungs-iam-server/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
6 changes: 4 additions & 2 deletions config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion src/modules/itslearning/actions/create-group.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions src/modules/itslearning/actions/read-group.action.spec.ts
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,
},
});
});
});
});
66 changes: 66 additions & 0 deletions src/modules/itslearning/actions/read-group.action.ts
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 src/modules/itslearning/itslearning-event-handler.spec.ts
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);
});
});
});
Loading

0 comments on commit cbeb83b

Please sign in to comment.