diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index 9abde17..81f8ea4 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -25,6 +25,7 @@ export const overrideAdB2cService = ( password: string, username: string, ) => Promise<{ sub: string } | ConflictError>; + deleteUser?: (externalId: string, context: Context) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -35,6 +36,12 @@ export const overrideAdB2cService = ( writable: true, }); } + if (overrides.deleteUser) { + Object.defineProperty(obj, obj.deleteUser.name, { + value: overrides.deleteUser, + writable: true, + }); + } }; /** @@ -115,6 +122,7 @@ export const overrideUsersRepositoryService = ( service: TService, overrides: { createNormalUser?: (user: newUser) => Promise; + deleteNormalUser?: (userId: number) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -125,6 +133,12 @@ export const overrideUsersRepositoryService = ( writable: true, }); } + if (overrides.deleteNormalUser) { + Object.defineProperty(obj, obj.deleteNormalUser.name, { + value: overrides.deleteNormalUser, + writable: true, + }); + } }; /** diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 1c2ee84..fa18ab9 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -40,6 +40,7 @@ import { } from '../../common/test/overrides'; import { NewTrialLicenseExpirationDate } from '../licenses/types/types'; import { License } from '../../repositories/licenses/entity/license.entity'; +import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; describe('UsersService.confirmUser', () => { let source: DataSource = null; @@ -709,9 +710,10 @@ describe('UsersService.createUser', () => { expect(users.length).toEqual(2); }); - it('DBネットワークエラーとなる場合、エラーとなる。', async () => { + it('DBネットワークエラーとなる場合、リカバリ処理を実施し、ADB2Cに作成したユーザーを削除する', async () => { const module = await makeTestingModule(source); const service = module.get(UsersService); + const b2cService = module.get(AdB2cService); const adminExternalId = 'ADMIN0001'; const { role: adminRole, tier } = await createAccountAndAdminUser( source, @@ -746,6 +748,7 @@ describe('UsersService.createUser', () => { return { sub: externalId }; }, + deleteUser: jest.fn(), }); overrideSendgridService(service, { sendMail: async () => { @@ -782,6 +785,99 @@ describe('UsersService.createUser', () => { fail(); } } + // ADB2Cに作成したユーザーを削除するメソッドが呼ばれていることを確認 + expect(b2cService.deleteUser).toBeCalledWith( + externalId, + makeContext('trackingId'), + ); + }); + + it('DBネットワークエラーとなる場合、リカバリ処理を実施されるが、そのリカバリ処理に失敗した場合、ADB2Cのユーザーは削除されない', async () => { + const module = await makeTestingModule(source); + const service = module.get(UsersService); + const b2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { + accountId, + role: adminRole, + tier, + } = await createAccountAndAdminUser(source, adminExternalId); + + const token: AccessToken = { + userId: adminExternalId, + role: adminRole, + tier: tier, + }; + + const name = 'test_user1'; + const role = USER_ROLES.NONE; + const email = 'test1@example.com'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + + const externalId = '0001'; + + overrideAdB2cService(service, { + createUser: async ( + _context: Context, + _email: string, + _password: string, + _username: string, + ) => { + // ユーザー作成時に指定したパラメータが正しく渡されていることを確認 + expect(email).toEqual(_email); + expect(name).toEqual(_username); + + return { sub: externalId }; + }, + deleteUser: jest.fn().mockRejectedValue(new Error('ADB2C error')), + }); + overrideSendgridService(service, { + sendMail: async () => { + return; + }, + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + }); + + // DBエラーを発生させる + overrideUsersRepositoryService(service, { + createNormalUser: async () => { + throw new Error('DB error'); + }, + }); + + try { + await service.createUser( + makeContext('trackingId'), + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + // 新規ユーザーが登録されていないことを確認 + const users = await getUsers(source); + expect(users.length).toEqual(1); + //アカウントIDがテスト用の管理者ユーザーのものであることを確認 + expect(users[0].account_id).toEqual(accountId); + // ADB2Cに作成したユーザーを削除するメソッドが呼ばれていることを確認 + expect(b2cService.deleteUser).toBeCalledWith( + externalId, + makeContext('trackingId'), + ); }); it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。', async () => { @@ -1051,14 +1147,16 @@ describe('UsersService.createUser', () => { } }); - it('AuthorIDが重複している場合、エラーとなる。(insert失敗)', async () => { + it('AuthorIDが重複している場合、エラー(insert失敗)となり、リカバリ処理が実行され、ADB2Cに追加したユーザーが削除される', async () => { const module = await makeTestingModule(source); const service = module.get(UsersService); + const b2cService = module.get(AdB2cService); const adminExternalId = 'ADMIN0001'; - const { role: adminRole, tier } = await createAccountAndAdminUser( - source, - adminExternalId, - ); + const { + accountId, + role: adminRole, + tier, + } = await createAccountAndAdminUser(source, adminExternalId); const token: AccessToken = { userId: adminExternalId, @@ -1092,6 +1190,7 @@ describe('UsersService.createUser', () => { return { sub: externalId }; }, + deleteUser: jest.fn(), }); overrideSendgridService(service, { sendMail: async () => { @@ -1132,6 +1231,183 @@ describe('UsersService.createUser', () => { fail(); } } + // 新規にユーザーが登録されていないことを確認 + const users = await getUsers(source); + expect(users.length).toEqual(1); + expect(users[0].account_id).toEqual(accountId); + // ADB2Cに作成したユーザーを削除するメソッドが呼ばれていることを確認 + expect(b2cService.deleteUser).toBeCalledWith( + externalId, + makeContext('trackingId'), + ); + }); + + it('メール送信に失敗した場合、リカバリ処理が実行され、ADB2C,DBのユーザーが削除される', async () => { + const module = await makeTestingModule(source); + const service = module.get(UsersService); + const b2cService = module.get(AdB2cService); + + const adminExternalId = 'ADMIN0001'; + const { + accountId, + role: adminRole, + tier, + } = await createAccountAndAdminUser(source, adminExternalId); + + const token: AccessToken = { + userId: adminExternalId, + role: adminRole, + tier: tier, + }; + + const name = 'test_user1'; + const role = USER_ROLES.NONE; + const email = 'test1@example.com'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + + const externalId = '0001'; + + overrideAdB2cService(service, { + createUser: async ( + _context: Context, + _email: string, + _password: string, + _username: string, + ) => { + // ユーザー作成時に指定したパラメータが正しく渡されていることを確認 + expect(email).toEqual(_email); + expect(name).toEqual(_username); + + return { sub: externalId }; + }, + deleteUser: jest.fn(), + }); + overrideSendgridService(service, { + sendMail: async () => { + throw new Error(); + }, + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + }); + + try { + await service.createUser( + makeContext('trackingId'), + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + + // 新規ユーザーが登録されていないことを確認 + const users = await getUsers(source); + expect(users.length).toEqual(1); + //アカウントIDがテスト用の管理者ユーザーのものであることを確認 + expect(users[0].account_id).toEqual(accountId); + // ADB2Cに作成したユーザーを削除するメソッドが呼ばれていることを確認 + expect(b2cService.deleteUser).toBeCalledWith( + externalId, + makeContext('trackingId'), + ); + }); + + it('メール送信に失敗した場合、リカバリ処理が実行されるが、そのリカバリ処理に失敗した場合、ADB2C,DBのユーザーが削除されない', async () => { + const module = await makeTestingModule(source); + const service = module.get(UsersService); + const b2cService = module.get(AdB2cService); + + const adminExternalId = 'ADMIN0001'; + const { role: adminRole, tier } = await createAccountAndAdminUser( + source, + adminExternalId, + ); + + const token: AccessToken = { + userId: adminExternalId, + role: adminRole, + tier: tier, + }; + + const name = 'test_user1'; + const role = USER_ROLES.NONE; + const email = 'test1@example.com'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + + const externalId = '0001'; + + overrideAdB2cService(service, { + createUser: async ( + _context: Context, + _email: string, + _password: string, + _username: string, + ) => { + // ユーザー作成時に指定したパラメータが正しく渡されていることを確認 + expect(email).toEqual(_email); + expect(name).toEqual(_username); + + return { sub: externalId }; + }, + deleteUser: jest.fn().mockRejectedValue(new Error()), + }); + overrideSendgridService(service, { + sendMail: async () => { + throw new Error(); + }, + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + }); + overrideUsersRepositoryService(service, { + deleteNormalUser: async () => { + throw new Error(); + }, + }); + + try { + await service.createUser( + makeContext('trackingId'), + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + + // リカバリ処理が失敗したため、DBのユーザーが削除されないことを確認 + const users = await getUsers(source); + expect(users.length).toEqual(2); + // ADB2Cに作成したユーザーを削除するメソッドが呼ばれていることを確認 + expect(b2cService.deleteUser).toBeCalledWith( + externalId, + makeContext('trackingId'), + ); }); }); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 032fb18..6e9fa07 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -229,6 +229,10 @@ export class UsersService { } catch (e) { this.logger.error(`error=${e}`); this.logger.error('create user failed'); + //リカバリー処理 + //Azure AD B2Cに登録したユーザー情報を削除する + await this.deleteB2cUser(externalUser.sub, context); + switch (e.code) { case 'ER_DUP_ENTRY': //AuthorID重複エラー @@ -269,7 +273,11 @@ export class UsersService { } catch (e) { this.logger.error(`error=${e}`); this.logger.error('create user failed'); - this.logger.error(`[NOT IMPLEMENT] [RECOVER] delete user: ${newUser.id}`); + //リカバリー処理 + //Azure AD B2Cに登録したユーザー情報を削除する + await this.deleteB2cUser(externalUser.sub, context); + // DBからユーザーを削除する + await this.deleteUser(newUser.id, context); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, @@ -279,6 +287,35 @@ export class UsersService { return; } + // Azure AD B2Cに登録したユーザー情報を削除する + // TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補 + private async deleteB2cUser(externalUserId: string, context: Context) { + try { + await this.adB2cService.deleteUser(externalUserId, context); + this.logger.log( + `[${context.trackingId}] delete externalUser: ${externalUserId}`, + ); + } catch (error) { + this.logger.error(`error=${error}`); + this.logger.error( + `[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`, + ); + } + } + + // DBに登録したユーザー情報を削除する + private async deleteUser(userId: number, context: Context) { + try { + await this.usersRepository.deleteNormalUser(userId); + this.logger.log(`[${context.trackingId}] delete user: ${userId}`); + } catch (error) { + this.logger.error(`error=${error}`); + this.logger.error( + `[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete user: ${userId}`, + ); + } + } + // roleを受け取って、roleに応じたnewUserを作成して返却する private createNewUserInfo( role: UserRoles, diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 2dce6a3..7683de3 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -381,4 +381,22 @@ export class UsersRepositoryService { return typists; }); } + + /** + * UserID指定のユーザーとソート条件を同時に削除する + * @param userId + * @returns delete + */ + async deleteNormalUser(userId: number): Promise { + await this.dataSource.transaction(async (entityManager) => { + const usersRepo = entityManager.getRepository(User); + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + // ソート条件を削除 + await sortCriteriaRepo.delete({ + user_id: userId, + }); + // プライマリ管理者を削除 + await usersRepo.delete({ id: userId }); + }); + } }