From 64fe6bcbe7c37dec02494ffdd6b3ddd72f7a2eea Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Thu, 16 Nov 2023 01:40:02 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20577:=20=E6=96=87=E5=AD=97?= =?UTF-8?q?=E8=B5=B7=E3=81=93=E3=81=97=E3=81=99=E3=82=8B=E3=82=BF=E3=82=B9?= =?UTF-8?q?=E3=82=AF=E3=82=92=E8=A4=87=E6=95=B0=E6=8C=81=E3=81=A6=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2223: 文字起こしするタスクを複数持てないようにする](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2223) - タスクをチェックアウト済みの場合は他のタスクをチェックアウトできないように修正しました。 ## レビューポイント - テストケース追加は適切でしょうか? - エラーとなった場合のエラーケースを`E010601`として返していますが処理として適切でしょうか? - タスクを変更できる状態でないということでこれを採用しています。 ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../src/features/tasks/tasks.service.spec.ts | 283 ++++++++++++------ .../src/features/tasks/tasks.service.ts | 2 + .../src/repositories/tasks/errors/types.ts | 2 + .../tasks/tasks.repository.service.ts | 20 ++ 4 files changed, 219 insertions(+), 88 deletions(-) diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 3eaef7d..c74de6b 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -1421,12 +1421,7 @@ describe('checkout', () => { it('ユーザーのRoleがTypistで、タスクのチェックアウト権限が個人指定である時、タスクをチェックアウトできる', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { @@ -1486,12 +1481,7 @@ describe('checkout', () => { it('ユーザーのRoleがTypistで、タスクのチェックアウト権限がグループ指定である時、タスクをチェックアウトできる', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { @@ -1551,12 +1541,7 @@ describe('checkout', () => { it('ユーザーのRoleがTypistで、タスクのステータスがPendingである時、タスクをチェックアウトできる', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: typistUserId } = await makeTestUser(source, { @@ -1610,12 +1595,7 @@ describe('checkout', () => { it('ユーザーのRoleがTypistで、対象のタスクのStatus[Uploaded,Inprogress,Pending]以外の時、タスクをチェックアウトできない', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { @@ -1642,26 +1622,27 @@ describe('checkout', () => { ); const service = module.get(TasksService); - await expect( - service.checkout( + try { + await service.checkout( makeContext('trackingId'), 1, ['typist'], 'typist-user-external-id', - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST), - ); + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010601')); + } else { + fail(); + } + } }); it('ユーザーのRoleがTypistで、チェックアウト権限が存在しない時、タスクをチェックアウトできない', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { @@ -1688,26 +1669,153 @@ describe('checkout', () => { ); const service = module.get(TasksService); - await expect( - service.checkout( + try { + await service.checkout( makeContext('trackingId'), 1, ['typist'], 'typist-user-external-id', - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010602'), HttpStatus.BAD_REQUEST), + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010602')); + } else { + fail(); + } + } + }); + + it('ユーザーのRoleがTypistで、既にチェックアウト中のタスク(InProgress)がある時、タスクをチェックアウトできない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'MY_AUTHOR_ID', + }); + + await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + TASK_STATUS.IN_PROGRESS, + typistUserId, ); + const { taskId, audioFileId } = await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000002', + TASK_STATUS.UPLOADED, + ); + + await createCheckoutPermissions(source, taskId, typistUserId); + + const service = module.get(TasksService); + + try { + await service.checkout( + makeContext('trackingId'), + audioFileId, + ['typist'], + 'typist-user-external-id', + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010601')); + } else { + fail(); + } + } + }); + + it('ユーザーのRoleがTypistで、別ユーザーによってチェックアウト中のタスク(InProgress)がある時、タスクをチェックアウトできる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { id: typistUserId1 } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id1', + role: 'typist', + }); + const { id: typistUserId2 } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id2', + role: 'typist', + }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'MY_AUTHOR_ID', + }); + + await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + TASK_STATUS.IN_PROGRESS, + typistUserId1, + ); + const { taskId } = await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000002', + TASK_STATUS.UPLOADED, + ); + + await createCheckoutPermissions(source, taskId, typistUserId2); + + const service = module.get(TasksService); + + await service.checkout( + makeContext('trackingId'), + 2, + ['typist'], + 'typist-user-external-id2', + ); + + const resultTask = await getTask(source, taskId); + const permisions = await getCheckoutPermissions(source, taskId); + + expect(resultTask?.status).toEqual(TASK_STATUS.IN_PROGRESS); + expect(resultTask?.typist_user_id).toEqual(typistUserId2); + expect(permisions.length).toBe(1); + expect(permisions[0].task_id).toBe(taskId); + expect(permisions[0].user_id).toBe(typistUserId2); }); it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクをチェックアウトできる(Uploaded)', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: authorUserId } = await makeTestUser(source, { @@ -1740,12 +1848,7 @@ describe('checkout', () => { it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクをチェックアウトできる(Finished)', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: authorUserId } = await makeTestUser(source, { @@ -1766,6 +1869,7 @@ describe('checkout', () => { ); const service = module.get(TasksService); + expect( await service.checkout( makeContext('trackingId'), @@ -1778,12 +1882,7 @@ describe('checkout', () => { it('ユーザーのRoleがAuthorで、アップロードした音声ファイルに紐づいたタスクが存在しない場合、タスクをチェックアウトできない', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { @@ -1794,26 +1893,27 @@ describe('checkout', () => { }); const service = module.get(TasksService); - await expect( - service.checkout( + try { + await service.checkout( makeContext('trackingId'), 1, ['author'], 'author-user-external-id', ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST), - ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.NOT_FOUND); + expect(e.getResponse()).toEqual(makeErrorResponse('E010601')); + } else { + fail(); + } + } }); it('ユーザーのRoleがAuthorで、音声ファイルに紐づいたタスクでユーザーと一致するAuthorIDでない場合、タスクをチェックアウトできない', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); const { id: authorUserId } = await makeTestUser(source, { @@ -1834,26 +1934,27 @@ describe('checkout', () => { ); const service = module.get(TasksService); - await expect( - service.checkout( + try { + await service.checkout( makeContext('trackingId'), 1, ['author'], 'author-user-external-id', - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010602'), HttpStatus.BAD_REQUEST), - ); + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010602')); + } else { + fail(); + } + } }); it('ユーザーのRoleに[Typist,author]が設定されていない時、タスクをチェックアウトできない', async () => { if (!source) fail(); - const notificationhubServiceMockValue = - makeDefaultNotificationhubServiceMockValue(); - const module = await makeTaskTestingModuleWithNotificaiton( - source, - notificationhubServiceMockValue, - ); + const module = await makeTestingModule(source); if (!module) fail(); const { id: accountId } = await makeTestSimpleAccount(source); await makeTestUser(source, { @@ -1864,16 +1965,22 @@ describe('checkout', () => { }); const service = module.get(TasksService); - await expect( - service.checkout( + try { + await service.checkout( makeContext('trackingId'), 1, ['none'], 'none-user-external-id', - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010602'), HttpStatus.BAD_REQUEST), - ); + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010602')); + } else { + fail(); + } + } }); }); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index ccd1f9d..408f7ef 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -19,6 +19,7 @@ import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { CheckoutPermission } from '../../repositories/checkout_permissions/entity/checkout_permission.entity'; import { AccountNotMatchError, + AlreadyHasInProgressTaskError, CheckoutPermissionNotFoundError, StatusNotMatchError, TaskAuthorIdNotMatchError, @@ -295,6 +296,7 @@ export class TasksService { ); case AccountNotMatchError: case StatusNotMatchError: + case AlreadyHasInProgressTaskError: throw new HttpException( makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST, diff --git a/dictation_server/src/repositories/tasks/errors/types.ts b/dictation_server/src/repositories/tasks/errors/types.ts index 3d2b2d4..b82c3ed 100644 --- a/dictation_server/src/repositories/tasks/errors/types.ts +++ b/dictation_server/src/repositories/tasks/errors/types.ts @@ -14,3 +14,5 @@ export class StatusNotMatchError extends Error {} export class TypistUserNotMatchError extends Error {} // Account不一致エラー export class AccountNotMatchError extends Error {} +// タスクチェックアウト済みエラー +export class AlreadyHasInProgressTaskError extends Error {} diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 0c8b4cc..784dcec 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -6,6 +6,7 @@ import { FindOptionsOrderValue, In, IsNull, + Not, Repository, } from 'typeorm'; import { Task } from './entity/task.entity'; @@ -26,6 +27,7 @@ import { UserGroup } from '../user_groups/entity/user_group.entity'; import { User } from '../users/entity/user.entity'; import { AccountNotMatchError, + AlreadyHasInProgressTaskError, CheckoutPermissionNotFoundError, StatusNotMatchError, TaskAuthorIdNotMatchError, @@ -163,6 +165,24 @@ export class TasksRepositoryService { `task not found. audio_file_id:${audio_file_id}`, ); } + + // 指定したタスク以外に実行ユーザー担当のInprogressのタスクが存在する場合はエラー + const tasks = await taskRepo.find({ + where: { + audio_file_id: Not(audio_file_id), + status: TASK_STATUS.IN_PROGRESS, + typist_user_id: user_id, + }, + }); + + if (tasks.length > 0) { + throw new AlreadyHasInProgressTaskError( + `checkout task already exists. task:${tasks.map( + (x) => x.audio_file_id, + )}`, + ); + } + // アカウントチェック if (task.account_id !== account_id) { throw new AccountNotMatchError(