Merged PR 154: API実装(タスク一覧取得 | author)

## 概要
[Task1949: API実装(タスク一覧取得 | author)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1949)

- タスク一覧取得(Author用)
  - AuthorIDとAccountIDを条件にタスクを取得する処理を実装
- テスト実装
  - 成功ケースの時に、Repositoryのメソッドが正しい引数で呼ばれているか確認するテストを追加

## レビューポイント
- テスト実装の内容はこれでよいか
- DBから取得した値をレスポンス用の型に変換する処理はAdminと同様の認識だがあっているか

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

## 動作確認状況
- ローカルで確認
- 実際にデータを取得して内容が正しいか確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-06-14 03:57:30 +00:00
parent 3220ae0552
commit 34b242684b
5 changed files with 476 additions and 250 deletions

View File

@ -63,7 +63,7 @@ it('アクセストークンからユーザ情報を取得する', async () => {
adb2cParam,
sendGridMockValue,
);
expect(await service.getMyAccountInfo(token)).toEqual(userInfo);
expect(await service.getMyAccountInfo(token)).toEqual(1234567890123456);
});
it('ユーザ情報が取得できない場合エラーとなる', async () => {
const token = {

View File

@ -1,29 +1,28 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TasksService } from './tasks.service';
import { TasksRepositoryModule } from '../../repositories/tasks/tasks.repository.module';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { makeDefaultTasksRepositoryMockValue, makeDefaultUsersRepositoryMockValue, makeTasksServiceMock } from '../tasks/test/tasks.service.mock';
import {
makeDefaultTasksRepositoryMockValue,
makeDefaultUsersRepositoryMockValue,
makeTasksServiceMock,
} from '../tasks/test/tasks.service.mock';
import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
describe('TasksService', () => {
it('タスク一覧を取得できるadmin', async () => {
const tasksRepositoryMockValue =
makeDefaultTasksRepositoryMockValue();
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const service = await makeTasksServiceMock(
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
);
const accessToken = { userId: "userId", role: "admin", tier: 5 };
const accessToken = { userId: 'userId', role: 'admin', tier: 5 };
const offset = 0;
const limit = 20;
const status = ["Uploaded,Backup"];
const paramName = "JOB_NUMBER";
const direction = "ASC";
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
expect(
await service.getTasksFromAccountId(
await service.tasksService.getTasksFromAccountId(
accessToken,
offset,
limit,
@ -72,38 +71,38 @@ describe('TasksService', () => {
});
});
it('アカウント情報の取得に失敗した場合、エラーを返却するadmin', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error("DB failed");
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
);
it('アカウント情報の取得に失敗した場合、エラーを返却する', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error('DB failed');
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
);
const accessToken = { userId: 'userId', role: 'admin', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
await expect(
service.tasksService.getTasksFromAccountId(
accessToken,
offset,
limit,
status,
paramName,
direction,
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E000101'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
const accessToken = { userId: 'userId', role: 'admin', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
await expect(
service.getTasksFromAccountId(
accessToken,
offset,
limit,
status,
paramName,
direction,
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E000101'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('タスク一覧の取得に失敗した場合、エラーを返却するadmin', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
@ -120,7 +119,7 @@ describe('TasksService', () => {
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
await expect(
service.getTasksFromAccountId(
service.tasksService.getTasksFromAccountId(
accessToken,
offset,
limit,
@ -135,59 +134,58 @@ describe('TasksService', () => {
),
);
});
it('取得したタスク一覧が不正な形式の場合、エラーを返却するadmin', async () => {
const tasksRepositoryMockValue =
makeDefaultTasksRepositoryMockValue();
tasksRepositoryMockValue.getTasksFromAccountId={
tasks: [
{
it('取得したタスク一覧が不正な形式の場合、エラーを返却する', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
tasksRepositoryMockValue.getTasksFromAccountId = {
tasks: [
{
id: 1,
job_number: '00000001',
account_id: 1,
is_job_number_enabled: true,
audio_file_id: 1,
status: 'Uploaded',
priority: '00',
created_at: new Date('2023-01-01T01:01:01.000'),
option_items: undefined,
file: {
id: 1,
job_number: '00000001',
account_id: 1,
is_job_number_enabled: true,
audio_file_id: 1,
status: 'Uploaded',
owner_user_id: 1,
url: 'test/test.zip',
file_name: 'test.zip',
author_id: 'AUTHOR',
work_type_id: 'WorkType',
started_at: new Date('2023-01-01T01:01:01.000'),
duration: '123000',
finished_at: new Date('2023-01-01T01:01:01.000'),
uploaded_at: new Date('2023-01-01T01:01:01.000'),
file_size: 123000,
priority: '00',
created_at: new Date('2023-01-01T01:01:01.000'),
option_items: undefined,
file: {
id: 1,
account_id: 1,
owner_user_id: 1,
url: 'test/test.zip',
file_name: 'test.zip',
author_id: 'AUTHOR',
work_type_id: 'WorkType',
started_at: new Date('2023-01-01T01:01:01.000'),
duration: '123000',
finished_at: new Date('2023-01-01T01:01:01.000'),
uploaded_at: new Date('2023-01-01T01:01:01.000'),
file_size: 123000,
priority: '00',
audio_format: 'DS',
comment: 'comment',
is_encrypted: true,
},
audio_format: 'DS',
comment: 'comment',
is_encrypted: true,
},
],
permissions: [],
count: 1,
}
},
],
permissions: [],
count: 1,
};
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const service = await makeTasksServiceMock(
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
);
const accessToken = { userId: "userId", role: "admin", tier: 5 };
const accessToken = { userId: 'userId', role: 'admin', tier: 5 };
const offset = 0;
const limit = 20;
const status = ["Uploaded,Backup"];
const paramName = "JOB_NUMBER";
const direction = "ASC";
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
await expect(
service.getTasksFromAccountId(
service.tasksService.getTasksFromAccountId(
accessToken,
offset,
limit,
@ -202,36 +200,142 @@ describe('TasksService', () => {
),
);
});
// TODO: 後続タスクでadmin以外の処理を実装するが、ここではエラーを返す
it('admin以外のRoleの場合、エラーを返却するadmin', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
);
const accessToken = { userId: 'userId', role: 'author', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
await expect(
service.getTasksFromAccountId(
accessToken,
offset,
limit,
status,
paramName,
direction,
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E000101'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('タスク一覧を取得できるauthor', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
if (usersRepositoryMockValue.findUserByExternalId instanceof Error) {
return;
}
usersRepositoryMockValue.findUserByExternalId.role = 'author';
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
);
const accessToken = { userId: 'userId', role: 'author', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
const result = await service.tasksService.getTasksFromAccountId(
accessToken,
offset,
limit,
status,
paramName,
direction,
);
expect(result).toEqual({
tasks: [
{
assignees: [{ typistName: 'USER_userId', typistUserId: 1 }],
audioCreatedDate: '2023-01-01T01:01:01.000Z',
audioDuration: '123000',
audioFileId: 1,
audioFinishedDate: '2023-01-01T01:01:01.000Z',
audioFormat: 'DS',
audioUploadedDate: '2023-01-01T01:01:01.000Z',
authorId: 'AUTHOR',
comment: 'comment',
fileName: 'test.zip',
fileSize: 123000,
isEncrypted: true,
jobNumber: '00000001',
optionItemList: [
{ optionItemLabel: 'label01', optionItemValue: 'value01' },
{ optionItemLabel: 'label02', optionItemValue: 'value02' },
{ optionItemLabel: 'label03', optionItemValue: 'value03' },
{ optionItemLabel: 'label04', optionItemValue: 'value04' },
{ optionItemLabel: 'label05', optionItemValue: 'value05' },
{ optionItemLabel: 'label06', optionItemValue: 'value06' },
{ optionItemLabel: 'label07', optionItemValue: 'value07' },
{ optionItemLabel: 'label08', optionItemValue: 'value08' },
{ optionItemLabel: 'label09', optionItemValue: 'value09' },
{ optionItemLabel: 'label10', optionItemValue: 'value10' },
],
priority: '00',
status: 'Uploaded',
transcriptionFinishedDate: '',
transcriptionStartedDate: '',
typist: undefined,
url: 'test/test.zip',
workType: 'WorkType',
},
],
total: 1,
});
expect(
service.taskRepoService.getTasksFromAuthorIdAndAccountId,
).toHaveBeenCalledWith('abcdef', 1, 0, 20, 'JOB_NUMBER', 'ASC', [
'Uploaded,Backup',
]);
});
it('タスク一覧の取得に失敗した場合、エラーを返却するauthor', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
tasksRepositoryMockValue.getTasksFromAuthorIdAndAccountId = new Error(
'DB failed',
);
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
);
const accessToken = { userId: 'userId', role: 'author', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
await expect(
service.tasksService.getTasksFromAccountId(
accessToken,
offset,
limit,
status,
paramName,
direction,
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E000101'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('想定外のRoleの場合、エラーを返却する', async () => {
const tasksRepositoryMockValue = makeDefaultTasksRepositoryMockValue();
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
);
const accessToken = { userId: 'userId', role: 'XXX', tier: 5 };
const offset = 0;
const limit = 20;
const status = ['Uploaded,Backup'];
const paramName = 'JOB_NUMBER';
const direction = 'ASC';
await expect(
service.tasksService.getTasksFromAccountId(
accessToken,
offset,
limit,
status,
paramName,
direction,
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E000101'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});

View File

@ -36,9 +36,8 @@ export class TasksService {
const defaultDirection: SortDirection = 'ASC';
try {
const { account_id } = await this.usersRepository.findUserByExternalId(
userId,
);
const { account_id, author_id } =
await this.usersRepository.findUserByExternalId(userId);
if (roles.includes(ADMIN_ROLES.ADMIN)) {
const result = await this.taskRepository.getTasksFromAccountId(
@ -54,9 +53,19 @@ export class TasksService {
return { tasks: tasks, total: result.count };
}
if (roles.includes(USER_ROLES.AUTHOR)) {
throw new Error(`NOT IMPLEMENTED`);
const result =
await this.taskRepository.getTasksFromAuthorIdAndAccountId(
author_id,
account_id,
offset,
limit,
paramName ?? defaultParamName,
direction ?? defaultDirection,
status,
);
const tasks = createTasks(result.tasks, result.permissions);
return { tasks: tasks, total: result.count };
}
if (roles.includes(USER_ROLES.TYPIST)) {

View File

@ -5,6 +5,10 @@ import { User } from '../../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../../repositories/users/users.repository.service';
import { Task } from '../../../repositories/tasks/entity/task.entity';
import { CheckoutPermission } from '../../../repositories/checkout_permissions/entity/checkout_permission.entity';
import {
SortDirection,
TaskListSortableAttribute,
} from '../../../common/types/sort';
export type TasksRepositoryMockValue = {
getTasksFromAccountId:
@ -14,6 +18,13 @@ export type TasksRepositoryMockValue = {
count: number;
}
| Error;
getTasksFromAuthorIdAndAccountId:
| {
tasks: Task[];
permissions: CheckoutPermission[];
count: number;
}
| Error;
};
export type UsersRepositoryMockValue = {
@ -23,7 +34,10 @@ export type UsersRepositoryMockValue = {
export const makeTasksServiceMock = async (
tasksRepositoryMockValue: TasksRepositoryMockValue,
usersRepositoryMockValue: UsersRepositoryMockValue,
): Promise<TasksService> => {
): Promise<{
tasksService: TasksService;
taskRepoService: TasksRepositoryService;
}> => {
const module: TestingModule = await Test.createTestingModule({
providers: [TasksService],
})
@ -37,13 +51,14 @@ export const makeTasksServiceMock = async (
})
.compile();
return module.get<TasksService>(TasksService);
return {
tasksService: module.get<TasksService>(TasksService),
taskRepoService: module.get(TasksRepositoryService),
};
};
export const makeTasksRepositoryMock = (
value:TasksRepositoryMockValue,
) => {
const { getTasksFromAccountId } = value;
export const makeTasksRepositoryMock = (value: TasksRepositoryMockValue) => {
const { getTasksFromAccountId, getTasksFromAuthorIdAndAccountId } = value;
return {
getTasksFromAccountId:
getTasksFromAccountId instanceof Error
@ -58,6 +73,32 @@ export const makeTasksRepositoryMock = (
[]
>()
.mockResolvedValue(getTasksFromAccountId),
getTasksFromAuthorIdAndAccountId:
getTasksFromAuthorIdAndAccountId instanceof Error
? jest
.fn<
Promise<void>,
[
string,
number,
number,
number,
TaskListSortableAttribute,
SortDirection,
string[],
]
>()
.mockRejectedValue(getTasksFromAuthorIdAndAccountId)
: jest
.fn<
Promise<{
tasks: Task[];
permissions: CheckoutPermission[];
count: number;
}>,
[]
>()
.mockResolvedValue(getTasksFromAuthorIdAndAccountId),
};
};
@ -75,125 +116,11 @@ export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
export const makeDefaultTasksRepositoryMockValue =
(): TasksRepositoryMockValue => {
return {
getTasksFromAccountId: {
tasks: [
{
id: 1,
job_number: '00000001',
account_id: 1,
is_job_number_enabled: true,
audio_file_id: 1,
status: 'Uploaded',
priority: '00',
created_at: new Date('2023-01-01T01:01:01.000'),
option_items: [
{
id: 1,
audio_file_id: 1,
label: "label01",
value: "value01",
},
{
id: 2,
audio_file_id: 1,
label: "label02",
value: "value02",
},
{
id: 3,
audio_file_id: 1,
label: "label03",
value: "value03",
},
{
id: 4,
audio_file_id: 1,
label: "label04",
value: "value04",
},
{
id: 5,
audio_file_id: 1,
label: "label05",
value: "value05",
},
{
id: 6,
audio_file_id: 1,
label: "label06",
value: "value06",
},
{
id: 7,
audio_file_id: 1,
label: "label07",
value: "value07",
},
{
id: 8,
audio_file_id: 1,
label: "label08",
value: "value08",
},
{
id: 9,
audio_file_id: 1,
label: "label09",
value: "value09",
},
{
id: 10,
audio_file_id: 1,
label: "label10",
value: "value10",
},
],
file: {
id: 1,
account_id: 1,
owner_user_id: 1,
url: 'test/test.zip',
file_name: 'test.zip',
author_id: 'AUTHOR',
work_type_id: 'WorkType',
started_at: new Date('2023-01-01T01:01:01.000'),
duration: '123000',
finished_at: new Date('2023-01-01T01:01:01.000'),
uploaded_at: new Date('2023-01-01T01:01:01.000'),
file_size: 123000,
priority: '00',
audio_format: 'DS',
comment: 'comment',
is_encrypted: true,
},
},
],
permissions: [
{
id: 1,
task_id: 1,
user_id: 1,
user: {
id: 1,
account_id: 1,
external_id: 'userId',
role: 'typist',
accepted_terms_version: '',
email_verified: true,
auto_renew: true,
license_alert: true,
notification: true,
created_by: 'test',
created_at: new Date(),
updated_by: 'test',
updated_at: new Date(),
},
},
],
count: 1,
},
getTasksFromAccountId: defaultTasksRepositoryMockValue,
getTasksFromAuthorIdAndAccountId: defaultTasksRepositoryMockValue,
};
};
export const makeDefaultUsersRepositoryMockValue =
(): UsersRepositoryMockValue => {
const user1 = new User();
@ -208,10 +135,130 @@ export const makeDefaultUsersRepositoryMockValue =
user1.deleted_at = undefined;
user1.created_by = 'test';
user1.created_at = new Date();
user1.author_id = 'abcdef';
return {
findUserByExternalId: user1,
};
};
const defaultTasksRepositoryMockValue: {
tasks: Task[];
permissions: CheckoutPermission[];
count: number;
} = {
tasks: [
{
id: 1,
job_number: '00000001',
account_id: 1,
is_job_number_enabled: true,
audio_file_id: 1,
status: 'Uploaded',
priority: '00',
created_at: new Date('2023-01-01T01:01:01.000'),
option_items: [
{
id: 1,
audio_file_id: 1,
label: 'label01',
value: 'value01',
},
{
id: 2,
audio_file_id: 1,
label: 'label02',
value: 'value02',
},
{
id: 3,
audio_file_id: 1,
label: 'label03',
value: 'value03',
},
{
id: 4,
audio_file_id: 1,
label: 'label04',
value: 'value04',
},
{
id: 5,
audio_file_id: 1,
label: 'label05',
value: 'value05',
},
{
id: 6,
audio_file_id: 1,
label: 'label06',
value: 'value06',
},
{
id: 7,
audio_file_id: 1,
label: 'label07',
value: 'value07',
},
{
id: 8,
audio_file_id: 1,
label: 'label08',
value: 'value08',
},
{
id: 9,
audio_file_id: 1,
label: 'label09',
value: 'value09',
},
{
id: 10,
audio_file_id: 1,
label: 'label10',
value: 'value10',
},
],
file: {
id: 1,
account_id: 1,
owner_user_id: 1,
url: 'test/test.zip',
file_name: 'test.zip',
author_id: 'AUTHOR',
work_type_id: 'WorkType',
started_at: new Date('2023-01-01T01:01:01.000'),
duration: '123000',
finished_at: new Date('2023-01-01T01:01:01.000'),
uploaded_at: new Date('2023-01-01T01:01:01.000'),
file_size: 123000,
priority: '00',
audio_format: 'DS',
comment: 'comment',
is_encrypted: true,
},
},
],
permissions: [
{
id: 1,
task_id: 1,
user_id: 1,
user: {
id: 1,
account_id: 1,
external_id: 'userId',
role: 'typist',
accepted_terms_version: '',
email_verified: true,
auto_renew: true,
license_alert: true,
notification: true,
created_by: 'test',
created_at: new Date(),
updated_by: 'test',
updated_at: new Date(),
},
},
],
count: 1,
};

View File

@ -88,6 +88,72 @@ export class TasksRepositoryService {
});
return value;
}
/**
* author_idとaccount_idに紐づくTask関連情報の一覧を取得します
* @param author_id
* @param account_id
* @param offset
* @param limit
* @param sort_criteria
* @param direction
* @param status
* @returns tasks: タスク情報 / permissions:タスクに紐づくチェックアウト権限情報 / count: offset|limitを行わなかった場合の該当タスクの合計
*/
async getTasksFromAuthorIdAndAccountId(
author_id: string,
account_id: number,
offset: number,
limit: number,
sort_criteria: TaskListSortableAttribute,
direction: SortDirection,
status: string[],
): Promise<{
tasks: Task[];
permissions: CheckoutPermission[];
count: number;
}> {
const order = makeOrder(sort_criteria, direction);
const value = await this.dataSource.transaction(async (entityManager) => {
const taskRepo = entityManager.getRepository(Task);
const count = await taskRepo.count({
where: {
account_id: account_id,
status: In(status),
file: { author_id: author_id },
},
});
const tasks = await taskRepo.find({
relations: {
file: true,
option_items: true,
typist_user: true,
},
where: {
account_id: account_id,
status: In(status),
file: { author_id: author_id },
},
order: order, // 引数によってOrderに使用するパラメータを変更
take: limit,
skip: offset,
});
const checkoutRepo = entityManager.getRepository(CheckoutPermission);
const taskIds = tasks.map((x) => x.id);
const permissions = await checkoutRepo.find({
relations: {
user: true,
user_group: true,
},
where: {
task_id: In(taskIds),
},
});
return { tasks, permissions, count };
});
return value;
}
/**
*