Merged PR 461: APIテスト実施

## 概要
[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の変更
なし

## 動作確認状況
- ローカルで確認(ユニットテスト)

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2023-10-05 08:16:00 +00:00
parent 964077a480
commit a8bacefc5f
4 changed files with 244 additions and 4 deletions

View File

@ -29,6 +29,7 @@ export const overrideAdB2cService = <TService>(
username: string,
) => Promise<{ sub: string } | ConflictError>;
deleteUser?: (externalId: string, context: Context) => Promise<void>;
deleteUsers?: (externalIds: string[], context: Context) => Promise<void>;
getUsers?: (
context: Context,
externalIds: string[],
@ -49,6 +50,12 @@ export const overrideAdB2cService = <TService>(
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 = <TService>(
adminUserAcceptedTermsVersion: string,
) => Promise<{ newAccount: Account; adminUser: User }>;
deleteAccount?: (accountId: number, userId: number) => Promise<void>;
deleteAccountAndInsertArchives?: (accountId: number) => Promise<User[]>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -248,4 +256,10 @@ export const overrideAccountsRepositoryService = <TService>(
writable: true,
});
}
if (overrides.deleteAccountAndInsertArchives) {
Object.defineProperty(obj, obj.deleteAccountAndInsertArchives.name, {
value: overrides.deleteAccountAndInsertArchives,
writable: true,
});
}
};

View File

@ -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>(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>(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>(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>(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);
});
});

View File

@ -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);

View File

@ -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;