Skip to content

Commit

Permalink
EW-561 code coverage, better searching and pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
psachmann committed Oct 17, 2023
1 parent 6f496ff commit 99b99ca
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 68 deletions.
4 changes: 4 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ declare type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error:
declare type Persisted<T, WasPersisted extends boolean> = WasPersisted extends true ? T : Option<T>;

declare type Counted<T> = [T[], number];

declare type Findable<T> = {
[P in keyof T]?: T[P] extends string ? string | RegExp : T[P];
};
21 changes: 14 additions & 7 deletions src/modules/person/api/person.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PersonenkontextUc } from './personenkontext.uc.js';
import { CreatePersonenkontextBodyParams } from './create-personenkontext.body.params.js';
import { CreatedPersonenkontextDto } from './created-personenkontext.dto.js';
import { Jahrgangsstufe, Personenstatus, Rolle } from '../domain/personenkontext.enums.js';
import { PagedResponse } from '../../../shared/paging/index.js';

describe('PersonController', () => {
let module: TestingModule;
Expand Down Expand Up @@ -153,7 +154,6 @@ describe('PersonController', () => {
lokalisierung: '',
vertrauensstufe: TrustLevel.TRUSTED,
};

const person2: PersonResponse = {
id: faker.string.uuid(),
name: {
Expand All @@ -167,20 +167,27 @@ describe('PersonController', () => {
lokalisierung: '',
vertrauensstufe: TrustLevel.TRUSTED,
};

const mockPersondatensatz1: PersonenDatensatz = {
person: person1,
};
const mockPersondatensatz2: PersonenDatensatz = {
person: person2,
};
const mockPersondatensatz: PersonenDatensatz[] = [mockPersondatensatz1, mockPersondatensatz2];
const mockPersondatensatz: PagedResponse<PersonenDatensatz> = new PagedResponse({
offset: 0,
limit: 10,
total: 2,
items: [mockPersondatensatz1, mockPersondatensatz2],
});

personUcMock.findAll.mockResolvedValue(mockPersondatensatz);
const result: PersonenDatensatz[] = await personController.findPersons(queryParams);

const result: PagedResponse<PersonenDatensatz> = await personController.findPersons(queryParams);

expect(personUcMock.findAll).toHaveBeenCalledTimes(1);
expect(result.at(0)?.person.referrer).toEqual(queryParams.referrer);
expect(result.at(0)?.person.name.vorname).toEqual(queryParams.vorname);
expect(result.at(0)?.person.name.familienname).toEqual(queryParams.familienname);
expect(result.items.at(0)?.person.referrer).toEqual(queryParams.referrer);
expect(result.items.at(0)?.person.name.vorname).toEqual(queryParams.vorname);
expect(result.items.at(0)?.person.name.familienname).toEqual(queryParams.familienname);
expect(result).toEqual(mockPersondatensatz);
});
});
Expand Down
4 changes: 2 additions & 2 deletions src/modules/person/api/person.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class PersonController {
}

@Get()
@ApiOkResponse({ description: 'The persons were successfully returned.' })
@ApiOkResponse({ description: 'The persons were successfully returned.', type: Array<PersonenDatensatz> })
@ApiUnauthorizedResponse({ description: 'Not authorized to get persons.' })
@ApiForbiddenResponse({ description: 'Insufficient permissions to get persons.' })
@ApiInternalServerErrorResponse({ description: 'Internal server error while getting all persons.' })
Expand All @@ -96,7 +96,7 @@ export class PersonController {
PersonenQueryParams,
FindPersonDatensatzDTO,
);
const persons: Paged<PersonenDatensatz> = await this.uc.findAll(personDatensatzDTO);
const persons: Paged<PersonenDatensatz> = await this.personUc.findAll(personDatensatzDTO);
const response: PagedResponse<PersonenDatensatz> = new PagedResponse(persons);

return response;
Expand Down
29 changes: 19 additions & 10 deletions src/modules/person/api/person.uc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { faker } from '@faker-js/faker';
import { PersonDo } from '../domain/person.do.js';
import { PersonenDatensatz } from './personendatensatz.js';
import { KeycloakUserService } from '../../keycloak-administration/index.js';
import { Paged } from '../../../shared/paging/index.js';

describe('PersonUc', () => {
let module: TestingModule;
Expand Down Expand Up @@ -145,21 +146,29 @@ describe('PersonUc', () => {
it('should find all persons that match with query param', async () => {
const firstPerson: PersonDo<true> = DoFactory.createPerson(true);
const secondPerson: PersonDo<true> = DoFactory.createPerson(true);
const persons: PersonDo<true>[] = [firstPerson, secondPerson];
const persons: Paged<PersonDo<true>> = {
offset: 0,
limit: 10,
total: 2,
items: [firstPerson, secondPerson],
};

personServiceMock.findAllPersons.mockResolvedValue(persons);
const result: PersonenDatensatz[] = await personUc.findAll(personDTO);
expect(result).toHaveLength(2);
expect(result.at(0)?.person.name.vorname).toEqual(firstPerson.firstName);
expect(result.at(0)?.person.name.familienname).toEqual(firstPerson.lastName);
expect(result.at(1)?.person.name.vorname).toEqual(secondPerson.firstName);
expect(result.at(1)?.person.name.familienname).toEqual(secondPerson.lastName);

const result: Paged<PersonenDatensatz> = await personUc.findAll(personDTO);

expect(result.items).toHaveLength(2);
expect(result.items.at(0)?.person.name.vorname).toEqual(firstPerson.firstName);
expect(result.items.at(0)?.person.name.familienname).toEqual(firstPerson.lastName);
expect(result.items.at(1)?.person.name.vorname).toEqual(secondPerson.firstName);
expect(result.items.at(1)?.person.name.familienname).toEqual(secondPerson.lastName);
});

it('should return an empty array when no matching persons are found', async () => {
const emptyResult: PersonDo<true>[] = [];
const emptyResult: Paged<PersonDo<true>> = { offset: 0, limit: 0, total: 0, items: [] };
personServiceMock.findAllPersons.mockResolvedValue(emptyResult);
const result: PersonenDatensatz[] = await personUc.findAll(personDTO);
expect(result).toEqual([]);
const result: Paged<PersonenDatensatz> = await personUc.findAll(personDTO);
expect(result.items).toEqual([]);
});
});
});
2 changes: 1 addition & 1 deletion src/modules/person/api/person.uc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export class PersonUc {
public async findAll(personDto: FindPersonDatensatzDto): Promise<Paged<PersonenDatensatz>> {
const personDo: PersonDo<false> = this.mapper.map(personDto, FindPersonDatensatzDto, PersonDo);
const result: Paged<PersonDo<true>> = await this.personService.findAllPersons(
personDo,
personDto.offset,
personDto.limit,
personDo,
);

if (result.total === 0) {
Expand Down
30 changes: 17 additions & 13 deletions src/modules/person/domain/person.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DoFactory } from '../../../../test/utils/do-factory.js';
import { PersonRepo } from '../persistence/person.repo.js';
import { PersonDo } from './person.do.js';
import { PersonService } from './person.service.js';
import { Paged } from '../../../shared/paging/index.js';

describe('PersonService', () => {
let module: TestingModule;
Expand Down Expand Up @@ -123,26 +124,29 @@ describe('PersonService', () => {

describe('findAllPersons', () => {
it('should get all persons that match', async () => {
const firstPerson: PersonDo<false> = DoFactory.createPerson(false);
const secondPerson: PersonDo<false> = DoFactory.createPerson(false);
const persons: PersonDo<true>[] = [
firstPerson as unknown as PersonDo<true>,
secondPerson as unknown as PersonDo<true>,
];
personRepoMock.findAll.mockResolvedValue(persons);
const firstPerson: PersonDo<true> = DoFactory.createPerson(true);
const secondPerson: PersonDo<true> = DoFactory.createPerson(true);
const persons: Counted<PersonDo<true>> = [[firstPerson, secondPerson], 2];

personRepoMock.findBy.mockResolvedValue(persons);
mapperMock.map.mockReturnValue(persons as unknown as Dictionary<unknown>);

const personDoWithQueryParam: PersonDo<false> = DoFactory.createPerson(false);
const result: PersonDo<true>[] = await personService.findAllPersons(personDoWithQueryParam);
expect(result).toHaveLength(2);
const result: Paged<PersonDo<true>> = await personService.findAllPersons(personDoWithQueryParam, 0, 10);

expect(result.items).toHaveLength(2);
});

it('should return an empty list of persons ', async () => {
const person: PersonDo<false> = DoFactory.createPerson(false);
personRepoMock.findAll.mockResolvedValue([]);

personRepoMock.findBy.mockResolvedValue([[], 0]);
mapperMock.map.mockReturnValue(person as unknown as Dictionary<unknown>);
const result: PersonDo<true>[] = await personService.findAllPersons(person);
expect(result).toBeInstanceOf(Array);
expect(result).toHaveLength(0);

const result: Paged<PersonDo<true>> = await personService.findAllPersons(person);

expect(result.items).toBeInstanceOf(Array);
expect(result.items).toHaveLength(0);
});
});
});
4 changes: 2 additions & 2 deletions src/modules/person/domain/person.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export class PersonService {
}

public async findAllPersons(
offset: Option<number>,
limit: Option<number>,
personDo: Partial<PersonDo<false>>,
offset?: number,
limit?: number,
): Promise<Paged<PersonDo<true>>> {
const scope: PersonScope = new PersonScope()
.findBy({
Expand Down
62 changes: 39 additions & 23 deletions src/modules/person/persistence/person.repo.integration-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { PersonDo } from '../domain/person.do.js';
import { PersonPersistenceMapperProfile } from './person-persistence.mapper.profile.js';
import { PersonEntity } from './person.entity.js';
import { PersonRepo } from './person.repo.js';
import { PersonScope } from './person.scope.js';
import { ScopeOperator } from '../../../shared/persistence/scope.enums.js';

describe('PersonRepo', () => {
let module: TestingModule;
Expand Down Expand Up @@ -132,31 +134,45 @@ describe('PersonRepo', () => {
});

describe('findAll', () => {
it('should find all persons from database', async () => {
const props: Partial<PersonDo<false>> = {
referrer: 'referrer_value',
firstName: 'first name',
lastName: 'last name',
isInformationBlocked: false,
};
const personDo1: PersonDo<false> = DoFactory.createPerson(false, props);
const personDo2: PersonDo<false> = DoFactory.createPerson(false, props);
await em.persistAndFlush(mapper.map(personDo1, PersonDo, PersonEntity));
await em.persistAndFlush(mapper.map(personDo2, PersonDo, PersonEntity));
const personDoFromQueryParam: PersonDo<false> = DoFactory.createPerson(false, props);
const result: PersonDo<true>[] = await sut.findAll(personDoFromQueryParam);
expect(result).not.toBeNull();
expect(result).toHaveLength(2);
await expect(em.find(PersonEntity, {})).resolves.toHaveLength(2);
describe('when persons match the query', () => {
it('should return all matching persons', async () => {
const props: Partial<PersonDo<false>> = {
referrer: 'referrer_value',
firstName: 'first name',
lastName: 'last name',
isInformationBlocked: false,
};
const personDo1: PersonDo<false> = DoFactory.createPerson(false, props);
const personDo2: PersonDo<false> = DoFactory.createPerson(false, props);

await em.persistAndFlush(mapper.map(personDo1, PersonDo, PersonEntity));
await em.persistAndFlush(mapper.map(personDo2, PersonDo, PersonEntity));

const [result]: Counted<PersonDo<true>> = await sut.findBy(
new PersonScope().findBy(
{
firstName: props.firstName,
lastName: props.lastName,
},
ScopeOperator.AND,
),
);

expect(result).not.toBeNull();
expect(result).toHaveLength(2);
await expect(em.find(PersonEntity, {})).resolves.toHaveLength(2);
});
});

it('should return an empty list', async () => {
const props: Partial<PersonDo<false>> = {};
const personDoFromQueryParam: PersonDo<false> = DoFactory.createPerson(false, props);
const result: PersonDo<true>[] = await sut.findAll(personDoFromQueryParam);
expect(result).not.toBeNull();
expect(result).toHaveLength(0);
await expect(em.find(PersonEntity, {})).resolves.toHaveLength(0);
describe('when no person matches the query', () => {
it('should return an empty list', async () => {
const [result]: Counted<PersonDo<true>> = await sut.findBy(new PersonScope());

expect(result).not.toBeNull();
expect(result).toHaveLength(0);

await expect(em.find(PersonEntity, {})).resolves.toHaveLength(0);
});
});
});
});
6 changes: 3 additions & 3 deletions src/modules/person/persistence/person.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ export class PersonRepo {
}

public async findById(id: string): Promise<Option<PersonDo<true>>> {
const person: Option<PersonEntity> = await this.em.findOne(PersonEntity, { id });
const person: Option<PersonEntity> = await this.em.findOne(this.entityName, { id });
if (person) {
return this.mapper.map(person, PersonEntity, PersonDo);
}
return null;
}

public async findByReferrer(referrer: string): Promise<Option<PersonDo<true>>> {
const person: Option<PersonEntity> = await this.em.findOne(PersonEntity, { referrer });
const person: Option<PersonEntity> = await this.em.findOne(this.entityName, { referrer });
if (person) {
return this.mapper.map(person, PersonEntity, PersonDo);
}
Expand Down Expand Up @@ -63,7 +63,7 @@ export class PersonRepo {
}

private async update(personDo: PersonDo<true>): Promise<PersonDo<true>> {
let person: Option<Loaded<PersonEntity, never>> = await this.em.findOne(PersonEntity, { id: personDo.id });
let person: Option<Loaded<PersonEntity, never>> = await this.em.findOne(this.entityName, { id: personDo.id });
if (person) {
person.assign(this.mapper.map(personDo, PersonDo, PersonEntity));
} else {
Expand Down
61 changes: 61 additions & 0 deletions src/modules/person/persistence/person.scope.integration-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Mapper } from '@automapper/core';
import { getMapperToken } from '@automapper/nestjs';
import { MikroORM } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/postgresql';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigTestModule, DatabaseTestModule, DoFactory, MapperTestModule } from '../../../../test/utils/index.js';
import { PersonDo } from '../domain/person.do.js';
import { PersonPersistenceMapperProfile } from './person-persistence.mapper.profile.js';
import { PersonEntity } from './person.entity.js';
import { PersonScope } from './person.scope.js';
import { ScopeOrder } from '../../../shared/persistence/scope.enums.js';

describe('PersonScope', () => {
let module: TestingModule;
let orm: MikroORM;
let em: EntityManager;
let mapper: Mapper;

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [ConfigTestModule, DatabaseTestModule.forRoot({ isDatabaseRequired: true }), MapperTestModule],
providers: [PersonPersistenceMapperProfile],
}).compile();
orm = module.get(MikroORM);
em = module.get(EntityManager);
mapper = module.get(getMapperToken());

await DatabaseTestModule.setupDatabase(orm);
}, 30 * 1_000);

afterAll(async () => {
await module.close();
}, 30 * 1_000);

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

describe('findBy', () => {
describe('when filtering for persons', () => {
beforeEach(async () => {
const persons: PersonEntity[] = Array.from({ length: 110 }, (_v: unknown, i: number) =>
mapper.map(DoFactory.createPerson(false, { firstName: `John #${i}` }), PersonDo, PersonEntity),
);

await em.persistAndFlush(persons);
});

it('should return found persons', async () => {
const scope: PersonScope = new PersonScope()
.findBy({ firstName: new RegExp('John #1') })
.sortBy('firstName', ScopeOrder.ASC)
.paged(10, 10);
const [persons, total]: Counted<PersonEntity> = await scope.executeQuery(em);

expect(total).toBe(21);
expect(persons).toHaveLength(10);
});
});
});
});
8 changes: 4 additions & 4 deletions src/modules/person/persistence/person.scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import { ScopeBase, ScopeOperator } from '../../../shared/persistence/index.js';
import { PersonEntity } from './person.entity.js';

type FindProps = {
firstName?: string;
lastName?: string;
birthDate?: Date;
firstName: string;
lastName: string;
birthDate: Date;
};

export class PersonScope extends ScopeBase<PersonEntity> {
public override get entityName(): EntityName<PersonEntity> {
return PersonEntity;
}

public findBy(findProps: FindProps, operator: ScopeOperator = ScopeOperator.AND): this {
public findBy(findProps: Findable<FindProps>, operator: ScopeOperator = ScopeOperator.AND): this {
this.findByInternal(
{
firstName: findProps.firstName,
Expand Down
Loading

0 comments on commit 99b99ca

Please sign in to comment.