Merged PR 335: API修正(アカウント登録)&テスト実装

## 概要
[Task2369: API修正(アカウント登録)&テスト実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2369)

- リカバリ処理を実装
- テスト追加

## レビューポイント
- テストケースは足りているか
- リカバリ処理を追加したが、漏れはないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-08-21 09:21:04 +00:00
parent 76ed87d82a
commit b8c2640719
5 changed files with 545 additions and 20 deletions

View File

@ -7,6 +7,8 @@ import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { User, newUser } from '../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { Account } from '../../repositories/accounts/entity/account.entity';
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
@ -155,6 +157,11 @@ export const overrideBlobstorageService = <TService>(
accountId: number,
country: string,
) => Promise<void>;
deleteContainer?: (
context: Context,
accountId: number,
country: string,
) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -165,4 +172,47 @@ export const overrideBlobstorageService = <TService>(
writable: true,
});
}
if (overrides.deleteContainer) {
Object.defineProperty(obj, obj.deleteContainer.name, {
value: overrides.deleteContainer,
writable: true,
});
}
};
/**
* accountsRepositoryのモックを作成してTServiceが依存するサービス(AccountsRepositoryService)
* serviceに指定するオブジェクトは`accountsRepository: AccountsRepositoryService`
* @param service TService
* @param overrides accountsRepositoryの各種メソッドのモックが返す値
*/
export const overrideAccountsRepositoryService = <TService>(
service: TService,
overrides: {
createAccount?: (
companyName: string,
country: string,
dealerAccountId: number | undefined,
tier: number,
adminExternalUserId: string,
adminUserRole: string,
adminUserAcceptedTermsVersion: string,
) => Promise<{ newAccount: Account; adminUser: User }>;
deleteAccount?: (accountId: number, userId: number) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
const obj = (service as any).accountRepository as AccountsRepositoryService;
if (overrides.deleteAccount) {
Object.defineProperty(obj, obj.deleteAccount.name, {
value: overrides.deleteAccount,
writable: true,
});
}
if (overrides.createAccount) {
Object.defineProperty(obj, obj.createAccount.name, {
value: overrides.createAccount,
writable: true,
});
}
};

View File

@ -21,19 +21,23 @@ import {
getUserFromExternalID,
getAccounts,
getUsers,
getSortCriteria,
createAccountAndAdminUser,
} from './test/utility';
import { DataSource } from 'typeorm';
import { makeTestingModule } from '../../common/test/modules';
import { AccountsService } from './accounts.service';
import { makeContext } from '../../common/log';
import { Context, makeContext } from '../../common/log';
import { TIERS } from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity';
import {
overrideAccountsRepositoryService,
overrideAdB2cService,
overrideBlobstorageService,
overrideSendgridService,
} from '../../common/test/overrides';
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
describe('createAccount', () => {
let source: DataSource = null;
@ -247,10 +251,10 @@ describe('createAccount', () => {
expect(users.length).toBe(0);
});
it('アカウントを作成がBlobStorageへの通信失敗によって失敗すると500エラーが発生する', async () => {
it('アカウントを作成がDBへの通信失敗によって500エラーが発生した場合、リカバリ処理としてADB2Cユーザーを削除され、500エラーが返却される', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const b2cService = module.get<AdB2cService>(AdB2cService);
const externalId = 'test_external_id';
const companyName = 'test_company_name';
const country = 'US';
@ -265,6 +269,135 @@ describe('createAccount', () => {
createUser: async () => {
return { sub: externalId };
},
deleteUser: jest.fn(),
});
overrideSendgridService(service, {});
overrideAccountsRepositoryService(service, {
createAccount: async () => {
throw new Error();
},
});
try {
await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
email,
password,
username,
role,
acceptedTermsVersion,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
// DB内が想定通りになっているか確認
// DBのデータ作成で失敗しているので、DB内は空
const accounts = await getAccounts(source);
expect(accounts.length).toBe(0);
const users = await getUsers(source);
expect(users.length).toBe(0);
const sortCriteria = await getSortCriteria(source);
expect(sortCriteria.length).toBe(0);
// ADB2Cユーザー削除メソッドが呼ばれているか確認
expect(b2cService.deleteUser).toBeCalledWith(
externalId,
makeContext('uuid'),
);
});
it('アカウントを作成がDBへの通信失敗によって500エラーが発生した場合、リカバリ処理が実行されるが、ADB2Cユーザー削除で失敗した場合、500エラーが返却される', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const b2cService = module.get<AdB2cService>(AdB2cService);
const externalId = 'test_external_id';
const companyName = 'test_company_name';
const country = 'US';
const dealerAccountId = 1;
const email = 'dummy@dummy.dummy';
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
return { sub: externalId };
},
deleteUser: jest.fn().mockRejectedValue(new Error()),
});
overrideSendgridService(service, {});
overrideAccountsRepositoryService(service, {
createAccount: async () => {
throw new Error();
},
});
try {
await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
email,
password,
username,
role,
acceptedTermsVersion,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
// DB内が想定通りになっているか確認
// DBのデータ作成で失敗しているので、DB内は空
const accounts = await getAccounts(source);
expect(accounts.length).toBe(0);
const users = await getUsers(source);
expect(users.length).toBe(0);
const sortCriteria = await getSortCriteria(source);
expect(sortCriteria.length).toBe(0);
// ADB2Cユーザー削除メソッドが呼ばれているか確認
expect(b2cService.deleteUser).toBeCalledWith(
externalId,
makeContext('uuid'),
);
});
it('アカウントを作成がBlobStorageへの通信失敗によって500エラーが発生した場合、リカバリ処理としてADB2C,DB上のデータが削除され、500エラーが返却される', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const b2cService = module.get<AdB2cService>(AdB2cService);
b2cService.deleteUser = jest.fn(); // リカバリ処理の確認のため、deleteUserをモック化
const externalId = 'test_external_id';
const companyName = 'test_company_name';
const country = 'US';
const dealerAccountId = 1;
const email = 'dummy@dummy.dummy';
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
return { sub: externalId };
},
deleteUser: async () => {
return;
},
});
overrideSendgridService(service, {});
overrideBlobstorageService(service, {
@ -290,9 +423,270 @@ describe('createAccount', () => {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
expect(true).toBe(false); // ここには来てはいけない
fail();
}
}
// DB内が想定通りになっているか確認
// リカバリ処理が走っているため、アカウント・ユーザーは削除されている
const accounts = await getAccounts(source);
expect(accounts.length).toBe(0);
const users = await getUsers(source);
expect(users.length).toBe(0);
const sortCriteria = await getSortCriteria(source);
expect(sortCriteria.length).toBe(0);
// ADB2Cユーザー削除メソッドが呼ばれているか確認
expect(b2cService.deleteUser).toBeCalledWith(
externalId,
makeContext('uuid'),
);
});
it('アカウントを作成がBlobStorageへの通信失敗によって500エラーが発生した場合、リカバリ処理が実行されるが、そのリカバリ処理に失敗した場合、500エラーが返却される', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const b2cService = module.get<AdB2cService>(AdB2cService);
const externalId = 'test_external_id';
const companyName = 'test_company_name';
const country = 'US';
const dealerAccountId = 1;
const email = 'dummy@dummy.dummy';
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
return { sub: externalId };
},
deleteUser: jest.fn().mockRejectedValue(new Error()),
});
overrideSendgridService(service, {});
overrideBlobstorageService(service, {
createContainer: async () => {
throw new Error();
},
});
overrideAccountsRepositoryService(service, {
deleteAccount: async () => {
throw new Error();
},
});
try {
await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
email,
password,
username,
role,
acceptedTermsVersion,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
// DB内が想定通りになっているか確認
// DB上のデータのリカバリ処理に失敗したため、DB上のデータは削除されない
const accounts = await getAccounts(source);
expect(accounts.length).toBe(1);
const users = await getUsers(source);
expect(users.length).toBe(1);
const sortCriteria = await getSortCriteria(source);
expect(sortCriteria.length).toBe(1);
// ADB2Cユーザー削除メソッドが呼ばれているか確認
expect(b2cService.deleteUser).toBeCalledWith(
externalId,
makeContext('uuid'),
);
});
it('アカウントを作成がSendGridへの通信失敗によって500エラーが発生した場合、リカバリ処理としてADB2C,DB上のデータとBlobストレージのコンテナが削除され、500エラーが返却される', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const b2cService = module.get<AdB2cService>(AdB2cService);
const blobstorageService =
module.get<BlobstorageService>(BlobstorageService);
const externalId = 'test_external_id';
const companyName = 'test_company_name';
const country = 'US';
const dealerAccountId = 1;
const email = 'dummy@dummy.dummy';
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async (
_context: Context,
_email: string,
_password: string,
_username: string,
) => {
// ユーザー作成時に指定したパラメータが正しく渡されていることを確認
expect(email).toEqual(_email);
expect(username).toEqual(_username);
return { sub: externalId };
},
deleteUser: jest.fn(),
});
overrideSendgridService(service, {
sendMail: async () => {
throw new Error();
},
createMailContentFromEmailConfirm: async () => {
return {
html: 'dummy_html',
subject: 'dummy_subject',
text: 'dummy_text',
};
},
});
overrideBlobstorageService(service, {
createContainer: async () => {
return;
},
deleteContainer: jest.fn(),
});
overrideAccountsRepositoryService(service, {});
try {
await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
email,
password,
username,
role,
acceptedTermsVersion,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
// DB内が想定通りになっているか確認
// リカバリ処理によってADB2C,DB上のデータとBlobストレージのコンテナが削除される
const accounts = await getAccounts(source);
expect(accounts.length).toBe(0);
const users = await getUsers(source);
expect(users.length).toBe(0);
const sortCriteria = await getSortCriteria(source);
expect(sortCriteria.length).toBe(0);
// ADB2Cユーザー削除メソッドが呼ばれているか確認
expect(b2cService.deleteUser).toBeCalledWith(
externalId,
makeContext('uuid'),
);
// Blobストレージのコンテナ削除メソッドが呼ばれているか確認
expect(blobstorageService.deleteContainer).toBeCalledWith(
makeContext('uuid'),
1, //新規作成したアカウントのID
country,
);
});
it('アカウントを作成がSendGridへの通信失敗によって500エラーが発生した場合、リカバリ処理が実行されるが、そのリカバリ処理に失敗した場合、500エラーが返却される', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const b2cService = module.get<AdB2cService>(AdB2cService);
const blobstorageService =
module.get<BlobstorageService>(BlobstorageService);
const externalId = 'test_external_id';
const companyName = 'test_company_name';
const country = 'US';
const dealerAccountId = 1;
const email = 'dummy@dummy.dummy';
const password = 'dummy_password';
const username = 'dummy_username';
const role = 'none';
const acceptedTermsVersion = '1.0.0';
overrideAdB2cService(service, {
createUser: async () => {
return { sub: externalId };
},
deleteUser: jest.fn().mockRejectedValue(new Error()),
});
overrideSendgridService(service, {
sendMail: async () => {
throw new Error();
},
createMailContentFromEmailConfirm: async () => {
return {
html: 'dummy_html',
subject: 'dummy_subject',
text: 'dummy_text',
};
},
});
overrideBlobstorageService(service, {
createContainer: async () => {
return;
},
deleteContainer: jest
.fn()
.mockRejectedValue(new Error('BlobStorage Error')),
});
overrideAccountsRepositoryService(service, {
deleteAccount: async () => {
throw new Error();
},
});
try {
await service.createAccount(
makeContext('uuid'),
companyName,
country,
dealerAccountId,
email,
password,
username,
role,
acceptedTermsVersion,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
// DB内が想定通りになっているか確認
// リカバリ処理によってADB2C,DB上のデータとBlobストレージのコンテナが削除されない
const accounts = await getAccounts(source);
expect(accounts.length).toBe(1);
const users = await getUsers(source);
expect(users.length).toBe(1);
const sortCriteria = await getSortCriteria(source);
expect(sortCriteria.length).toBe(1);
// ADB2Cユーザー削除メソッドが呼ばれているか確認
expect(b2cService.deleteUser).toBeCalledWith(
externalId,
makeContext('uuid'),
);
// Blobストレージのコンテナ削除メソッドが呼ばれているか確認
expect(blobstorageService.deleteContainer).toBeCalledWith(
makeContext('uuid'),
1, //新規作成したアカウントのID
country,
);
});
});

View File

@ -190,9 +190,10 @@ export class AccountsService {
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('create account failed');
this.logger.error(
`[NOT IMPLEMENT] [RECOVER] delete account: ${externalUser.sub}`,
);
//リカバリ処理
// idpのユーザーを削除
await this.deleteAdB2cUser(externalUser.sub, context);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -209,6 +210,13 @@ export class AccountsService {
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('create 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,
@ -238,16 +246,18 @@ export class AccountsService {
html,
);
} catch (e) {
console.log(e);
this.logger.error(`error=${e}`);
this.logger.error('create user failed');
this.logger.error(
`[NOT IMPLEMENT] [RECOVER] delete account: ${account.id}`,
);
this.logger.error(
`[NOT IMPLEMENT] [RECOVER] delete externalUser: ${externalUser.sub}`,
);
this.logger.error(`[NOT IMPLEMENT] [RECOVER] delete user: ${user.id}`);
this.logger.error('send E-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,
@ -268,6 +278,67 @@ export class AccountsService {
}
}
// AdB2cのユーザーを削除
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
private async deleteAdB2cUser(
externalUserId: string,
context: Context,
): Promise<void> {
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 deleteAccount(
accountId: number,
userId: number,
context: Context,
): Promise<void> {
try {
await this.accountRepository.deleteAccount(accountId, userId);
this.logger.log(
`[${context.trackingId}] delete account: ${accountId}, user: ${userId}`,
);
} catch (error) {
this.logger.error(`error=${error}`);
this.logger.error(
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
);
}
}
// Blobコンテナを削除
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
private async deleteBlobContainer(
accountId: number,
country: string,
context: Context,
): Promise<void> {
try {
await this.blobStorageService.deleteContainer(
context,
accountId,
country,
);
this.logger.log(
`[${context.trackingId}] delete container: ${accountId}, country: ${country}`,
);
} catch (error) {
this.logger.error(
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
);
}
}
/**
*
* @param token

View File

@ -5,6 +5,7 @@ import {
License,
LicenseOrder,
} from '../../../repositories/licenses/entity/license.entity';
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
// TODO: [PBI 2379] 他のUtilityからコピペしてきたもの。後日整理される前提。
export const createAccountAndAdminUser = async (
@ -162,6 +163,15 @@ export const getUsers = async (dataSource: DataSource): Promise<User[]> => {
return await dataSource.getRepository(User).find();
};
/**
* ユーティリティ: すべてのソート条件を取得する
* @param dataSource
* @returns
*/
export const getSortCriteria = async (dataSource: DataSource) => {
return await dataSource.getRepository(SortCriteria).find();
};
export const createLicense = async (
datasource: DataSource,
accountId: number,

View File

@ -160,14 +160,14 @@ export class AccountsRepositoryService {
const accountsRepo = entityManager.getRepository(Account);
const usersRepo = entityManager.getRepository(User);
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
// アカウントを削除
await accountsRepo.delete({ id: accountId });
// プライマリ管理者を削除
await usersRepo.delete({ id: userId });
// ソート条件を削除
await sortCriteriaRepo.delete({
user_id: userId,
});
// プライマリ管理者を削除
await usersRepo.delete({ id: userId });
// アカウントを削除
await accountsRepo.delete({ id: accountId });
});
}