Skip to content

Commit

Permalink
Merge branch 'main' into SPSH-1553
Browse files Browse the repository at this point in the history
  • Loading branch information
YoussefBouch authored Dec 16, 2024
2 parents 7137875 + ee26a0d commit 95663d4
Show file tree
Hide file tree
Showing 16 changed files with 351 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { RolleRepo } from '../../rolle/repo/rolle.repo.js';
import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js';
import { KeycloakConfig } from '../../../shared/config/keycloak.config.js';
import { KeycloakUserService } from '../../keycloak-administration/index.js';
import { TimeLimitOccasion } from '../../person/domain/time-limit-occasion.enums.js';
import PersonTimeLimitService from '../../person/domain/person-time-limit-info.service.js';

describe('AuthenticationController', () => {
let module: TestingModule;
Expand All @@ -38,7 +40,7 @@ describe('AuthenticationController', () => {
let rolleRepoMock: DeepMocked<RolleRepo>;
const keycloakUserServiceMock: DeepMocked<KeycloakUserService> = createMock<KeycloakUserService>();
let keyCloakConfig: KeycloakConfig;

const personTimeLimitServiceMock: DeepMocked<PersonTimeLimitService> = createMock<PersonTimeLimitService>();
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
Expand Down Expand Up @@ -76,6 +78,10 @@ describe('AuthenticationController', () => {
provide: KeycloakUserService,
useValue: keycloakUserServiceMock,
},
{
provide: PersonTimeLimitService,
useValue: personTimeLimitServiceMock,
},
],
}).compile();

Expand Down Expand Up @@ -275,6 +281,13 @@ describe('AuthenticationController', () => {
value: person.updatedAt,
});

personTimeLimitServiceMock.getPersonTimeLimitInfo.mockResolvedValueOnce([
{
occasion: TimeLimitOccasion.KOPERS,
deadline: faker.date.future(),
},
]);

const requestMock: Request = setupRequest();
const result: UserinfoResponse = await authController.info(permissions, requestMock);

Expand Down
12 changes: 12 additions & 0 deletions src/modules/authentication/api/authentication.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import { AuthenticationExceptionFilter } from './authentication-exception-filter
import { KeycloakUserService } from '../../keycloak-administration/index.js';
import { DomainError } from '../../../shared/error/domain.error.js';
import { getLowestStepUpLevel } from '../passport/oidc.strategy.js';
import { PersonTimeLimitInfo } from '../../person/domain/person-time-limit-info.js';
import { PersonTimeLimitInfoResponse } from './person-time-limit-info.reponse.js';
import PersonTimeLimitService from '../../person/domain/person-time-limit-info.service.js';

@UseFilters(new AuthenticationExceptionFilter())
@ApiTags('auth')
Expand All @@ -47,6 +50,7 @@ export class AuthenticationController {
@Inject(OIDC_CLIENT) private client: Client,
private readonly logger: ClassLogger,
private keycloakUserService: KeycloakUserService,
private readonly personTimeLimitService: PersonTimeLimitService,
) {
const frontendConfig: FrontendConfig = configService.getOrThrow<FrontendConfig>('FRONTEND');
const keycloakConfig: KeycloakConfig = configService.getOrThrow<KeycloakConfig>('KEYCLOAK');
Expand Down Expand Up @@ -132,10 +136,18 @@ export class AuthenticationController {
if (lastPasswordChange.ok) userinfoExtension.password_updated_at = lastPasswordChange.value;
}

const timeLimitInfos: PersonTimeLimitInfo[] = await this.personTimeLimitService.getPersonTimeLimitInfo(
permissions.personFields.id,
);
const timeLimitInfosResponse: PersonTimeLimitInfoResponse[] = timeLimitInfos.map(
(info: PersonTimeLimitInfo) => new PersonTimeLimitInfoResponse(info.occasion, info.deadline),
);

return new UserinfoResponse(
permissions,
rolleFieldsResponse,
req.passportUser?.stepUpLevel ?? getLowestStepUpLevel(),
timeLimitInfosResponse,
userinfoExtension,
);
}
Expand Down
15 changes: 15 additions & 0 deletions src/modules/authentication/api/person-time-limit-info.reponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { TimeLimitOccasion } from '../../person/domain/time-limit-occasion.enums.js';

export class PersonTimeLimitInfoResponse {
@ApiProperty()
public occasion: TimeLimitOccasion;

@ApiProperty()
public deadline: string;

public constructor(occasion: TimeLimitOccasion, deadline: Date) {
this.occasion = occasion;
this.deadline = deadline.toISOString();
}
}
12 changes: 11 additions & 1 deletion src/modules/authentication/api/userinfo.response.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { RolleRepo } from '../../rolle/repo/rolle.repo.js';
import { PersonenkontextRolleFieldsResponse } from './personen-kontext-rolle-fields.response.js';
import { createMock } from '@golevelup/ts-jest';
import { StepUpLevel } from '../passport/oidc.strategy.js';
import { PersonTimeLimitInfoResponse } from './person-time-limit-info.reponse.js';
import { TimeLimitOccasion } from '../../person/domain/time-limit-occasion.enums.js';

describe('UserinfoResponse', () => {
const permissions: PersonPermissions = new PersonPermissions(
Expand All @@ -22,8 +24,15 @@ describe('UserinfoResponse', () => {
rolle: { systemrechte: [faker.string.alpha()], serviceProviderIds: [faker.string.uuid()] },
};

const personTimeLimtit: PersonTimeLimitInfoResponse = {
occasion: TimeLimitOccasion.KOPERS,
deadline: faker.date.future().toISOString(),
};

it('constructs the object without optional extension', () => {
const userinfoResponse: UserinfoResponse = new UserinfoResponse(permissions, [pk], StepUpLevel.SILVER);
const userinfoResponse: UserinfoResponse = new UserinfoResponse(permissions, [pk], StepUpLevel.SILVER, [
personTimeLimtit,
]);
expect(userinfoResponse).toBeDefined();
expect(userinfoResponse.password_updated_at).toBeUndefined();
});
Expand All @@ -34,6 +43,7 @@ describe('UserinfoResponse', () => {
permissions,
[pk],
StepUpLevel.SILVER,
[personTimeLimtit],
extension,
);
expect(userinfoResponse).toBeDefined();
Expand Down
6 changes: 6 additions & 0 deletions src/modules/authentication/api/userinfo.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { PersonPermissions } from '../domain/person-permissions.js';
import { PersonenkontextRolleFieldsResponse } from './personen-kontext-rolle-fields.response.js';
import { StepUpLevel } from '../passport/oidc.strategy.js';
import { PersonTimeLimitInfoResponse } from './person-time-limit-info.reponse.js';

export type UserinfoExtension = {
password_updated_at?: Date;
Expand Down Expand Up @@ -74,10 +75,14 @@ export class UserinfoResponse {
@ApiProperty({ nullable: false })
public acr: StepUpLevel;

@ApiProperty({ type: PersonTimeLimitInfoResponse, isArray: true })
public timeLimits: PersonTimeLimitInfoResponse[];

public constructor(
info: PersonPermissions,
personenkontexte: PersonenkontextRolleFieldsResponse[],
acr: StepUpLevel,
timeLimits: PersonTimeLimitInfoResponse[],
extension?: UserinfoExtension,
) {
this.sub = info.personFields.keycloakUserId!;
Expand All @@ -93,5 +98,6 @@ export class UserinfoResponse {
this.personenkontexte = personenkontexte;
this.password_updated_at = extension?.password_updated_at?.toISOString();
this.acr = acr;
this.timeLimits = timeLimits;
}
}
1 change: 1 addition & 0 deletions src/modules/person/api/person.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export class PersonController {
const personEmailResponse: Option<PersonEmailResponse> = await this.emailRepo.getEmailAddressAndStatusForPerson(
personResult.value,
);

const response: PersonendatensatzResponse = new PersonendatensatzResponse(
personResult.value,
false,
Expand Down
120 changes: 120 additions & 0 deletions src/modules/person/domain/person-time-limit-info.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { TestingModule, Test } from '@nestjs/testing';
import { PersonRepository } from '../../person/persistence/person.repository.js';
import PersonTimeLimitService from './person-time-limit-info.service.js';
import { DBiamPersonenkontextService } from '../../personenkontext/domain/dbiam-personenkontext.service.js';
import { Person } from '../../person/domain/person.js';
import { DoFactory } from '../../../../test/utils/do-factory.js';
import { TimeLimitOccasion } from '../domain/time-limit-occasion.enums.js';
import { Personenkontext } from '../../personenkontext/domain/personenkontext.js';
import { PersonTimeLimitInfo } from './person-time-limit-info.js';
import { KOPERS_DEADLINE_IN_DAYS } from './person-time-limit.js';

describe('PersonTimeLimitService', () => {
let module: TestingModule;
let sut: PersonTimeLimitService;
let personRepoMock: DeepMocked<PersonRepository>;
let dBiamPersonenkontextServiceMock: DeepMocked<DBiamPersonenkontextService>;

beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
PersonTimeLimitService,
{
provide: PersonRepository,
useValue: createMock<PersonRepository>(),
},
{
provide: DBiamPersonenkontextService,
useValue: createMock<DBiamPersonenkontextService>(),
},
],
}).compile();
sut = module.get(PersonTimeLimitService);
personRepoMock = module.get(PersonRepository);
dBiamPersonenkontextServiceMock = module.get(DBiamPersonenkontextService);
});

afterAll(async () => {
await module.close();
});

beforeEach(() => {
jest.resetAllMocks();
});

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

describe('getPersonTimeLimitInfo', () => {
it('should return PersonTimeLimitInfo array', async () => {
const person: Person<true> = DoFactory.createPerson(true);
person.personalnummer = undefined;
personRepoMock.findById.mockResolvedValue(person);

const pesonenkontext: Personenkontext<true> = DoFactory.createPersonenkontext(true);
dBiamPersonenkontextServiceMock.getKopersPersonenkontexte.mockResolvedValue([pesonenkontext]);

const result: PersonTimeLimitInfo[] = await sut.getPersonTimeLimitInfo(person.id);

const expectedDeadline: Date = new Date(pesonenkontext.createdAt);
expectedDeadline.setDate(expectedDeadline.getDate() + KOPERS_DEADLINE_IN_DAYS);

expect(result).toEqual<PersonTimeLimitInfo[]>([
{
occasion: TimeLimitOccasion.KOPERS,
deadline: expectedDeadline,
},
]);
});

it.each([
{
personenkontextDates: ['2021-01-02', '2021-01-01'],
expectedDate: '2021-01-01',
},
{
personenkontextDates: ['2021-01-01', '2021-01-02'],
expectedDate: '2021-01-01',
},
])(
'should return PersonTimeLimitInfo array with earliest Koperslock',
async ({
personenkontextDates,
expectedDate,
}: {
personenkontextDates: string[];
expectedDate: string;
}) => {
const person: Person<true> = DoFactory.createPerson(true);
person.personalnummer = undefined;
personRepoMock.findById.mockResolvedValue(person);

const personenkontexte: Personenkontext<true>[] = personenkontextDates.map((date: string) =>
DoFactory.createPersonenkontext(true, { createdAt: new Date(date) }),
);
dBiamPersonenkontextServiceMock.getKopersPersonenkontexte.mockResolvedValue(personenkontexte);

const result: PersonTimeLimitInfo[] = await sut.getPersonTimeLimitInfo(person.id);

const expectedDeadline: Date = new Date(expectedDate);
expectedDeadline.setDate(expectedDeadline.getDate() + KOPERS_DEADLINE_IN_DAYS);

expect(result).toEqual<PersonTimeLimitInfo[]>([
{
occasion: TimeLimitOccasion.KOPERS,
deadline: expectedDeadline,
},
]);
},
);

it('should return empty array when person isnt found ', async () => {
personRepoMock.findById.mockResolvedValue(null);
const result: PersonTimeLimitInfo[] = await sut.getPersonTimeLimitInfo('');

expect(result).toEqual<PersonTimeLimitInfo[]>([]);
});
});
});
39 changes: 39 additions & 0 deletions src/modules/person/domain/person-time-limit-info.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { Personenkontext } from '../../personenkontext/domain/personenkontext.js';
import { DBiamPersonenkontextService } from '../../personenkontext/domain/dbiam-personenkontext.service.js';
import { PersonRepository } from '../../person/persistence/person.repository.js';
import { Person } from '../../person/domain/person.js';
import { PersonTimeLimitInfo } from './person-time-limit-info.js';
import { TimeLimitOccasion } from './time-limit-occasion.enums.js';
import { KOPERS_DEADLINE_IN_DAYS } from './person-time-limit.js';

@Injectable()
export default class PersonTimeLimitService {
public constructor(
private readonly personRepository: PersonRepository,
private readonly dBiamPersonenkontextService: DBiamPersonenkontextService,
) {}

public async getPersonTimeLimitInfo(personId: string): Promise<PersonTimeLimitInfo[]> {
const person: Option<Person<true>> = await this.personRepository.findById(personId);
if (!person) {
return [];
}
const lockInfos: PersonTimeLimitInfo[] = [];
if (!person.personalnummer) {
const kopersKontexte: Personenkontext<true>[] =
await this.dBiamPersonenkontextService.getKopersPersonenkontexte(person.id);
if (kopersKontexte.length > 0) {
const earliestKopersKontext: Personenkontext<true> = kopersKontexte.reduce(
(prev: Personenkontext<true>, current: Personenkontext<true>) =>
prev.createdAt < current.createdAt ? prev : current,
kopersKontexte[0]!,
);
const kopersdeadline: Date = new Date(earliestKopersKontext.createdAt);
kopersdeadline.setDate(kopersdeadline.getDate() + KOPERS_DEADLINE_IN_DAYS);
lockInfos.push(new PersonTimeLimitInfo(TimeLimitOccasion.KOPERS, kopersdeadline));
}
}
return lockInfos;
}
}
8 changes: 8 additions & 0 deletions src/modules/person/domain/person-time-limit-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { TimeLimitOccasion } from './time-limit-occasion.enums.js';

export class PersonTimeLimitInfo {
public constructor(
public readonly occasion: TimeLimitOccasion,
public readonly deadline: Date,
) {}
}
1 change: 1 addition & 0 deletions src/modules/person/domain/person-time-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const KOPERS_DEADLINE_IN_DAYS: number = 56;
3 changes: 3 additions & 0 deletions src/modules/person/domain/time-limit-occasion.enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum TimeLimitOccasion {
KOPERS = 'KOPERS',
}
5 changes: 3 additions & 2 deletions src/modules/person/persistence/person.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { PersonalNummerForPersonWithTrailingSpaceError } from '../domain/persona
import { VornameForPersonWithTrailingSpaceError } from '../domain/vorname-with-trailing-space.error.js';
import { SystemConfig } from '../../../shared/config/system.config.js';
import { UserLock } from '../../keycloak-administration/domain/user-lock.js';
import { KOPERS_DEADLINE_IN_DAYS } from '../domain/person-time-limit.js';

/**
* Return email-address for person, if an enabled email-address exists, return it.
Expand Down Expand Up @@ -762,15 +763,15 @@ export class PersonRepository {

public async getKoPersUserLockList(): Promise<[PersonID, string][]> {
const daysAgo: Date = new Date();
daysAgo.setDate(daysAgo.getDate() - 56);
daysAgo.setDate(daysAgo.getDate() - KOPERS_DEADLINE_IN_DAYS);

const filters: QBFilterQuery<PersonEntity> = {
$and: [
{ personalnummer: { $eq: null } },
{
personenKontexte: {
$some: {
createdAt: { $lte: daysAgo }, // Check that createdAt is older than 56 days
createdAt: { $lte: daysAgo }, // Check that createdAt is older than KOPERS_DEADLINE_IN_DAYS
rolleId: {
merkmale: { merkmal: RollenMerkmal.KOPERS_PFLICHT },
},
Expand Down
Loading

0 comments on commit 95663d4

Please sign in to comment.