Skip to content

Commit

Permalink
SPSH-657: Schulen und Lehrer per Event ins LDAP kopieren (#510)
Browse files Browse the repository at this point in the history
* start persisting schools on creation in LDAP via EventBus

* implement event for CreatePersonenKontext (Lehrer on school)

* create LDAP-TestModule

* test for ldap-event-handler

* add missing tests, remove obsolete ones

* fix lint

* adjust test cases for LDAP Client Svc

* adjust docu about test with LDAP

* fix lint

* adjust coverageProvider to babel, adjust tests

* add test for ProviderController (allServiceProviders)

* add new events for DeleteSchule and DeletePersonenkontext

* fix lint in ldap-failure-test

* remove dbiam-personenuebersicht-scope

* adjust ldap tests

* Update tests for person-scope, oidc-strategy and authentication-controller

* exclude redisClient in server.module from test-coverage

* fix lint

* adjust tests for ldap person methods

* fix lint

* adjust ldpa person methods test again

* tests for ldap-client-svc

* renamed events, use aggregates instead of Dtos

* adjust tests for ldpa methods when using aggregates

* adjust tests for ldap again

* add test stuff for ldap

* adjust tests for LdapClientSvc to use mocks

* mock client in LdapClient and bind method

* use person referrer as uid for LDAP

* Wrapped a mutex around the LDAP-Calls since openldap doesn't seem to like parallel requests from the same client

* add SchuleKennungEindeutig specification

* renamed events classes

---------

Co-authored-by: Marvin Rode <[email protected]>
Co-authored-by: Kristoff Kiefer <[email protected]>
Co-authored-by: Kristoff Kiefer <[email protected]>
  • Loading branch information
4 people authored Jun 6, 2024
1 parent 9b01c04 commit f0cf28c
Show file tree
Hide file tree
Showing 40 changed files with 1,512 additions and 28 deletions.
2 changes: 1 addition & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ services:
volumes:
- ./config/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom
environment:
- LDAP_ORGANISATION=spsh-de
- LDAP_ORGANISATION=schule-sh-de
- LDAP_DOMAIN=schule-sh.de
- LDAP_BASE_DN=dc=schule-sh,dc=de
- LDAP_ADMIN_PASSWORD=admin
Expand Down
19 changes: 19 additions & 0 deletions docs/test-with-ldap-test-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Test with LDAP / LDAP Test Module

## Order of tests / dependencies

Because some entries in LDAP dependent on others, testing requires a little bit preparation.
Sometimes it makes sense to define a `beforeAll` for individual tests (describe or it).

Currently, for each method of `LdapClientService` for example a separate test is done in one separate file
and each single test case (describe or it) should be tested separately whether it returns the expected result.

## Waiting / timing issues

The class `LdapClient` is used as wrapper, mainly to be able to mock the injection of this class-type and mock every
call that would result in an *ADD* or *DEL* operation, because regardless of execution order and implicit waiting, sometimes
an organisation simply does not exist (yet), when e.g. a teacher should be inserted in/under the corresponding OU.

Currently, the tests which use `LdapTestModule` also import `DatabaseTestModule` and set up the database to ensure proper
wait for the LDAP test-container to start. In the future this should be replaced by a proper wait-strategy. Unfortunately at the moment
waiting for a simple health-check is not that easy.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@nestjs/swagger": "^7.3.1",
"@nestjs/terminus": "^10.2.3",
"@s3pweb/keycloak-admin-client-cjs": "^22.0.1",
"async-mutex": "^0.5.0",
"axios": "^1.6.8",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
Expand Down
2 changes: 1 addition & 1 deletion src/console/dbmigrate/db-migration-console-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('DbMigrateConsole', () => {

orm = module.get(MikroORM);
fs.rmSync('test-migrations', { recursive: true, force: true });
}, DEFAULT_TIMEOUT_FOR_TESTCONTAINERS);
}, 2 * DEFAULT_TIMEOUT_FOR_TESTCONTAINERS);

afterAll(async () => {
await orm.close();
Expand Down
314 changes: 314 additions & 0 deletions src/core/ldap/domain/ldap-client.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
import { EntityManager, MikroORM } from '@mikro-orm/core';
import { INestApplication } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import {
ConfigTestModule,
DatabaseTestModule,
DEFAULT_TIMEOUT_FOR_TESTCONTAINERS,
LdapTestModule,
MapperTestModule,
} from '../../../../test/utils/index.js';
import { GlobalValidationPipe } from '../../../shared/validation/global-validation.pipe.js';
import { LdapConfigModule } from '../ldap-config.module.js';
import { LdapModule } from '../ldap.module.js';
import { faker } from '@faker-js/faker';
import { OrganisationsTyp } from '../../../modules/organisation/domain/organisation.enums.js';
import { LdapClientService } from './ldap-client.service.js';
import { Organisation } from '../../../modules/organisation/domain/organisation.js';
import { Person } from '../../../modules/person/domain/person.js';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { LdapClient } from './ldap-client.js';
import { Client } from 'ldapts';

describe('LDAP Client Service', () => {
let app: INestApplication;
let module: TestingModule;
let orm: MikroORM;
let em: EntityManager;
let ldapClientService: LdapClientService;
let ldapClientMock: DeepMocked<LdapClient>;
let clientMock: DeepMocked<Client>;

let organisation: Organisation<true>;
let invalidOrganisation: Organisation<true>;
let person: Person<true>;
let personWithoutReferrer: Person<true>;
let ouKennung: string;

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
ConfigTestModule,
DatabaseTestModule.forRoot({ isDatabaseRequired: true }),
LdapModule,
MapperTestModule,
],
providers: [
{
provide: APP_PIPE,
useClass: GlobalValidationPipe,
},
],
})
.overrideModule(LdapConfigModule)
.useModule(LdapTestModule.forRoot({ isLdapRequired: true }))
.overrideProvider(LdapClient)
.useValue(createMock<LdapClient>())
.compile();

orm = module.get(MikroORM);
em = module.get(EntityManager);
ldapClientService = module.get(LdapClientService);
ldapClientMock = module.get(LdapClient);
clientMock = createMock<Client>();

ouKennung = faker.string.numeric({ length: 7 });
organisation = Organisation.construct(
faker.string.uuid(),
faker.date.past(),
faker.date.recent(),
undefined,
undefined,
ouKennung,
faker.company.name(),
undefined,
undefined,
OrganisationsTyp.SCHULE,
undefined,
);
invalidOrganisation = {
id: faker.string.uuid(),
name: faker.company.name(),
kennung: undefined,
typ: OrganisationsTyp.SCHULE,
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
};
person = Person.construct(
faker.string.uuid(),
faker.date.past(),
faker.date.recent(),
faker.person.lastName(),
faker.person.firstName(),
'1',
faker.lorem.word(),
undefined,
faker.string.uuid(),
);
personWithoutReferrer = Person.construct(
faker.string.uuid(),
faker.date.past(),
faker.date.recent(),
faker.person.lastName(),
faker.person.firstName(),
'1',
faker.lorem.word(),
undefined,
undefined,
);

//currently only used to wait for the LDAP container, because setupDatabase() is blocking
await DatabaseTestModule.setupDatabase(module.get(MikroORM));
app = module.createNestApplication();
await app.init();
}, DEFAULT_TIMEOUT_FOR_TESTCONTAINERS);

afterAll(async () => {
await DatabaseTestModule.clearDatabase(orm);
await orm.close();
await app.close();
});

beforeEach(async () => {
jest.resetAllMocks();
await DatabaseTestModule.clearDatabase(orm);
});

it('should be defined', () => {
expect(em).toBeDefined();
});

describe('bind', () => {
describe('when error is thrown inside', () => {
it('should return error result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockRejectedValueOnce(new Error());
clientMock.add.mockResolvedValueOnce();
return clientMock;
});

const result: Result<Organisation<true>> = await ldapClientService.createOrganisation(organisation);

expect(result.ok).toBeFalsy();
});
});
});

describe('creation', () => {
describe('organisation', () => {
it('when called with valid organisation should return truthy result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.add.mockResolvedValueOnce();
return clientMock;
});

const result: Result<Organisation<true>> = await ldapClientService.createOrganisation(organisation);

expect(result.ok).toBeTruthy();
});

it('when called with organisation without kennung should return error result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Organisation<true>> =
await ldapClientService.createOrganisation(invalidOrganisation);

expect(result.ok).toBeFalsy();
});
});

describe('lehrer', () => {
it('when called with valid person and organisation should return truthy result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Person<true>> = await ldapClientService.createLehrer(person, organisation);

expect(result.ok).toBeTruthy();
});

it('when called with valid person and an organisation without kennung should return error result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Person<true>> = await ldapClientService.createLehrer(person, invalidOrganisation);

expect(result.ok).toBeFalsy();
});

it('when called with person without referrer should return error result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Person<true>> = await ldapClientService.createLehrer(
personWithoutReferrer,
organisation,
);

expect(result.ok).toBeFalsy();
});

it('when bind returns error', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockRejectedValueOnce(new Error());
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Person<true>> = await ldapClientService.createLehrer(person, organisation);

expect(result.ok).toBeFalsy();
});
});
});

describe('deletion', () => {
describe('delete lehrer', () => {
it('should return truthy result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.del.mockResolvedValueOnce();
return clientMock;
});

const result: Result<Person<true>> = await ldapClientService.deleteLehrer(person, organisation);

expect(result.ok).toBeTruthy();
});

it('when called with valid person and an organisation without kennung should return error result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Person<true>> = await ldapClientService.deleteLehrer(person, invalidOrganisation);

expect(result.ok).toBeFalsy();
});

it('when called with person without referrer should return error result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Person<true>> = await ldapClientService.deleteLehrer(
personWithoutReferrer,
organisation,
);

expect(result.ok).toBeFalsy();
});

it('when bind returns error', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockRejectedValueOnce(new Error());
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Person<true>> = await ldapClientService.deleteLehrer(person, organisation);

expect(result.ok).toBeFalsy();
});
});

describe('delete organisation', () => {
it('when called with valid organisation should return truthy result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.del.mockResolvedValueOnce();
return clientMock;
});

const result: Result<Organisation<true>> = await ldapClientService.deleteOrganisation(organisation);

expect(result.ok).toBeTruthy();
});

it('when called with organisation without kennung should return error result', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockResolvedValueOnce();
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Organisation<true>> =
await ldapClientService.deleteOrganisation(invalidOrganisation);

expect(result.ok).toBeFalsy();
});

it('when bind returns error', async () => {
ldapClientMock.getClient.mockImplementation(() => {
clientMock.bind.mockRejectedValueOnce(new Error());
clientMock.add.mockResolvedValueOnce();
return clientMock;
});
const result: Result<Organisation<true>> =
await ldapClientService.deleteOrganisation(invalidOrganisation);

expect(result.ok).toBeFalsy();
});
});
});
});
Loading

0 comments on commit f0cf28c

Please sign in to comment.