Merged PR 337: API修正(ユーザー追加)&テスト実装

## 概要
[Task2398: API修正(ユーザー追加)&テスト実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2398)

- ユーザー追加のリカバリ処理を実装
  - ADB2Cに追加したユーザー削除
  - DBに追加したユーザー削除

## レビューポイント
- リカバリ処理に不足はないか
- ログはこれでよいか
- テストケースはこれで足りているか
  - ADB2Cのユーザー削除:OK , DB上のユーザーは削除:NO のケースはいるか等

## UIの変更

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

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-08-21 08:25:57 +00:00
parent 6f92ef9453
commit 76ed87d82a
4 changed files with 352 additions and 7 deletions

View File

@ -25,6 +25,7 @@ export const overrideAdB2cService = <TService>(
password: string,
username: string,
) => Promise<{ sub: string } | ConflictError>;
deleteUser?: (externalId: string, context: Context) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -35,6 +36,12 @@ export const overrideAdB2cService = <TService>(
writable: true,
});
}
if (overrides.deleteUser) {
Object.defineProperty(obj, obj.deleteUser.name, {
value: overrides.deleteUser,
writable: true,
});
}
};
/**
@ -115,6 +122,7 @@ export const overrideUsersRepositoryService = <TService>(
service: TService,
overrides: {
createNormalUser?: (user: newUser) => Promise<User>;
deleteNormalUser?: (userId: number) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -125,6 +133,12 @@ export const overrideUsersRepositoryService = <TService>(
writable: true,
});
}
if (overrides.deleteNormalUser) {
Object.defineProperty(obj, obj.deleteNormalUser.name, {
value: overrides.deleteNormalUser,
writable: true,
});
}
};
/**

View File

@ -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>(UsersService);
const b2cService = module.get<AdB2cService>(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>(UsersService);
const b2cService = module.get<AdB2cService>(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>(UsersService);
const b2cService = module.get<AdB2cService>(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>(UsersService);
const b2cService = module.get<AdB2cService>(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>(UsersService);
const b2cService = module.get<AdB2cService>(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'),
);
});
});

View File

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

View File

@ -381,4 +381,22 @@ export class UsersRepositoryService {
return typists;
});
}
/**
* UserID指定のユーザーとソート条件を同時に削除する
* @param userId
* @returns delete
*/
async deleteNormalUser(userId: number): Promise<void> {
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 });
});
}
}