Merged PR 859: パートナーユーザー取得API実装

## 概要
[Task3936: パートナーユーザー取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3936)
- このPull Requestでの対象/対象外
パートナー変更APIの修正は別タスクで対応
- 影響範囲(他の機能にも影響があるか)
新規APIのため他の機能に影響はない

## レビューポイント
- パートナーのアカウントIDからユーザー一覧を取得する際に、Repository層ではEmai認証状態を意識した取得は行わない
 →service層でフィルタリングする実装にしたが
 (アカウントIDからユーザー一覧を取得する処理がいままでなかったので、あったほうがいいかなと思い)

## クエリの変更
新規APIのためクエリの変更はない

## 動作確認状況
- ローカルで確認
UT+POSTMAN
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか
既存機能には手を入れていない
## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2024-04-03 00:50:53 +00:00 committed by makabe.t
parent 8752448eed
commit 0288292058
7 changed files with 322 additions and 10 deletions

View File

@ -87,4 +87,5 @@ export const ErrorCodes = [
'E017003', // 親アカウント変更不可エラー(リージョンが同一でない)
'E017004', // 親アカウント変更不可エラー(国が同一でない)
'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない)
'E019001', // パートナーアカウント取得不可エラー(階層構造が不正)
] as const;

View File

@ -77,4 +77,5 @@ export const errors: Errors = {
E017003: 'Parent account switch failed Error: region mismatch',
E017004: 'Parent account switch failed Error: country mismatch',
E018001: 'Partner account delete failed Error: not satisfied conditions',
E019001: 'Partner account get failed Error: hierarchy mismatch',
};

View File

@ -2559,13 +2559,8 @@ export class AccountsController {
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
// TODO: 仮実装
/*await this.accountService.getPartnerUsers(
context,
targetAccountId,
);
*/
//仮の返却値
await this.accountService.getPartnerUsers(context, userId, targetAccountId);
return { users: [] };
}

View File

@ -161,7 +161,7 @@ describe('createAccount', () => {
},
});
let _subject: string = '';
let _subject = '';
let _url: string | undefined = '';
overrideSendgridService(service, {
sendMail: async (
@ -6261,7 +6261,7 @@ describe('アカウント情報更新', () => {
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AccountsService>(AccountsService);
let _subject: string = '';
let _subject = '';
let _url: string | undefined = '';
overrideSendgridService(service, {
sendMail: async (
@ -9447,3 +9447,191 @@ describe('deletePartnerAccount', () => {
}
});
});
describe('getPartnerUsers', () => {
let source: DataSource | null = null;
beforeAll(async () => {
if (source == null) {
source = await (async () => {
const s = new DataSource({
type: 'mysql',
host: 'test_mysql_db',
port: 3306,
username: 'user',
password: 'password',
database: 'odms',
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
logger: new TestLogger('none'),
logging: true,
});
return await s.initialize();
})();
}
});
beforeEach(async () => {
if (source) {
await truncateAllTable(source);
}
});
afterAll(async () => {
await source?.destroy();
source = null;
});
it('パートナーアカウント情報が取得できること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AccountsService>(AccountsService);
// 第3階層のアカウント作成
const { account: tier3Account, admin: tier3Admin } = await makeTestAccount(
source,
{ tier: 3 },
);
// 第4階層のアカウント作成
const { account: tier4Account, admin: tier4Admin } = await makeTestAccount(
source,
{
parent_account_id: tier3Account.id,
tier: 4,
},
{
role: USER_ROLES.AUTHOR,
author_id: 'AUTHOR_ID',
},
);
const typist = await makeTestUser(source, {
account_id: tier4Account.id,
role: USER_ROLES.TYPIST,
});
const context = makeContext(tier3Admin.external_id, 'requestId');
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) =>
externalIds.map((externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
}),
deleteUsers: jest.fn(),
});
// パートナーアカウント情報の取得
const partnerUsers = await service.getPartnerUsers(
context,
tier3Admin.external_id,
tier4Account.id,
);
expect(partnerUsers).toEqual([
{
id: tier4Admin.id,
name: 'adb2c' + tier4Admin.external_id,
email: 'mail@example.com',
isPrimaryAdmin: true,
},
{
id: typist.id,
name: 'adb2c' + typist.external_id,
email: 'mail@example.com',
isPrimaryAdmin: false,
},
]);
});
it('パートナーアカウントの親が実行者でない場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AccountsService>(AccountsService);
// 第3階層のアカウント作成
const { admin: tier3Admin } = await makeTestAccount(source, { tier: 3 });
const { account: tier3Parent } = await makeTestAccount(source, { tier: 3 });
// 第4階層のアカウント作成
const { account: tier4Account } = await makeTestAccount(
source,
{
parent_account_id: tier3Parent.id,
tier: 4,
},
{
role: USER_ROLES.AUTHOR,
author_id: 'AUTHOR_ID',
},
);
const context = makeContext(tier3Admin.external_id, 'requestId');
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) =>
externalIds.map((externalId) => {
return {
displayName: 'adb2c' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
}),
deleteUsers: jest.fn(),
});
try {
// パートナーアカウント情報の取得
await service.getPartnerUsers(
context,
tier3Admin.external_id,
tier4Account.id,
);
fail();
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E019001'));
} else {
fail();
}
}
});
});

View File

@ -37,6 +37,7 @@ import {
Author,
Partner,
GetCompanyNameResponse,
PartnerUser,
} from './types/types';
import {
DateWithZeroTime,
@ -2981,4 +2982,112 @@ export class AccountsService {
);
}
}
/**
* IDのユーザー情報を取得します
* @param context
* @param targetAccountId ID
* @returns PartnerUser[]
*/
async getPartnerUsers(
context: Context,
externalId: string,
targetAccountId: number,
): Promise<PartnerUser[]> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getPartnerUsers.name
} | params: { ` +
`externalId: ${externalId}, ` +
`targetAccountId: ${targetAccountId},};`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account: myAccount } =
await this.usersRepository.findUserByExternalId(context, externalId);
if (myAccount === null) {
throw new AccountNotFoundError(
`account not found. externalId: ${externalId}`,
);
}
// 指定したアカウントIDの情報を取得する
const targetAccount = await this.accountRepository.findAccountById(
context,
targetAccountId,
);
// 実行者のアカウントが対象アカウントの親アカウントであるか確認する。
if (myAccount.id !== targetAccount.parent_account_id) {
throw new HierarchyMismatchError(
`Invalid hierarchy relation. accountId: ${targetAccountId}`,
);
}
// 対象アカウントのユーザ一覧を取得する
const users = await this.usersRepository.findUsersByAccountId(
context,
targetAccountId,
);
//ADB2Cからユーザー情報を取得する
const externalIds = users.map((x) => x.external_id);
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
// ユーザー情報をマージする
const partnerUsers = users.map((user) => {
const adb2cUser = adb2cUsers.find(
(adb2c) => user.external_id === adb2c.id,
);
if (!adb2cUser) {
throw new Error(
`adb2c user not found. externalId: ${user.external_id}`,
);
}
const { displayName, emailAddress } =
getUserNameAndMailAddress(adb2cUser);
if (!emailAddress) {
throw new Error(
`adb2c user mail not found. externalId: ${user.external_id}`,
);
}
return {
id: user.id,
name: displayName,
email: emailAddress,
isPrimaryAdmin: targetAccount.primary_admin_user_id === user.id,
};
});
return partnerUsers;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
case HierarchyMismatchError:
throw new HttpException(
makeErrorResponse('E019001'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getPartnerUsers.name}`,
);
}
}
}

View File

@ -15,7 +15,6 @@ import {
Max,
IsString,
IsNotEmpty,
IsBoolean,
} from 'class-validator';
import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator';
import { IsUnique } from '../../../common/validators/IsUnique.validator';

View File

@ -190,7 +190,26 @@ export class UsersRepositoryService {
}
return user;
}
/**
* IDをもとにユーザー一覧を取得します
* @param context
* @param accountId ID
* @returns users[]
*/
async findUsersByAccountId(
context: Context,
accountId: number,
): Promise<User[]> {
const users = await this.dataSource.getRepository(User).find({
where: {
email_verified: true,
account_id: accountId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
return users;
}
/**
* AuthorIDをもとにユーザーを取得します
* AuthorIDがセットされていない場合や