diff --git a/apps/server/src/modules/user-import/index.ts b/apps/server/src/modules/user-import/index.ts index b6cd58577bf..0d666df6a20 100644 --- a/apps/server/src/modules/user-import/index.ts +++ b/apps/server/src/modules/user-import/index.ts @@ -1,3 +1,4 @@ export { ImportUserModule } from './user-import.module'; export { UserImportConfigModule } from './user-import-config.module'; export { IUserImportFeatures, UserImportConfiguration } from './config'; +export { UserImportService } from './service'; diff --git a/apps/server/src/modules/user-import/service/user-import.service.spec.ts b/apps/server/src/modules/user-import/service/user-import.service.spec.ts index 397d042eaea..adfbba48f6d 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.spec.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.spec.ts @@ -1,6 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { LegacySchoolService } from '@modules/legacy-school'; import { UserService } from '@modules/user'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -19,7 +20,7 @@ import { } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { IUserImportFeatures, UserImportFeatures } from '../config'; -import { UserMigrationIsNotEnabled } from '../loggable'; +import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; import { UserImportService } from './user-import.service'; describe(UserImportService.name, () => { @@ -31,6 +32,7 @@ describe(UserImportService.name, () => { let legacySystemRepo: DeepMocked; let userService: DeepMocked; let logger: DeepMocked; + let schoolService: DeepMocked; const features: IUserImportFeatures = { userMigrationSystemId: new ObjectId().toHexString(), @@ -65,6 +67,10 @@ describe(UserImportService.name, () => { provide: Logger, useValue: createMock(), }, + { + provide: LegacySchoolService, + useValue: createMock(), + }, ], }).compile(); @@ -74,6 +80,7 @@ describe(UserImportService.name, () => { legacySystemRepo = module.get(LegacySystemRepo); userService = module.get(UserService); logger = module.get(Logger); + schoolService = module.get(LegacySchoolService); }); afterAll(async () => { @@ -356,4 +363,49 @@ describe(UserImportService.name, () => { }); }); }); + + describe('resetMigrationForUsersSchool', () => { + describe('when resetting the migration for a school', () => { + const setup = () => { + const currentUser: User = userFactory.build(); + const school: LegacySchoolDo = legacySchoolDoFactory.build(); + + return { + currentUser, + school, + }; + }; + + it('should delete import users for school', async () => { + const { currentUser, school } = setup(); + + await service.resetMigrationForUsersSchool(currentUser, school); + + expect(importUserRepo.deleteImportUsersBySchool).toHaveBeenCalledWith(currentUser.school); + }); + + it('should save school with reset migration flags', async () => { + const { currentUser, school } = setup(); + + await service.resetMigrationForUsersSchool(currentUser, school); + + expect(schoolService.save).toHaveBeenCalledWith( + { + ...school, + inUserMigration: undefined, + inMaintenanceSince: undefined, + }, + true + ); + }); + + it('should log canceled migration', async () => { + const { currentUser, school } = setup(); + + await service.resetMigrationForUsersSchool(currentUser, school); + + expect(logger.notice).toHaveBeenCalledWith(new UserMigrationCanceledLoggable(expect.any(LegacySchoolDo))); + }); + }); + }); }); diff --git a/apps/server/src/modules/user-import/service/user-import.service.ts b/apps/server/src/modules/user-import/service/user-import.service.ts index 0e165d4828c..2f6d8ef6c49 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.ts @@ -1,12 +1,13 @@ +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; import { LegacySchoolDo } from '@shared/domain/domainobject'; import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; import { SchoolFeature } from '@shared/domain/types'; import { ImportUserRepo, LegacySystemRepo } from '@shared/repo'; -import { UserService } from '@modules/user'; import { Logger } from '@src/core/logger'; import { IUserImportFeatures, UserImportFeatures } from '../config'; -import { UserMigrationIsNotEnabled } from '../loggable'; +import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; @Injectable() export class UserImportService { @@ -15,7 +16,8 @@ export class UserImportService { private readonly systemRepo: LegacySystemRepo, private readonly userService: UserService, @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, - private readonly logger: Logger + private readonly logger: Logger, + private readonly schoolService: LegacySchoolService ) {} public async saveImportUsers(importUsers: ImportUser[]): Promise { @@ -74,4 +76,15 @@ export class UserImportService { public async deleteImportUsersBySchool(school: SchoolEntity): Promise { await this.userImportRepo.deleteImportUsersBySchool(school); } + + public async resetMigrationForUsersSchool(currentUser: User, school: LegacySchoolDo): Promise { + await this.userImportRepo.deleteImportUsersBySchool(currentUser.school); + + school.inUserMigration = undefined; + school.inMaintenanceSince = undefined; + + await this.schoolService.save(school, true); + + this.logger.notice(new UserMigrationCanceledLoggable(school)); + } } diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts index 81ed9b6502f..705531a3e90 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts @@ -3,6 +3,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Account, AccountService } from '@modules/account'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; @@ -26,14 +27,8 @@ import { } from '@shared/testing'; import { systemEntityFactory } from '@shared/testing/factory/systemEntityFactory'; import { Logger } from '@src/core/logger'; -import { UserService } from '@modules/user'; import { IUserImportFeatures, UserImportFeatures } from '../config'; -import { - SchoolNotMigratedLoggableException, - UserAlreadyMigratedLoggable, - UserMigrationCanceledLoggable, -} from '../loggable'; - +import { SchoolNotMigratedLoggableException, UserAlreadyMigratedLoggable } from '../loggable'; import { UserImportService } from '../service'; import { LdapAlreadyPersistedException, @@ -1195,35 +1190,12 @@ describe('[ImportUserModule]', () => { expect(userImportService.checkFeatureEnabled).toHaveBeenCalled(); }); - it('should delete import users for school', async () => { - const { user } = setup(); - - await uc.cancelMigration(user.id); - - expect(importUserRepo.deleteImportUsersBySchool).toHaveBeenCalledWith(user.school); - }); - - it('should save school with reset migration flags', async () => { + it('should call reset migration', async () => { const { user, school } = setup(); await uc.cancelMigration(user.id); - expect(schoolService.save).toHaveBeenCalledWith( - { - ...school, - inUserMigration: undefined, - inMaintenanceSince: undefined, - }, - true - ); - }); - - it('should log canceled migration', async () => { - const { user } = setup(); - - await uc.cancelMigration(user.id); - - expect(logger.notice).toHaveBeenCalledWith(new UserMigrationCanceledLoggable(expect.any(LegacySchoolDo))); + expect(userImportService.resetMigrationForUsersSchool).toHaveBeenCalledWith(user, school); }); }); }); diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index 06ef3403eb0..a77127c51e4 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -1,6 +1,7 @@ import { Account, AccountSave, AccountService } from '@modules/account'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; @@ -12,7 +13,6 @@ import { IFindOptions, Permission } from '@shared/domain/interface'; import { Counted, EntityId, IImportUserScope, MatchCreatorScope, NameMatch } from '@shared/domain/types'; import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { UserService } from '@modules/user'; import { IUserImportFeatures, UserImportFeatures } from '../config'; import { MigrationMayBeCompleted, @@ -22,7 +22,6 @@ import { SchoolInUserMigrationStartLoggable, SchoolNotMigratedLoggableException, UserAlreadyMigratedLoggable, - UserMigrationCanceledLoggable, } from '../loggable'; import { UserImportService } from '../service'; @@ -332,14 +331,7 @@ export class UserImportUc { this.userImportService.checkFeatureEnabled(school); - await this.importUserRepo.deleteImportUsersBySchool(currentUser.school); - - school.inUserMigration = undefined; - school.inMaintenanceSince = undefined; - - await this.schoolService.save(school, true); - - this.logger.notice(new UserMigrationCanceledLoggable(school)); + await this.userImportService.resetMigrationForUsersSchool(currentUser, school); } private async getCurrentUser(currentUserId: EntityId, permission: UserImportPermissions): Promise { diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index 7caed266954..6c305a98353 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -38,7 +38,7 @@ import { UserImportConfigModule } from './user-import-config.module'; UserImportService, SchulconnexFetchImportUsersService, ], - exports: [], + exports: [UserImportService], }) /** * Module to provide user migration, diff --git a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts index 4e99f03dc14..053897544a1 100644 --- a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts @@ -1,11 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserImportService } from '@modules/user-import'; import { Test, TestingModule } from '@nestjs/testing'; - import { UserLoginMigrationDO } from '@shared/domain/domainobject'; import { Permission } from '@shared/domain/interface'; -import { setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { + legacySchoolDoFactory, + schoolEntityFactory, + setupEntities, + userFactory, + userLoginMigrationDOFactory, +} from '@shared/testing'; import { UserLoginMigrationNotFoundLoggableException } from '../loggable'; import { SchoolMigrationService, UserLoginMigrationRevertService, UserLoginMigrationService } from '../service'; import { CloseUserLoginMigrationUc } from './close-user-login-migration.uc'; @@ -18,6 +25,8 @@ describe(CloseUserLoginMigrationUc.name, () => { let schoolMigrationService: DeepMocked; let userLoginMigrationRevertService: DeepMocked; let authorizationService: DeepMocked; + let schoolService: DeepMocked; + let userImportService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -41,6 +50,14 @@ describe(CloseUserLoginMigrationUc.name, () => { provide: AuthorizationService, useValue: createMock(), }, + { + provide: LegacySchoolService, + useValue: createMock(), + }, + { + provide: UserImportService, + useValue: createMock(), + }, ], }).compile(); @@ -49,6 +66,8 @@ describe(CloseUserLoginMigrationUc.name, () => { schoolMigrationService = module.get(SchoolMigrationService); userLoginMigrationRevertService = module.get(UserLoginMigrationRevertService); authorizationService = module.get(AuthorizationService); + schoolService = module.get(LegacySchoolService); + userImportService = module.get(UserImportService); }); afterAll(async () => { @@ -62,18 +81,20 @@ describe(CloseUserLoginMigrationUc.name, () => { describe('closeMigration', () => { describe('when the user login migration was closed after a migration', () => { const setup = () => { - const user = userFactory.buildWithId(); + const schoolId = new ObjectId().toHexString(); + const user = userFactory.buildWithId({ school: schoolEntityFactory.build({ _id: schoolId }) }); const userLoginMigration = userLoginMigrationDOFactory.buildWithId(); const closedUserLoginMigration = new UserLoginMigrationDO({ ...userLoginMigration, closedAt: new Date(2023, 1), }); - const schoolId = new ObjectId().toHexString(); + const school = legacySchoolDoFactory.build({ id: schoolId, inUserMigration: false }); userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(userLoginMigration); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); userLoginMigrationService.closeMigration.mockResolvedValueOnce(closedUserLoginMigration); schoolMigrationService.hasSchoolMigratedUser.mockResolvedValueOnce(true); + schoolService.getSchoolById.mockResolvedValueOnce(school); return { user, @@ -144,18 +165,20 @@ describe(CloseUserLoginMigrationUc.name, () => { describe('when the user login migration was closed without any migration', () => { const setup = () => { - const user = userFactory.buildWithId(); + const schoolId = new ObjectId().toHexString(); + const user = userFactory.buildWithId({ school: schoolEntityFactory.build({ _id: schoolId }) }); const userLoginMigration = userLoginMigrationDOFactory.buildWithId(); const closedUserLoginMigration = new UserLoginMigrationDO({ ...userLoginMigration, closedAt: new Date(2023, 1), }); - const schoolId = 'schoolId'; + const school = legacySchoolDoFactory.build({ id: schoolId, inUserMigration: false }); userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(userLoginMigration); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); userLoginMigrationService.closeMigration.mockResolvedValueOnce(closedUserLoginMigration); schoolMigrationService.hasSchoolMigratedUser.mockResolvedValueOnce(false); + schoolService.getSchoolById.mockResolvedValueOnce(school); return { user, @@ -201,5 +224,46 @@ describe(CloseUserLoginMigrationUc.name, () => { expect(result).toBeUndefined(); }); }); + + describe('when migration wizard is active', () => { + const setup = () => { + const schoolId = new ObjectId().toHexString(); + const user = userFactory.buildWithId({ school: schoolEntityFactory.build({ _id: schoolId }) }); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId(); + const closedUserLoginMigration = new UserLoginMigrationDO({ + ...userLoginMigration, + closedAt: new Date(2023, 1), + }); + const school = legacySchoolDoFactory.build({ id: schoolId, inUserMigration: true }); + + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(userLoginMigration); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userLoginMigrationService.closeMigration.mockResolvedValueOnce(closedUserLoginMigration); + schoolMigrationService.hasSchoolMigratedUser.mockResolvedValueOnce(false); + schoolService.getSchoolById.mockResolvedValueOnce(school); + + return { + user, + schoolId, + school, + }; + }; + + it('should check all permissions', async () => { + const { user, schoolId } = setup(); + + await uc.closeMigration(user.id, schoolId); + + expect(authorizationService.checkAllPermissions).toHaveBeenCalledWith(user, [Permission.IMPORT_USER_MIGRATE]); + }); + + it('should reset migration for user school', async () => { + const { user, schoolId, school } = setup(); + + await uc.closeMigration(user.id, schoolId); + + expect(userImportService.resetMigrationForUsersSchool).toHaveBeenCalledWith(user, school); + }); + }); }); }); diff --git a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts index 5228e644733..c9964e0d4e9 100644 --- a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts @@ -1,6 +1,8 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserImportService } from '@modules/user-import'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { UserLoginMigrationDO } from '@shared/domain/domainobject'; +import { LegacySchoolDo, UserLoginMigrationDO } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; @@ -13,10 +15,12 @@ export class CloseUserLoginMigrationUc { private readonly userLoginMigrationService: UserLoginMigrationService, private readonly schoolMigrationService: SchoolMigrationService, private readonly userLoginMigrationRevertService: UserLoginMigrationRevertService, - private readonly authorizationService: AuthorizationService + private readonly authorizationService: AuthorizationService, + private readonly userImportService: UserImportService, + private readonly schoolService: LegacySchoolService ) {} - async closeMigration(userId: EntityId, schoolId: EntityId): Promise { + public async closeMigration(userId: EntityId, schoolId: EntityId): Promise { const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( schoolId ); @@ -32,6 +36,8 @@ export class CloseUserLoginMigrationUc { AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]) ); + await this.cleanupMigrationWizardWhenActive(user); + const updatedUserLoginMigration: UserLoginMigrationDO = await this.userLoginMigrationService.closeMigration( userLoginMigration ); @@ -48,4 +54,14 @@ export class CloseUserLoginMigrationUc { return updatedUserLoginMigration; } + + private async cleanupMigrationWizardWhenActive(user: User): Promise { + const school: LegacySchoolDo = await this.schoolService.getSchoolById(user.school.id); + + if (school.inUserMigration) { + this.authorizationService.checkAllPermissions(user, [Permission.IMPORT_USER_MIGRATE]); + + await this.userImportService.resetMigrationForUsersSchool(user, school); + } + } } diff --git a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts index 21471537a76..9784261ac44 100644 --- a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts +++ b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts @@ -3,6 +3,7 @@ import { AuthorizationModule } from '@modules/authorization'; import { LegacySchoolModule } from '@modules/legacy-school'; import { OauthModule } from '@modules/oauth'; import { ProvisioningModule } from '@modules/provisioning'; +import { ImportUserModule } from '@modules/user-import'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { UserLoginMigrationRollbackController } from './controller/user-login-migration-rollback.controller'; @@ -26,6 +27,7 @@ import { UserLoginMigrationModule } from './user-login-migration.module'; AuthorizationModule, LoggerModule, LegacySchoolModule, + ImportUserModule, ], providers: [ UserLoginMigrationUc,