From a8bacefc5f1fd9440d098f628a773d4b7c32e68e Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Thu, 5 Oct 2023 08:16:00 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20461:=20API=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E5=AE=9F=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2672: APIテスト実施](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2672) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 詳細なレコード(ライセンス、タスク、ユーザーグループなど)は別途dev動作確認にてデータを用意して行います。 現時点では、各レコードの削除はMySQL用にmigrationファイルにて記述したON DELETE CASCADEの機能にて削除を行う為、SQLiteを用いた本ユニットテストでは動作確認対象外としています。 - 影響範囲(他の機能にも影響があるか) entityの定義(accounts - users)のON DELETE CASCADEを明記 ## レビューポイント - 本ユニットテストは正常系の動作確認と、それぞれのservice内部で異常発生時もAPI自体は正常終了し、[MANUAL_RECOVERY_REQUIRED]ログが表示されることの確認を主な目的として実装しています。 ## UIの変更 なし ## 動作確認状況 - ローカルで確認(ユニットテスト) ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/common/test/overrides.ts | 14 ++ .../accounts/accounts.service.spec.ts | 230 +++++++++++++++++- .../accounts/accounts.repository.service.ts | 2 - .../repositories/users/entity/user.entity.ts | 2 +- 4 files changed, 244 insertions(+), 4 deletions(-) diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index 31ce97c..be8404f 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -29,6 +29,7 @@ export const overrideAdB2cService = ( username: string, ) => Promise<{ sub: string } | ConflictError>; deleteUser?: (externalId: string, context: Context) => Promise; + deleteUsers?: (externalIds: string[], context: Context) => Promise; getUsers?: ( context: Context, externalIds: string[], @@ -49,6 +50,12 @@ export const overrideAdB2cService = ( writable: true, }); } + if (overrides.deleteUsers) { + Object.defineProperty(obj, obj.deleteUsers.name, { + value: overrides.deleteUsers, + writable: true, + }); + } if (overrides.getUsers) { Object.defineProperty(obj, obj.getUsers.name, { value: overrides.getUsers, @@ -232,6 +239,7 @@ export const overrideAccountsRepositoryService = ( adminUserAcceptedTermsVersion: string, ) => Promise<{ newAccount: Account; adminUser: User }>; deleteAccount?: (accountId: number, userId: number) => Promise; + deleteAccountAndInsertArchives?: (accountId: number) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -248,4 +256,10 @@ export const overrideAccountsRepositoryService = ( writable: true, }); } + if (overrides.deleteAccountAndInsertArchives) { + Object.defineProperty(obj, obj.deleteAccountAndInsertArchives.name, { + value: overrides.deleteAccountAndInsertArchives, + writable: true, + }); + } }; diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 170f607..e911f36 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -34,6 +34,7 @@ import { getUsers, makeTestUser, makeHierarchicalAccounts, + getUser, } from '../../common/test/utility'; import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; @@ -5205,7 +5206,6 @@ describe('getAccountInfo', () => { } }); }); - describe('getAuthors', () => { let source: DataSource = null; beforeEach(async () => { @@ -5312,3 +5312,231 @@ describe('getAuthors', () => { } }); }); +describe('deleteAccountAndData', () => { + let source: DataSource = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + await source.destroy(); + source = null; + }); + it('アカウント情報が削除されること', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const tier4Accounts = await makeHierarchicalAccounts(source); + const { account: account1, admin: admin1 } = await makeTestAccount(source, { + parent_account_id: tier4Accounts.tier4Accounts[0].account.id, + }); + const account = account1; + const admin = admin1; + const context = makeContext(admin.external_id); + // 第五階層のアカウント作成 + const tier5Accounts = await makeTestAccount(source, { + parent_account_id: account.id, + tier: 5, + }); + + // ユーザの作成 + const user = await makeTestUser(source, { + account_id: tier5Accounts.account.id, + }); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + }); + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + // アカウント情報の削除 + await service.deleteAccountAndData( + context, + tier5Accounts.admin.external_id, + tier5Accounts.account.id, + ); + + // DB内が想定通りになっているか確認 + const accountRecord = await getAccount(source, tier5Accounts.account.id); + expect(accountRecord).toBe(null); + + const userRecord = await getUser(source, user.id); + expect(userRecord).toBe(null); + }); + it('アカウントの削除に失敗した場合はエラーを返す', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); + // 第五階層のアカウント作成 + const tier4Accounts = await makeHierarchicalAccounts(source); + const { account: account1, admin: admin1 } = await makeTestAccount(source, { + parent_account_id: tier4Accounts.tier4Accounts[0].account.id, + }); + const account = account1; + const admin = admin1; + const context = makeContext(admin.external_id); + // 第五階層のアカウント作成 + const tier5Accounts = await makeTestAccount(source, { + parent_account_id: account.id, + tier: 5, + }); + + // ユーザの作成 + const user = await makeTestUser(source, { + account_id: tier5Accounts.account.id, + }); + + // アカウント情報の削除失敗 + overrideAccountsRepositoryService(service, { + deleteAccountAndInsertArchives: jest.fn().mockRejectedValue(new Error()), + }); + + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // アカウント情報の削除に失敗することを確認 + await expect( + service.deleteAccountAndData( + context, + tier5Accounts.admin.external_id, + tier5Accounts.account.id, + ), + ).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); + + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が削除されていないことを確認 + const accountRecord = await getAccount(source, tier5Accounts.account.id); + expect(accountRecord.id).not.toBeNull(); + const userRecord = await getUser(source, user.id); + expect(userRecord.id).not.toBeNull(); + }); + it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); + // 第五階層のアカウント作成 + const tier4Accounts = await makeHierarchicalAccounts(source); + const { account: account1, admin: admin1 } = await makeTestAccount(source, { + parent_account_id: tier4Accounts.tier4Accounts[0].account.id, + }); + const account = account1; + const admin = admin1; + const context = makeContext(admin.external_id); + // 第五階層のアカウント作成 + const tier5Accounts = await makeTestAccount(source, { + parent_account_id: account.id, + tier: 5, + }); + + // ユーザの作成 + const user = await makeTestUser(source, { + account_id: tier5Accounts.account.id, + }); + + // ADB2Cユーザーの削除失敗 + overrideAdB2cService(service, { + deleteUsers: jest.fn().mockRejectedValue(new Error()), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 処理自体は成功することを確認 + expect( + await service.deleteAccountAndData( + context, + tier5Accounts.admin.external_id, + tier5Accounts.account.id, + ), + ).toEqual(undefined); + + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が想定通りになっているか確認 + const accountRecord = await getAccount(source, tier5Accounts.account.id); + expect(accountRecord).toBe(null); + const userRecord = await getUser(source, user.id); + expect(userRecord).toBe(null); + }); + it('blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); + + // 第五階層のアカウント作成 + const tier4Accounts = await makeHierarchicalAccounts(source); + const { account: account1, admin: admin1 } = await makeTestAccount(source, { + parent_account_id: tier4Accounts.tier4Accounts[0].account.id, + }); + const account = account1; + const admin = admin1; + const context = makeContext(admin.external_id); + // 第五階層のアカウント作成 + const tier5Accounts = await makeTestAccount(source, { + parent_account_id: account.id, + tier: 5, + }); + + // ユーザの作成 + const user = await makeTestUser(source, { + account_id: tier5Accounts.account.id, + }); + + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除失敗 + overrideBlobstorageService(service, { + deleteContainer: jest.fn().mockRejectedValue(new Error()), + }); + + // 処理自体は成功することを確認 + expect( + await service.deleteAccountAndData( + context, + tier5Accounts.admin.external_id, + tier5Accounts.account.id, + ), + ).toEqual(undefined); + + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が想定通りになっているか確認 + const accountRecord = await getAccount(source, tier5Accounts.account.id); + expect(accountRecord).toBe(null); + const userRecord = await getUser(source, user.id); + expect(userRecord).toBe(null); + }); +}); diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 510a865..2df7ab7 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -916,7 +916,6 @@ export class AccountsRepositoryService { account_id: accountId, }, }); - const userArchiveRepo = entityManager.getRepository(UserArchive); await userArchiveRepo .createQueryBuilder() @@ -924,7 +923,6 @@ export class AccountsRepositoryService { .into(UserArchive) .values(users) .execute(); - // アカウントを削除 // アカウントを削除することで、外部キー制約がで紐づいている関連テーブルのデータも削除される const accountRepo = entityManager.getRepository(Account); diff --git a/dictation_server/src/repositories/users/entity/user.entity.ts b/dictation_server/src/repositories/users/entity/user.entity.ts index 9e375df..007be46 100644 --- a/dictation_server/src/repositories/users/entity/user.entity.ts +++ b/dictation_server/src/repositories/users/entity/user.entity.ts @@ -70,7 +70,7 @@ export class User { @UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定 updated_at: Date; - @ManyToOne(() => Account, (account) => account.user) + @ManyToOne(() => Account, (account) => account.user, { onDelete: 'CASCADE' }) // onDeleteはSQLite用設定値.本番用は別途migrationで設定 @JoinColumn({ name: 'account_id' }) account?: Account;