diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index e5c0fd2..1879e68 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -250,7 +250,6 @@ describe('createAccount', () => { const users = await getUsers(source); expect(users.length).toBe(0); }); - it('アカウントを作成がDBへの通信失敗によって500エラーが発生した場合、リカバリ処理としてADB2Cユーザーを削除され、500エラーが返却される', async () => { const module = await makeTestingModule(source); const service = module.get(AccountsService); @@ -745,7 +744,7 @@ describe('createPartnerAccount', () => { }, }); - // DB上に容易されたデータが想定通りであるか確認 + // DB上に用意されたデータが想定通りであるか確認 { const accounts = await getAccounts(source); expect(accounts.length).toBe(1); @@ -815,7 +814,7 @@ describe('createPartnerAccount', () => { }, }); - // DB上に容易されたデータが想定通りであるか確認 + // DB上に用意されたデータが想定通りであるか確認 { const accounts = await getAccounts(source); expect(accounts.length).toBe(1); @@ -851,6 +850,507 @@ describe('createPartnerAccount', () => { } }); + it('DBへの通信失敗によって500エラーが発生した場合、リカバリ処理としてADB2Cユーザーを削除され、500エラーが返却される', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const b2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + + // 第一階層のアカウントを作成 + const { tier } = await createAccountAndAdminUser(source, adminExternalId); + + const context = makeContext('uuid'); + const companyName = 'test_company_name'; + const country = 'US'; + const email = 'partner@example.com'; + const adminName = 'partner_admin'; + const partnerExternalId = 'partner_external_id'; + + overrideAdB2cService(service, { + createUser: async () => { + return { sub: partnerExternalId }; + }, + deleteUser: jest.fn(), + }); + + overrideSendgridService(service, { + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + sendMail: async () => { + return; + }, + }); + overrideAccountsRepositoryService(service, { + createAccount: async () => { + throw new Error('DB Error'); + }, + }); + + overrideBlobstorageService(service, { + createContainer: async () => { + return; + }, + }); + + // DB上に用意されたデータが想定通りであるか確認 + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + const users = await getUsers(source); + expect(users.length).toBe(1); + } + + try { + await service.createPartnerAccount( + context, + companyName, + country, + email, + adminName, + adminExternalId, + tier, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + + // DB上に作成されたデータが想定通りであるか確認 + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + expect(accounts[0].tier).toBe(1); + const users = await getUsers(source); + expect(users.length).toBe(1); + expect(users[0].external_id).toBe(adminExternalId); + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); + } + }); + + it('DBへの通信失敗によって500エラーが発生した場合、リカバリ処理が実行されるが、ADB2Cユーザー削除で失敗した場合、500エラーが返却される', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const b2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + + // 第一階層のアカウントを作成 + const { tier } = await createAccountAndAdminUser(source, adminExternalId); + + const context = makeContext('uuid'); + const companyName = 'test_company_name'; + const country = 'US'; + const email = 'partner@example.com'; + const adminName = 'partner_admin'; + const partnerExternalId = 'partner_external_id'; + + overrideAdB2cService(service, { + createUser: async () => { + return { sub: partnerExternalId }; + }, + deleteUser: jest.fn().mockRejectedValue(new Error('ADB2C Error')), + }); + + overrideSendgridService(service, { + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + sendMail: async () => { + return; + }, + }); + overrideAccountsRepositoryService(service, { + createAccount: async () => { + throw new Error('DB Error'); + }, + }); + + overrideBlobstorageService(service, { + createContainer: async () => { + return; + }, + }); + + // DB上に用意されたデータが想定通りであるか確認 + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + const users = await getUsers(source); + expect(users.length).toBe(1); + } + + try { + await service.createPartnerAccount( + context, + companyName, + country, + email, + adminName, + adminExternalId, + tier, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + + // DB上に作成されたデータが想定通りであるか確認 + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + expect(accounts[0].tier).toBe(1); + const users = await getUsers(source); + expect(users.length).toBe(1); + expect(users[0].external_id).toBe(adminExternalId); + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); + } + }); + + it('BlobStorageへの通信失敗が原因でアカウントの追加に失敗した場合、リカバリ処理としてADB2C,DB上のデータが削除され、500エラーが返却される', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const b2cService = module.get(AdB2cService); + + const parentExternalId = 'parent_external_id'; + + await createAccountAndAdminUser(source, parentExternalId); + + const context = makeContext(parentExternalId); + const partnerExternalId = 'partner_external_id'; + const companyName = 'partner_company_name'; + const country = 'US'; + const email = 'partner@example.com'; + const username = 'partner_username'; + + overrideAdB2cService(service, { + createUser: async () => { + return { sub: partnerExternalId }; + }, + deleteUser: jest.fn(), + }); + + overrideSendgridService(service, { + sendMail: async () => { + return; + }, + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + }); + + overrideBlobstorageService(service, { + createContainer: async () => { + throw new Error(); + }, + }); + + // DB上に用意されたデータが想定通りであるか確認 + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + } + + try { + await service.createPartnerAccount( + context, + companyName, + country, + email, + username, + parentExternalId, + TIERS.TIER1, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + expect(accounts[0].tier).toBe(1); + const users = await getUsers(source); + expect(users.length).toBe(1); + expect(users[0].external_id).toBe(parentExternalId); + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); + } + }); + + it('BlobStorageへの通信失敗が原因でアカウントの追加に失敗した場合、リカバリ処理が実行されるが、そのリカバリ処理に失敗した場合、500エラーが返却される', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const b2cService = module.get(AdB2cService); + + const parentExternalId = 'parent_external_id'; + + await createAccountAndAdminUser(source, parentExternalId); + + const context = makeContext(parentExternalId); + const partnerExternalId = 'partner_external_id'; + const companyName = 'partner_company_name'; + const country = 'US'; + const email = 'partner@example.com'; + const username = 'partner_username'; + + overrideAdB2cService(service, { + createUser: async () => { + return { sub: partnerExternalId }; + }, + deleteUser: jest.fn().mockRejectedValue(new Error('ADB2C Error')), + }); + + overrideSendgridService(service, { + sendMail: async () => { + return; + }, + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + }); + + overrideAccountsRepositoryService(service, { + deleteAccount: jest.fn().mockRejectedValue(new Error('DB Error')), + }); + + overrideBlobstorageService(service, { + createContainer: async () => { + throw new Error(); + }, + }); + + // DB上に用意されたデータが想定通りであるか確認 + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + } + + try { + await service.createPartnerAccount( + context, + companyName, + country, + email, + username, + parentExternalId, + TIERS.TIER1, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + + { + // DB上に作成されたデータが想定通りであるか確認 + // リカバリ処理が失敗した場合、DB上のデータは削除されない + const accounts = await getAccounts(source); + expect(accounts.length).toBe(2); + expect(accounts[0].tier).toBe(1); + expect(accounts[1].tier).toBe(2); + const users = await getUsers(source); + expect(users.length).toBe(2); + expect(users[0].external_id).toBe(parentExternalId); + expect(users[1].external_id).toBe(partnerExternalId); + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); + } + }); + + it('SendGridへの通信失敗によって500エラーが発生した場合、リカバリ処理としてADB2C,DB上のデータ,コンテナが削除され、500エラーが返却される', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const b2cService = module.get(AdB2cService); + const blobstorageService = + module.get(BlobstorageService); + + const parentExternalId = 'parent_external_id'; + + await createAccountAndAdminUser(source, parentExternalId); + + const context = makeContext(parentExternalId); + const partnerExternalId = 'partner_external_id'; + const companyName = 'partner_company_name'; + const country = 'US'; + const email = 'partner@example.com'; + const username = 'partner_username'; + + overrideAdB2cService(service, { + createUser: async () => { + return { sub: partnerExternalId }; + }, + deleteUser: jest.fn(), + }); + + overrideSendgridService(service, { + sendMail: async () => { + throw new Error(); + }, + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + }); + + overrideBlobstorageService(service, { + createContainer: async () => { + return; + }, + deleteContainer: jest.fn(), + }); + + // DB上に用意されたデータが想定通りであるか確認 + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + } + + try { + await service.createPartnerAccount( + context, + companyName, + country, + email, + username, + parentExternalId, + TIERS.TIER1, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + + { + // DB上に作成されたデータが想定通りであるか確認 + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + expect(accounts[0].tier).toBe(1); + const users = await getUsers(source); + expect(users.length).toBe(1); + expect(users[0].external_id).toBe(parentExternalId); + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); + // コンテナ削除メソッドが呼ばれているか確認 + expect(blobstorageService.deleteContainer).toBeCalledWith( + context, + 2, //新規作成したパートナーのアカウントID + country, + ); + } + }); + + it('SendGridへの通信失敗によって500エラーが発生した場合、リカバリ処理が実行されるが、そのリカバリ処理に失敗した場合、500エラーが返却される', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const b2cService = module.get(AdB2cService); + const blobstorageService = + module.get(BlobstorageService); + + const parentExternalId = 'parent_external_id'; + + await createAccountAndAdminUser(source, parentExternalId); + + const context = makeContext(parentExternalId); + const partnerExternalId = 'partner_external_id'; + const companyName = 'partner_company_name'; + const country = 'US'; + const email = 'partner@example.com'; + const username = 'partner_username'; + + overrideAdB2cService(service, { + createUser: async () => { + return { sub: partnerExternalId }; + }, + deleteUser: jest.fn().mockRejectedValue(new Error('ADB2C Error')), + }); + + overrideSendgridService(service, { + sendMail: async () => { + throw new Error(); + }, + createMailContentFromEmailConfirmForNormalUser: async () => { + return { html: '', text: '', subject: '' }; + }, + }); + + overrideAccountsRepositoryService(service, { + deleteAccount: jest.fn().mockRejectedValue(new Error('DB Error')), + }); + + overrideBlobstorageService(service, { + createContainer: async () => { + return; + }, + deleteContainer: jest.fn().mockRejectedValue(new Error('Blob Error')), + }); + + // DB上に用意されたデータが想定通りであるか確認 + { + const accounts = await getAccounts(source); + expect(accounts.length).toBe(1); + } + + try { + await service.createPartnerAccount( + context, + companyName, + country, + email, + username, + parentExternalId, + TIERS.TIER1, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + + { + // DB上に作成されたデータが想定通りであるか確認 + // リカバリ処理が失敗したため、DB上のデータは削除されていない + const accounts = await getAccounts(source); + expect(accounts.length).toBe(2); + expect(accounts[0].tier).toBe(1); + expect(accounts[1].tier).toBe(2); + const users = await getUsers(source); + expect(users.length).toBe(2); + expect(users[0].external_id).toBe(parentExternalId); + expect(users[1].external_id).toBe(partnerExternalId); + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); + // コンテナ削除メソッドが呼ばれているか確認 + expect(blobstorageService.deleteContainer).toBeCalledWith( + context, + 2, //新規作成したパートナーのアカウントID + country, + ); + } + }); + it('既に登録済みのメールアドレスが原因でアカウントの追加に失敗した場合、エラーとなる(400エラー)', async () => { const module = await makeTestingModule(source); const service = module.get(AccountsService); @@ -887,7 +1387,7 @@ describe('createPartnerAccount', () => { }, }); - // DB上に容易されたデータが想定通りであるか確認 + // DB上に用意されたデータが想定通りであるか確認 { const accounts = await getAccounts(source); expect(accounts.length).toBe(1); @@ -922,61 +1422,6 @@ describe('createPartnerAccount', () => { expect(users.length).toBe(1); } }); - - it('BlobStorageへの通信失敗が原因でアカウントの追加に失敗した場合、エラーとなる(500エラー)', async () => { - const module = await makeTestingModule(source); - const service = module.get(AccountsService); - - const parentExternalId = 'parent_external_id'; - - await createAccountAndAdminUser(source, parentExternalId); - - const context = makeContext(parentExternalId); - const partnerExternalId = 'partner_external_id'; - const companyName = 'partner_company_name'; - const country = 'US'; - const email = 'partner@example.com'; - const username = 'partner_username'; - - overrideAdB2cService(service, { - createUser: async () => { - return { sub: partnerExternalId }; - }, - }); - - overrideSendgridService(service, { - sendMail: async () => { - return; - }, - createMailContentFromEmailConfirmForNormalUser: async () => { - return { html: '', text: '', subject: '' }; - }, - }); - overrideBlobstorageService(service, { - createContainer: async () => { - throw new Error(); - }, - }); - - try { - await service.createPartnerAccount( - context, - companyName, - country, - email, - username, - parentExternalId, - TIERS.TIER1, - ); - } catch (e) { - if (e instanceof HttpException) { - expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR); - expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); - } else { - fail(); - } - } - }); }); describe('AccountsService', () => { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 0246e02..7c1cc7b 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -530,6 +530,8 @@ export class AccountsService { ); } + let account: Account; + let user: User; try { // アカウントと管理者をセットで作成 const { newAccount, adminUser } = @@ -542,19 +544,50 @@ export class AccountsService { USER_ROLES.NONE, null, ); + account = newAccount; + user = adminUser; + } catch (e) { + this.logger.error(`error=${e}`); + this.logger.error('create partner account failed'); + //リカバリ処理 + // idpのユーザーを削除 + await this.deleteAdB2cUser(externalUser.sub, context); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + try { // 新規作成アカウント用のBlobコンテナを作成 await this.blobStorageService.createContainer( context, - newAccount.id, + account.id, country, ); + } catch (e) { + this.logger.error(`error=${e}`); + this.logger.error('create partner container failed'); + //リカバリ処理 + // idpのユーザーを削除 + await this.deleteAdB2cUser(externalUser.sub, context); + // DBのアカウントとユーザーを削除 + await this.deleteAccount(account.id, user.id, context); + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + try { const from = this.configService.get('MAIL_FROM') || ''; const { subject, text, html } = await this.sendgridService.createMailContentFromEmailConfirmForNormalUser( - newAccount.id, - adminUser.id, + account.id, + user.id, email, ); await this.sendgridService.sendMail( @@ -565,10 +598,20 @@ export class AccountsService { text, html, ); - return { accountId: newAccount.id }; + return { accountId: account.id }; } catch (e) { this.logger.error(`error=${e}`); - this.logger.error('create partner account failed'); + this.logger.error('create partner account send mail failed'); + //リカバリ処理 + // idpのユーザーを削除 + await this.deleteAdB2cUser(externalUser.sub, context); + + // DBのアカウントを削除 + await this.deleteAccount(account.id, user.id, context); + + // Blobコンテナを削除 + await this.deleteBlobContainer(account.id, country, context); + throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR,