Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spsh 1327 #832

Merged
merged 15 commits into from
Dec 16, 2024
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
Loading