Merged PR 361: API実装(TypistGroup取得API)

## 概要
[Task2461: API実装(TypistGroup取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2461)

- TypistGroup取得APIとテストを実装しました。

## レビューポイント
- DBからの取得ロジックに問題はないか
- テストケースは十分か

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-08-29 06:25:15 +00:00
parent 1c4026eff9
commit 5b1c3a0e99
9 changed files with 335 additions and 7 deletions

View File

@ -373,7 +373,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/GetTypistGroupsResponse" "$ref": "#/components/schemas/GetTypistGroupResponse"
} }
} }
} }
@ -2449,6 +2449,14 @@
}, },
"required": ["typistGroups"] "required": ["typistGroups"]
}, },
"GetTypistGroupResponse": {
"type": "object",
"properties": {
"typistGroupName": { "type": "string" },
"typistIds": { "type": "array", "items": { "type": "integer" } }
},
"required": ["typistGroupName", "typistIds"]
},
"CreateTypistGroupRequest": { "CreateTypistGroupRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -47,4 +47,5 @@ export const ErrorCodes = [
'E010805', // ライセンス有効期限切れエラー 'E010805', // ライセンス有効期限切れエラー
'E010806', // ライセンス割り当て不可エラー 'E010806', // ライセンス割り当て不可エラー
'E010807', // ライセンス割り当て解除済みエラー 'E010807', // ライセンス割り当て解除済みエラー
'E010908', // タイピストグループ不在エラー
] as const; ] as const;

View File

@ -36,4 +36,5 @@ export const errors: Errors = {
E010805: 'License is expired Error', E010805: 'License is expired Error',
E010806: 'License is unavailable Error', E010806: 'License is unavailable Error',
E010807: 'License is already deallocated Error', E010807: 'License is already deallocated Error',
E010908: 'Typist Group not exist Error',
}; };

View File

@ -245,7 +245,7 @@ export class AccountsController {
@ApiResponse({ @ApiResponse({
status: HttpStatus.OK, status: HttpStatus.OK,
type: GetTypistGroupsResponse, type: GetTypistGroupResponse,
description: '成功時のレスポンス', description: '成功時のレスポンス',
}) })
@ApiResponse({ @ApiResponse({
@ -276,14 +276,21 @@ export class AccountsController {
@Req() req: Request, @Req() req: Request,
@Param() param: GetTypistGroupRequest, @Param() param: GetTypistGroupRequest,
): Promise<GetTypistGroupResponse> { ): Promise<GetTypistGroupResponse> {
console.log(req.header('Authorization')); const { typistGroupId } = param;
// アクセストークン取得 // アクセストークン取得
const accessToken = retrieveAuthorizationToken(req); const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken; const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
console.log(param.typistGroupId); const context = makeContext(userId);
return { typistGroupName: '', typistIds: [] }; const typistGroup = await this.accountService.getTypistGroup(
context,
userId,
typistGroupId,
);
return typistGroup;
} }
@ApiResponse({ @ApiResponse({

View File

@ -18,6 +18,7 @@ import {
getSortCriteria, getSortCriteria,
getTypistGroup, getTypistGroup,
getTypistGroupMember, getTypistGroupMember,
createTypistGroup,
} from './test/utility'; } from './test/utility';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { makeTestingModule } from '../../common/test/modules'; import { makeTestingModule } from '../../common/test/modules';
@ -31,7 +32,7 @@ import {
} from '../../common/test/utility'; } from '../../common/test/utility';
import { AccountsService } from './accounts.service'; import { AccountsService } from './accounts.service';
import { Context, makeContext } from '../../common/log'; import { Context, makeContext } from '../../common/log';
import { TIERS } from '../../constants'; import { TIERS, USER_ROLES } from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity'; import { License } from '../../repositories/licenses/entity/license.entity';
import { import {
overrideAccountsRepositoryService, overrideAccountsRepositoryService,
@ -2590,3 +2591,188 @@ describe('createTypistGroup', () => {
); );
}); });
}); });
describe('getTypistGroup', () => {
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('指定したIDのTypistGroupを取得できる', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
// 作成したアカウントにユーザーを3名追加する
const typiptUserExternalIds = [
'typist-user-external-id1',
'typist-user-external-id2',
'typist-user-external-id3',
];
const userIds: number[] = [];
for (const typiptUserExternalId of typiptUserExternalIds) {
const { id: userId } = await makeTestUser(source, {
account_id: account.id,
external_id: typiptUserExternalId,
role: USER_ROLES.TYPIST,
});
userIds.push(userId);
}
// アカウントにタイピストグループを作成する
const typistGroupName = 'typist-group-name';
const { id: typistGroupId } = await createTypistGroup(
source,
account.id,
typistGroupName,
userIds,
);
//作成したデータを確認
{
const group = await getTypistGroup(source, account.id);
expect(group.length).toBe(1);
expect(group[0].name).toBe(typistGroupName);
const groupUsers = await getTypistGroupMember(source, group[0].id);
expect(groupUsers.length).toBe(3);
expect(groupUsers.map((user) => user.user_id)).toEqual(userIds);
}
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
const typistGroup = await service.getTypistGroup(
context,
admin.external_id,
typistGroupId,
);
//実行結果を確認
{
const typistGroups = await getTypistGroup(source, account.id);
expect(typistGroups.length).toBe(1);
expect(typistGroup.typistGroupName).toBe(typistGroupName);
expect(typistGroup.typistIds.length).toBe(3);
expect(typistGroup.typistIds).toEqual(userIds);
}
});
it('指定したタイピストグループIDのタイピストグループが存在しない場合、400エラーを返却する', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
// 作成したアカウントにユーザーを3名追加する
const typiptUserExternalIds = [
'typist-user-external-id1',
'typist-user-external-id2',
'typist-user-external-id3',
];
const userIds: number[] = [];
for (const typiptUserExternalId of typiptUserExternalIds) {
const { id: userId } = await makeTestUser(source, {
account_id: account.id,
external_id: typiptUserExternalId,
role: USER_ROLES.TYPIST,
});
userIds.push(userId);
}
// アカウントにタイピストグループを作成する
const typistGroupName = 'typist-group-name';
await createTypistGroup(source, account.id, typistGroupName, userIds);
//作成したデータを確認
{
const group = await getTypistGroup(source, account.id);
expect(group.length).toBe(1);
expect(group[0].name).toBe(typistGroupName);
const groupUsers = await getTypistGroupMember(source, group[0].id);
expect(groupUsers.length).toBe(3);
expect(groupUsers.map((user) => user.user_id)).toEqual(userIds);
}
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
try {
await service.getTypistGroup(context, admin.external_id, 999);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010908'));
} else {
fail();
}
}
});
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
const module = await makeTestingModule(source);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
// 作成したアカウントにユーザーを3名追加する
const typiptUserExternalIds = [
'typist-user-external-id1',
'typist-user-external-id2',
'typist-user-external-id3',
];
const userIds: number[] = [];
for (const typiptUserExternalId of typiptUserExternalIds) {
const { id: userId } = await makeTestUser(source, {
account_id: account.id,
external_id: typiptUserExternalId,
role: USER_ROLES.TYPIST,
});
userIds.push(userId);
}
// アカウントにタイピストグループを作成する
const typistGroupName = 'typist-group-name';
const { id: typistGroupId } = await createTypistGroup(
source,
account.id,
typistGroupName,
userIds,
);
//作成したデータを確認
{
const group = await getTypistGroup(source, account.id);
expect(group.length).toBe(1);
expect(group[0].name).toBe(typistGroupName);
const groupUsers = await getTypistGroupMember(source, group[0].id);
expect(groupUsers.length).toBe(3);
expect(groupUsers.map((user) => user.user_id)).toEqual(userIds);
}
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(admin.external_id);
//DBアクセスに失敗するようにする
const typistGroupService = module.get<UserGroupsRepositoryService>(
UserGroupsRepositoryService,
);
typistGroupService.getTypistGroup = jest
.fn()
.mockRejectedValue('DB failed');
try {
await service.getTypistGroup(context, admin.external_id, typistGroupId);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});

View File

@ -21,6 +21,7 @@ import {
GetDealersResponse, GetDealersResponse,
Dealer, Dealer,
GetMyAccountResponse, GetMyAccountResponse,
GetTypistGroupResponse,
} from './types/types'; } from './types/types';
import { import {
DateWithZeroTime, DateWithZeroTime,
@ -40,6 +41,7 @@ import {
OrderNotFoundError, OrderNotFoundError,
} from '../../repositories/licenses/errors/types'; } from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { TypistGroupNotExistError } from '../../repositories/user_groups/errors/types';
import { TypistIdInvalidError } from '../../repositories/user_groups/errors/types'; import { TypistIdInvalidError } from '../../repositories/user_groups/errors/types';
@Injectable() @Injectable()
@ -416,6 +418,61 @@ export class AccountsService {
this.logger.log(`[OUT] ${this.getTypistGroups.name}`); this.logger.log(`[OUT] ${this.getTypistGroups.name}`);
} }
} }
/**
* IDを指定してタイピストグループを取得する
* @param context
* @param externalId
* @param typistGroupId
* @returns typist group
*/
async getTypistGroup(
context: Context,
externalId: string,
typistGroupId: number,
): Promise<GetTypistGroupResponse> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getTypistGroup.name} | params: { externalId: ${externalId}, typistGroupId: ${typistGroupId} };`,
);
try {
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
const userGroup = await this.userGroupsRepository.getTypistGroup(
account_id,
typistGroupId,
);
return {
typistGroupName: userGroup.name,
typistIds: userGroup.userGroupMembers.map((x) => x.user_id),
};
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TypistGroupNotExistError:
throw new HttpException(
makeErrorResponse('E010908'),
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.trackingId}] ${this.getTypistGroup.name}`,
);
}
}
/** /**
* Gets typists * Gets typists

View File

@ -108,3 +108,34 @@ export const getTypistGroupMember = async (
}, },
}); });
}; };
// タイピストグループを作成する
export const createTypistGroup = async (
datasource: DataSource,
accountId: number,
name: string,
memberIds: number[],
): Promise<UserGroup> => {
const { identifiers } = await datasource.getRepository(UserGroup).insert({
account_id: accountId,
name: name,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const userGroup = identifiers.pop() as UserGroup;
for (const memberId of memberIds) {
await datasource.getRepository(UserGroupMember).insert({
user_group_id: userGroup.id,
user_id: memberId,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
}
return userGroup;
};

View File

@ -1,2 +1,4 @@
// タイピストグループが存在しないエラー
export class TypistGroupNotExistError extends Error {}
// typistIdが不正な場合のエラー // typistIdが不正な場合のエラー
export class TypistIdInvalidError extends Error {} export class TypistIdInvalidError extends Error {}

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm'; import { DataSource, In, IsNull } from 'typeorm';
import { UserGroup } from './entity/user_group.entity'; import { UserGroup } from './entity/user_group.entity';
import { UserGroupMember } from './entity/user_group_member.entity'; import { UserGroupMember } from './entity/user_group_member.entity';
import { TypistGroupNotExistError } from './errors/types';
import { User } from '../users/entity/user.entity'; import { User } from '../users/entity/user.entity';
import { TypistIdInvalidError } from './errors/types'; import { TypistIdInvalidError } from './errors/types';
import { USER_ROLES } from '../../constants'; import { USER_ROLES } from '../../constants';
@ -46,6 +47,40 @@ export class UserGroupsRepositoryService {
return groupMembers; return groupMembers;
}); });
} }
/**
* IDを取得します
* @param accountId
* @param typistGroupId
* @returns typist group
*/
async getTypistGroup(
accountId: number,
typistGroupId: number,
): Promise<UserGroup> {
return await this.dataSource.transaction(async (entityManager) => {
const userGroupRepo = entityManager.getRepository(UserGroup);
const userGroup = await userGroupRepo.findOne({
where: {
id: typistGroupId,
account_id: accountId,
deleted_at: IsNull(),
},
relations: {
userGroupMembers: true,
},
});
if (!userGroup) {
throw new TypistGroupNotExistError(
`Typist Group is not exist. typistGroupId: ${typistGroupId}`,
);
}
return userGroup;
});
}
/** /**
* IDでタイピストグループを作成しtypistIdsのユーザーを紐付ける * IDでタイピストグループを作成しtypistIdsのユーザーを紐付ける
* @param accountId * @param accountId