diff --git a/apps/server/src/modules/authentication/index.ts b/apps/server/src/modules/authentication/index.ts index 59e749c7abc..3a3ea0c2755 100644 --- a/apps/server/src/modules/authentication/index.ts +++ b/apps/server/src/modules/authentication/index.ts @@ -1,3 +1,4 @@ export { ICurrentUser } from './interface'; export { JWT, CurrentUser, Authenticate } from './decorator'; export { AuthenticationModule } from './authentication.module'; +export { AuthenticationService } from './services'; diff --git a/apps/server/src/modules/authentication/services/index.ts b/apps/server/src/modules/authentication/services/index.ts new file mode 100644 index 00000000000..45277bbf1c5 --- /dev/null +++ b/apps/server/src/modules/authentication/services/index.ts @@ -0,0 +1,2 @@ +export * from './ldap.service'; +export * from './authentication.service'; diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts index e6bf4ef2fa6..428f3c5e048 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts @@ -1,8 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { OAuthTokenDto, OAuthService } from '@modules/oauth'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, RoleName } from '@shared/domain'; diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts index e83e9174abc..e5bc6f942f8 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts @@ -1,7 +1,6 @@ import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { OAuthTokenDto, OAuthService } from '@modules/oauth'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { UserDO } from '@shared/domain/domainobject/user.do'; diff --git a/apps/server/src/modules/oauth/index.ts b/apps/server/src/modules/oauth/index.ts index 40724ec25c2..67f5f98b440 100644 --- a/apps/server/src/modules/oauth/index.ts +++ b/apps/server/src/modules/oauth/index.ts @@ -1,2 +1,3 @@ export * from './oauth.module'; export * from './interface'; +export * from './service'; diff --git a/apps/server/src/modules/oauth/service/index.ts b/apps/server/src/modules/oauth/service/index.ts new file mode 100644 index 00000000000..d9123c8e5bd --- /dev/null +++ b/apps/server/src/modules/oauth/service/index.ts @@ -0,0 +1,3 @@ +export * from './hydra.service'; +export * from './oauth.service'; +export * from './oauth-adapter.service'; diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 9a484a52d47..5e787d79082 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -1,7 +1,6 @@ import { DefaultEncryptionService, IEncryptionService } from '@infra/encryption'; import { LegacySchoolService } from '@modules/legacy-school'; -import { ProvisioningService } from '@modules/provisioning'; -import { OauthDataDto } from '@modules/provisioning/dto'; +import { ProvisioningService, OauthDataDto } from '@modules/provisioning'; import { SystemService } from '@modules/system'; import { SystemDto } from '@modules/system/service'; import { UserService } from '@modules/user'; diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts index 339bc6c09d9..b889995b9e5 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts @@ -7,8 +7,7 @@ import { OauthConfig } from '@shared/domain'; import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; -import { HydraSsoService } from '@modules/oauth/service/hydra.service'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { OAuthService, HydraSsoService } from '@modules/oauth'; import { AxiosResponse } from 'axios'; import { HydraOauthUc } from '.'; import { AuthorizationParams } from '../controller/dto'; diff --git a/apps/server/src/modules/provisioning/index.ts b/apps/server/src/modules/provisioning/index.ts index 0e0bc64d04a..caf7d1f3483 100644 --- a/apps/server/src/modules/provisioning/index.ts +++ b/apps/server/src/modules/provisioning/index.ts @@ -1,4 +1,4 @@ export * from './provisioning.module'; -export * from './dto/provisioning.dto'; +export * from './dto'; export * from './service/provisioning.service'; export * from './strategy'; diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts index 321eba624f9..4e5fec1c31b 100644 --- a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-already-closed.loggable-exception.ts @@ -3,7 +3,7 @@ import { EntityId } from '@shared/domain'; import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; export class UserLoginMigrationAlreadyClosedLoggableException extends UnprocessableEntityException implements Loggable { - constructor(private readonly userLoginMigrationId: EntityId, private readonly closedAt: Date) { + constructor(private readonly closedAt: Date, private readonly userLoginMigrationId?: EntityId) { super(); } diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.spec.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.spec.ts new file mode 100644 index 00000000000..a2b1cde606b --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.spec.ts @@ -0,0 +1,33 @@ +import { ObjectId } from 'bson'; +import { UserLoginMigrationMandatoryLoggable } from './user-login-migration-mandatory.loggable'; + +describe(UserLoginMigrationMandatoryLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const userLoginMigrationId = new ObjectId().toHexString(); + const exception = new UserLoginMigrationMandatoryLoggable(userId, userLoginMigrationId, true); + + return { + exception, + userId, + userLoginMigrationId, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId, userLoginMigrationId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + message: 'The school administrator changed the requirement status of the user login migration for his school.', + data: { + userId, + userLoginMigrationId, + mandatory: true, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.ts index 93312a60402..b3bf5724aaf 100644 --- a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.ts +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-mandatory.loggable.ts @@ -4,7 +4,7 @@ import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from export class UserLoginMigrationMandatoryLoggable implements Loggable { constructor( private readonly userId: EntityId, - private readonly userLoginMigrationId: EntityId, + private readonly userLoginMigrationId: EntityId | undefined, private readonly mandatory: boolean ) {} diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.spec.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.spec.ts new file mode 100644 index 00000000000..f15ac763e5c --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.spec.ts @@ -0,0 +1,32 @@ +import { ObjectId } from 'bson'; +import { UserLoginMigrationStartLoggable } from './user-login-migration-start.loggable'; + +describe(UserLoginMigrationStartLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const userLoginMigrationId = new ObjectId().toHexString(); + const exception = new UserLoginMigrationStartLoggable(userId, userLoginMigrationId); + + return { + exception, + userId, + userLoginMigrationId, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId, userLoginMigrationId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + message: 'The school administrator started the migration for his school.', + data: { + userId, + userLoginMigrationId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.ts index f1ccb50d4d3..150ce3117b1 100644 --- a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.ts +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-start.loggable.ts @@ -2,7 +2,7 @@ import { EntityId } from '@shared/domain'; import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; export class UserLoginMigrationStartLoggable implements Loggable { - constructor(private readonly userId: EntityId, private readonly userLoginMigrationId: EntityId) {} + constructor(private readonly userId: EntityId, private readonly userLoginMigrationId: EntityId | undefined) {} getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { return { diff --git a/apps/server/src/modules/user-login-migration/service/dto/index.ts b/apps/server/src/modules/user-login-migration/service/dto/index.ts index 7a92999ded8..bbc5344f4dd 100644 --- a/apps/server/src/modules/user-login-migration/service/dto/index.ts +++ b/apps/server/src/modules/user-login-migration/service/dto/index.ts @@ -1,3 +1,2 @@ export * from './migration.dto'; export * from './page-content.dto'; -export * from './school-migration-flags'; diff --git a/apps/server/src/modules/user-login-migration/service/dto/school-migration-flags.ts b/apps/server/src/modules/user-login-migration/service/dto/school-migration-flags.ts deleted file mode 100644 index b2b0f8b6e52..00000000000 --- a/apps/server/src/modules/user-login-migration/service/dto/school-migration-flags.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface SchoolMigrationFlags { - isPossible: boolean; - isMandatory: boolean; -} diff --git a/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts b/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts index 7afc017cf98..c3e2d1eac91 100644 --- a/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts @@ -44,7 +44,7 @@ describe('MigrationCheckService', () => { await module.close(); }); - describe('shouldUserMigrate is called', () => { + describe('shouldUserMigrate', () => { describe('when no school with the official school number was found', () => { const setup = () => { schoolService.getSchoolBySchoolNumber.mockResolvedValue(null); @@ -87,6 +87,7 @@ describe('MigrationCheckService', () => { targetSystemId: 'targetSystemId', startedAt: new Date('2023-03-03'), }); + schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); userService.findByExternalId.mockResolvedValue(null); userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigration); diff --git a/apps/server/src/modules/user-login-migration/service/migration-check.service.ts b/apps/server/src/modules/user-login-migration/service/migration-check.service.ts index 70d0ab94066..4c30cc2f8c0 100644 --- a/apps/server/src/modules/user-login-migration/service/migration-check.service.ts +++ b/apps/server/src/modules/user-login-migration/service/migration-check.service.ts @@ -1,8 +1,8 @@ +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { EntityId, LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; -import { LegacySchoolService } from '@modules/legacy-school'; -import { UserService } from '@modules/user'; @Injectable() export class MigrationCheckService { @@ -12,22 +12,39 @@ export class MigrationCheckService { private readonly userLoginMigrationRepo: UserLoginMigrationRepo ) {} - async shouldUserMigrate(externalUserId: string, systemId: EntityId, officialSchoolNumber: string): Promise { + public async shouldUserMigrate( + externalUserId: string, + systemId: EntityId, + officialSchoolNumber: string + ): Promise { const school: LegacySchoolDo | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); - if (school && school.id) { - const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId( - school.id - ); + if (!school?.id) { + return false; + } - const user: UserDO | null = await this.userService.findByExternalId(externalUserId, systemId); + const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(school.id); - if (user?.lastLoginSystemChange && userLoginMigration && !userLoginMigration.closedAt) { - const hasMigrated: boolean = user.lastLoginSystemChange > userLoginMigration.startedAt; - return !hasMigrated; - } - return !!userLoginMigration && !userLoginMigration.closedAt; + if (!userLoginMigration || !this.isMigrationActive(userLoginMigration)) { + return false; } - return false; + + const user: UserDO | null = await this.userService.findByExternalId(externalUserId, systemId); + + if (this.isUserMigrated(user, userLoginMigration)) { + return false; + } + + return true; + } + + private isUserMigrated(user: UserDO | null, userLoginMigration: UserLoginMigrationDO): boolean { + return ( + !!user && user.lastLoginSystemChange !== undefined && user.lastLoginSystemChange > userLoginMigration.startedAt + ); + } + + private isMigrationActive(userLoginMigration: UserLoginMigrationDO): boolean { + return !userLoginMigration.closedAt; } } diff --git a/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts index 988e6de9b01..c84339c9fbf 100644 --- a/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts @@ -303,8 +303,8 @@ describe(SchoolMigrationService.name, () => { const closedAt: Date = new Date('2023-05-01'); const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - schoolId: 'schoolId', - targetSystemId: 'targetSystemId', + schoolId: new ObjectId().toHexString(), + targetSystemId: new ObjectId().toHexString(), startedAt: new Date('2023-05-01'), closedAt, finishedAt: new Date('2023-05-01'), @@ -379,7 +379,7 @@ describe(SchoolMigrationService.name, () => { }; }; - it('should call userLoginMigrationRepo.findBySchoolId', async () => { + it('should find user login migration by school id', async () => { setup(); await service.hasSchoolMigratedUser('schoolId'); @@ -399,7 +399,7 @@ describe(SchoolMigrationService.name, () => { }; }; - it('should call userService.findUsers', async () => { + it('should call user service to find users', async () => { const { userLoginMigration } = setup(); await service.hasSchoolMigratedUser('schoolId'); diff --git a/apps/server/src/modules/user-login-migration/service/school-migration.service.ts b/apps/server/src/modules/user-login-migration/service/school-migration.service.ts index aa5173dcff6..41cabc13d03 100644 --- a/apps/server/src/modules/user-login-migration/service/school-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/school-migration.service.ts @@ -20,7 +20,11 @@ export class SchoolMigrationService { private readonly userLoginMigrationRepo: UserLoginMigrationRepo ) {} - async migrateSchool(existingSchool: LegacySchoolDo, externalId: string, targetSystemId: string): Promise { + public async migrateSchool( + existingSchool: LegacySchoolDo, + externalId: string, + targetSystemId: string + ): Promise { const schoolDOCopy: LegacySchoolDo = new LegacySchoolDo({ ...existingSchool }); try { @@ -45,7 +49,7 @@ export class SchoolMigrationService { await this.schoolService.save(school); } - private async tryRollbackMigration(originalSchoolDO: LegacySchoolDo) { + private async tryRollbackMigration(originalSchoolDO: LegacySchoolDo): Promise { try { await this.schoolService.save(originalSchoolDO); } catch (error: unknown) { @@ -55,7 +59,7 @@ export class SchoolMigrationService { } } - async getSchoolForMigration( + public async getSchoolForMigration( userId: string, externalId: string, officialSchoolNumber: string @@ -89,7 +93,7 @@ export class SchoolMigrationService { return isExternalIdEquivalent; } - async markUnmigratedUsersAsOutdated(userLoginMigration: UserLoginMigrationDO): Promise { + public async markUnmigratedUsersAsOutdated(userLoginMigration: UserLoginMigrationDO): Promise { const startTime: number = performance.now(); const notMigratedUsers: Page = await this.userService.findUsers({ @@ -112,7 +116,7 @@ export class SchoolMigrationService { ); } - async unmarkOutdatedUsers(userLoginMigration: UserLoginMigrationDO): Promise { + public async unmarkOutdatedUsers(userLoginMigration: UserLoginMigrationDO): Promise { const startTime: number = performance.now(); const migratedUsers: Page = await this.userService.findUsers({ @@ -132,7 +136,7 @@ export class SchoolMigrationService { ); } - async hasSchoolMigratedUser(schoolId: string): Promise { + public async hasSchoolMigratedUser(schoolId: string): Promise { const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); if (!userLoginMigration) { diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts index ac7f3bc4740..0edaf9d1a38 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts @@ -11,8 +11,8 @@ import { EntityId, LegacySchoolDo, SchoolFeatures, UserDO, UserLoginMigrationDO import { UserLoginMigrationRepo } from '@shared/repo'; import { legacySchoolDoFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { + UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, } from '../loggable'; import { UserLoginMigrationService } from './user-login-migration.service'; @@ -523,34 +523,25 @@ describe(UserLoginMigrationService.name, () => { describe('setMigrationMandatory', () => { describe('when migration is set to mandatory', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, + const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ startedAt: mockedDate, mandatorySince: undefined, }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigrationDO); - userLoginMigrationRepo.save.mockResolvedValue(userLoginMigrationDO); + userLoginMigrationRepo.save.mockResolvedValue(userLoginMigration); return { - schoolId, - targetSystemId, - userLoginMigrationDO, + userLoginMigration, }; }; it('should call save the user login migration', async () => { - const { schoolId, userLoginMigrationDO } = setup(); + const { userLoginMigration } = setup(); - await service.setMigrationMandatory(schoolId, true); + await service.setMigrationMandatory(userLoginMigration, true); expect(userLoginMigrationRepo.save).toHaveBeenCalledWith({ - ...userLoginMigrationDO, + ...userLoginMigration, mandatorySince: mockedDate, }); }); @@ -558,65 +549,76 @@ describe(UserLoginMigrationService.name, () => { describe('when migration is set to optional', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, + const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ startedAt: mockedDate, mandatorySince: mockedDate, }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigrationDO); - userLoginMigrationRepo.save.mockResolvedValue(userLoginMigrationDO); - return { - schoolId, - targetSystemId, - userLoginMigrationDO, + userLoginMigration, }; }; it('should call save the user login migration', async () => { - const { schoolId, userLoginMigrationDO } = setup(); + const { userLoginMigration } = setup(); - await service.setMigrationMandatory(schoolId, false); + await service.setMigrationMandatory(userLoginMigration, false); expect(userLoginMigrationRepo.save).toHaveBeenCalledWith({ - ...userLoginMigrationDO, + ...userLoginMigration, mandatorySince: undefined, }); }); }); - describe('when migration could not be found', () => { + describe('when the grace period for the user login migration is expired', () => { const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); + const dateInThePast: Date = new Date(mockedDate.getTime() - 100); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ + closedAt: dateInThePast, + finishedAt: dateInThePast, + }); - const targetSystemId: EntityId = new ObjectId().toHexString(); + return { + userLoginMigration, + dateInThePast, + }; + }; - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, - startedAt: mockedDate, + it('should not save the user login migration again', async () => { + const { userLoginMigration } = setup(); + + await expect(service.setMigrationMandatory({ ...userLoginMigration }, true)).rejects.toThrow(); + + expect(userLoginMigrationRepo.save).not.toHaveBeenCalled(); + }); + + it('should return throw an error', async () => { + const { userLoginMigration, dateInThePast } = setup(); + + await expect(service.setMigrationMandatory({ ...userLoginMigration }, true)).rejects.toThrow( + new UserLoginMigrationGracePeriodExpiredLoggableException(userLoginMigration.id as string, dateInThePast) + ); + }); + }); + + describe('when migration is closed', () => { + const setup = () => { + const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ + closedAt: new Date(2023, 5), }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); return { - schoolId, - targetSystemId, - userLoginMigrationDO, + userLoginMigration, }; }; - it('should throw UserLoginMigrationLoggableException ', async () => { - const { schoolId } = setup(); + it('should throw a UserLoginMigrationAlreadyClosedLoggableException', async () => { + const { userLoginMigration } = setup(); - const func = async () => service.setMigrationMandatory(schoolId, true); + const func = async () => service.setMigrationMandatory(userLoginMigration, true); - await expect(func).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); + await expect(func).rejects.toThrow(UserLoginMigrationAlreadyClosedLoggableException); }); }); }); diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts index 04f74f8408c..459b163f119 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts @@ -6,8 +6,8 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId, LegacySchoolDo, SchoolFeatures, SystemTypeEnum, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; import { + UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, } from '../loggable'; @Injectable() @@ -47,12 +47,14 @@ export class UserLoginMigrationService { return updatedUserLoginMigration; } - public async setMigrationMandatory(schoolId: string, mandatory: boolean): Promise { - // this.checkGracePeriod(userLoginMigration); - let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId(schoolId); + public async setMigrationMandatory( + userLoginMigration: UserLoginMigrationDO, + mandatory: boolean + ): Promise { + this.checkGracePeriod(userLoginMigration); - if (!userLoginMigration) { - throw new UserLoginMigrationNotFoundLoggableException(schoolId); + if (userLoginMigration.closedAt) { + throw new UserLoginMigrationAlreadyClosedLoggableException(userLoginMigration.closedAt, userLoginMigration.id); } if (mandatory) { diff --git a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts index 3fcecce5196..60be2ca956b 100644 --- a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts @@ -38,7 +38,7 @@ export class RestartUserLoginMigrationUc { await this.schoolMigrationService.unmarkOutdatedUsers(updatedUserLoginMigration); - this.logger.info(new UserLoginMigrationStartLoggable(userId, updatedUserLoginMigration.id as string)); + this.logger.info(new UserLoginMigrationStartLoggable(userId, updatedUserLoginMigration.id)); return updatedUserLoginMigration; } diff --git a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts index 0f7c615bfbd..1bf47635d83 100644 --- a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts @@ -1,7 +1,7 @@ import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; +import { EntityId, LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { Logger } from '@src/core/logger'; import { SchoolNumberMissingLoggableException, @@ -21,7 +21,7 @@ export class StartUserLoginMigrationUc { this.logger.setContext(StartUserLoginMigrationUc.name); } - async startMigration(userId: string, schoolId: string): Promise { + async startMigration(userId: EntityId, schoolId: EntityId): Promise { await this.checkPreconditions(userId, schoolId); let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( @@ -31,18 +31,15 @@ export class StartUserLoginMigrationUc { if (!userLoginMigration) { userLoginMigration = await this.userLoginMigrationService.startMigration(schoolId); - this.logger.info(new UserLoginMigrationStartLoggable(userId, userLoginMigration.id as string)); + this.logger.info(new UserLoginMigrationStartLoggable(userId, userLoginMigration.id)); } else if (userLoginMigration.closedAt) { - throw new UserLoginMigrationAlreadyClosedLoggableException( - userLoginMigration.id as string, - userLoginMigration.closedAt - ); + throw new UserLoginMigrationAlreadyClosedLoggableException(userLoginMigration.closedAt, userLoginMigration.id); } return userLoginMigration; } - async checkPreconditions(userId: string, schoolId: string): Promise { + private async checkPreconditions(userId: string, schoolId: string): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); diff --git a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts index 75fffd4a3c2..3416250a67e 100644 --- a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts @@ -6,11 +6,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { - UserLoginMigrationAlreadyClosedLoggableException, - UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationNotFoundLoggableException, -} from '../loggable'; +import { ObjectId } from 'bson'; +import { UserLoginMigrationNotFoundLoggableException } from '../loggable'; import { UserLoginMigrationService } from '../service'; import { ToggleUserLoginMigrationUc } from './toggle-user-login-migration.uc'; @@ -78,13 +75,13 @@ describe(ToggleUserLoginMigrationUc.name, () => { userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migrationBeforeMandatory); userLoginMigrationService.setMigrationMandatory.mockResolvedValueOnce(migrationAfterMandatory); - return { user, school, migrationAfterMandatory }; + return { user, school, migrationAfterMandatory, migrationBeforeMandatory }; }; it('should check permission', async () => { const { user, school } = setup(); - await uc.setMigrationMandatory('userId', 'schoolId', true); + await uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), true); expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, @@ -94,17 +91,21 @@ describe(ToggleUserLoginMigrationUc.name, () => { }); it('should call the service to set a migration mandatory', async () => { - setup(); + const { migrationBeforeMandatory } = setup(); - await uc.setMigrationMandatory('userId', 'schoolId', true); + await uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), true); - expect(userLoginMigrationService.setMigrationMandatory).toHaveBeenCalledWith('schoolId', true); + expect(userLoginMigrationService.setMigrationMandatory).toHaveBeenCalledWith(migrationBeforeMandatory, true); }); it('should return a UserLoginMigration', async () => { const { migrationAfterMandatory } = setup(); - const result: UserLoginMigrationDO = await uc.setMigrationMandatory('userId', 'schoolId', true); + const result: UserLoginMigrationDO = await uc.setMigrationMandatory( + new ObjectId().toHexString(), + new ObjectId().toHexString(), + true + ); expect(result).toEqual(migrationAfterMandatory); }); @@ -126,13 +127,13 @@ describe(ToggleUserLoginMigrationUc.name, () => { userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migrationBeforeOptional); userLoginMigrationService.setMigrationMandatory.mockResolvedValueOnce(migrationAfterOptional); - return { user, school, migrationAfterOptional }; + return { user, school, migrationAfterOptional, migrationBeforeOptional }; }; it('should check permission', async () => { const { user, school } = setup(); - await uc.setMigrationMandatory('userId', 'schoolId', false); + await uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), false); expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, @@ -142,17 +143,21 @@ describe(ToggleUserLoginMigrationUc.name, () => { }); it('should call the service to set a migration optional', async () => { - setup(); + const { migrationBeforeOptional } = setup(); - await uc.setMigrationMandatory('userId', 'schoolId', false); + await uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), false); - expect(userLoginMigrationService.setMigrationMandatory).toHaveBeenCalledWith('schoolId', false); + expect(userLoginMigrationService.setMigrationMandatory).toHaveBeenCalledWith(migrationBeforeOptional, false); }); it('should return a UserLoginMigration', async () => { const { migrationAfterOptional } = setup(); - const result: UserLoginMigrationDO = await uc.setMigrationMandatory('userId', 'schoolId', false); + const result: UserLoginMigrationDO = await uc.setMigrationMandatory( + new ObjectId().toHexString(), + new ObjectId().toHexString(), + false + ); expect(result).toEqual(migrationAfterOptional); }); @@ -174,9 +179,9 @@ describe(ToggleUserLoginMigrationUc.name, () => { it('should throw an exception', async () => { setup(); - const func = async () => uc.setMigrationMandatory('userId', 'schoolId', true); - - await expect(func).rejects.toThrow(ForbiddenException); + await expect( + uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), true) + ).rejects.toThrow(ForbiddenException); }); }); @@ -194,61 +199,9 @@ describe(ToggleUserLoginMigrationUc.name, () => { it('should throw a UserLoginMigrationNotFoundLoggableException', async () => { setup(); - const func = async () => uc.setMigrationMandatory('userId', 'schoolId', true); - - await expect(func).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); - }); - }); - - describe('when the grace period for restarting a migration has expired', () => { - const setup = () => { - jest.useFakeTimers(); - jest.setSystemTime(new Date(2023, 6)); - - const migration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - closedAt: new Date(2023, 5), - finishedAt: new Date(2023, 5), - }); - - const user: User = userFactory.buildWithId(); - - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); - userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migration); - }; - - it('should throw a UserLoginMigrationGracePeriodExpiredLoggableException', async () => { - setup(); - - const func = async () => uc.setMigrationMandatory('userId', 'schoolId', true); - - await expect(func).rejects.toThrow(UserLoginMigrationGracePeriodExpiredLoggableException); - }); - }); - - describe('when migration is closed', () => { - const setup = () => { - const migration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - closedAt: new Date(2023, 5), - }); - - const user: User = userFactory.buildWithId(); - - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - - authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); - schoolService.getSchoolById.mockResolvedValueOnce(school); - userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(migration); - }; - - it('should throw a UserLoginMigrationAlreadyClosedLoggableException', async () => { - setup(); - - const func = async () => uc.setMigrationMandatory('userId', 'schoolId', true); - - await expect(func).rejects.toThrow(UserLoginMigrationAlreadyClosedLoggableException); + await expect( + uc.setMigrationMandatory(new ObjectId().toHexString(), new ObjectId().toHexString(), true) + ).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); }); }); }); diff --git a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts index bb020379b9c..cb964ff7fa6 100644 --- a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts @@ -1,14 +1,9 @@ import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; import { Injectable } from '@nestjs/common'; -import { LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; +import { EntityId, LegacySchoolDo, Permission, User, UserLoginMigrationDO } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { - UserLoginMigrationAlreadyClosedLoggableException, - UserLoginMigrationGracePeriodExpiredLoggableException, - UserLoginMigrationMandatoryLoggable, - UserLoginMigrationNotFoundLoggableException, -} from '../loggable'; +import { UserLoginMigrationMandatoryLoggable, UserLoginMigrationNotFoundLoggableException } from '../loggable'; import { UserLoginMigrationService } from '../service'; @Injectable() @@ -20,7 +15,7 @@ export class ToggleUserLoginMigrationUc { private readonly logger: Logger ) {} - async setMigrationMandatory(userId: string, schoolId: string, mandatory: boolean): Promise { + async setMigrationMandatory(userId: EntityId, schoolId: EntityId, mandatory: boolean): Promise { await this.checkPermission(userId, schoolId); let userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( @@ -29,26 +24,16 @@ export class ToggleUserLoginMigrationUc { if (!userLoginMigration) { throw new UserLoginMigrationNotFoundLoggableException(schoolId); - } else if (userLoginMigration.finishedAt && Date.now() >= userLoginMigration.finishedAt.getTime()) { - throw new UserLoginMigrationGracePeriodExpiredLoggableException( - userLoginMigration.id as string, - userLoginMigration.finishedAt - ); - } else if (userLoginMigration.closedAt) { - throw new UserLoginMigrationAlreadyClosedLoggableException( - userLoginMigration.id as string, - userLoginMigration.closedAt - ); - } else { - userLoginMigration = await this.userLoginMigrationService.setMigrationMandatory(schoolId, mandatory); - - this.logger.debug(new UserLoginMigrationMandatoryLoggable(userId, userLoginMigration.id as string, mandatory)); } + userLoginMigration = await this.userLoginMigrationService.setMigrationMandatory(userLoginMigration, mandatory); + + this.logger.debug(new UserLoginMigrationMandatoryLoggable(userId, userLoginMigration.id, mandatory)); + return userLoginMigration; } - async checkPermission(userId: string, schoolId: string): Promise { + private async checkPermission(userId: string, schoolId: string): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts index bebd6b7115a..4179505d009 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts @@ -1,12 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { AuthenticationService } from '@modules/authentication'; import { Action, AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; -import { ProvisioningService } from '@modules/provisioning'; -import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; +import { OAuthTokenDto, OAuthService } from '@modules/oauth'; +import { + ProvisioningService, + ExternalSchoolDto, + ExternalUserDto, + OauthDataDto, + ProvisioningSystemDto, +} from '@modules/provisioning'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; @@ -20,8 +24,7 @@ import { userLoginMigrationDOFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { ExternalSchoolNumberMissingLoggableException } from '../loggable'; -import { InvalidUserLoginMigrationLoggableException } from '../loggable/invalid-user-login-migration.loggable-exception'; +import { ExternalSchoolNumberMissingLoggableException, InvalidUserLoginMigrationLoggableException } from '../loggable'; import { SchoolMigrationService, UserLoginMigrationService, UserMigrationService } from '../service'; import { UserLoginMigrationUc } from './user-login-migration.uc'; @@ -104,7 +107,7 @@ describe(UserLoginMigrationUc.name, () => { describe('getMigrations', () => { describe('when searching for a users migration', () => { const setup = () => { - const userId = 'userId'; + const userId = new ObjectId().toHexString(); const migrations: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ schoolId: 'schoolId', @@ -117,7 +120,7 @@ describe(UserLoginMigrationUc.name, () => { return { userId, migrations }; }; - it('should return a response', async () => { + it('should return a page response with data', async () => { const { userId, migrations } = setup(); const result: Page = await uc.getMigrations(userId, { userId }); @@ -131,14 +134,14 @@ describe(UserLoginMigrationUc.name, () => { describe('when a user has no migration available', () => { const setup = () => { - const userId = 'userId'; + const userId = new ObjectId().toHexString(); userLoginMigrationService.findMigrationByUser.mockResolvedValueOnce(null); return { userId }; }; - it('should return a response', async () => { + it('should return a page response without data', async () => { const { userId } = setup(); const result: Page = await uc.getMigrations(userId, { userId }); @@ -152,12 +155,12 @@ describe(UserLoginMigrationUc.name, () => { describe('when searching for other users migrations', () => { const setup = () => { - const userId = 'userId'; + const userId = new ObjectId().toHexString(); return { userId }; }; - it('should return a response', async () => { + it('should throw a forbidden exception', async () => { const { userId } = setup(); const func = async () => uc.getMigrations(userId, { userId: 'otherUserId' }); @@ -172,7 +175,7 @@ describe(UserLoginMigrationUc.name, () => { describe('findUserLoginMigrationBySchool', () => { describe('when searching for an existing user login migration', () => { const setup = () => { - const schoolId = 'schoolId'; + const schoolId = new ObjectId().toHexString(); const migration: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ schoolId, @@ -209,7 +212,7 @@ describe(UserLoginMigrationUc.name, () => { describe('when a user login migration does not exist', () => { const setup = () => { - const schoolId = 'schoolId'; + const schoolId = new ObjectId().toHexString(); const user: User = userFactory.buildWithId(); @@ -230,7 +233,7 @@ describe(UserLoginMigrationUc.name, () => { describe('when the authorization fails', () => { const setup = () => { - const schoolId = 'schoolId'; + const schoolId = new ObjectId().toHexString(); const user: User = userFactory.buildWithId(); diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts index 24442147e5e..8314cbfd455 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts @@ -1,9 +1,7 @@ -import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { AuthenticationService } from '@modules/authentication'; import { Action, AuthorizationService } from '@modules/authorization'; -import { OAuthTokenDto } from '@modules/oauth'; -import { OAuthService } from '@modules/oauth/service/oauth.service'; -import { ProvisioningService } from '@modules/provisioning'; -import { OauthDataDto } from '@modules/provisioning/dto'; +import { OAuthTokenDto, OAuthService } from '@modules/oauth'; +import { ProvisioningService, OauthDataDto } from '@modules/provisioning'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { EntityId, LegacySchoolDo, Page, Permission, User, UserLoginMigrationDO } from '@shared/domain';