From f1b75a7ff083521678d01eb09f9c2b74eebe6b75 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Wed, 18 Sep 2024 01:35:28 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20921:=20API=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4478: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4478) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 - 修正箇所がほかの機能に影響していないか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - クエリ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか ## 補足 - 相談、参考資料などがあれば --- .../src/features/files/files.service.spec.ts | 244 +++++++----------- .../src/features/files/files.service.ts | 4 +- .../src/features/tasks/tasks.service.spec.ts | 118 ++++++++- .../src/features/tasks/tasks.service.ts | 1 - .../tasks/tasks.repository.service.ts | 63 +---- 5 files changed, 222 insertions(+), 208 deletions(-) diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 813b155..d38c991 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -494,132 +494,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); }); - it('タスク作成時に、自動ルーティングを行うことができる(API実行者のAuthorIDとworkType)', async () => { - if (!source) fail(); - const { id: accountId } = await makeTestSimpleAccount(source); - // 音声ファイルの録音者のユーザー - const { author_id: authorAuthorId } = await makeTestUser(source, { - account_id: accountId, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - // ルーティング先のタイピストのユーザー - const { id: typistUserId } = await makeTestUser(source, { - account_id: accountId, - external_id: 'typist-user-external-id', - role: 'typist', - author_id: undefined, - }); - // API実行者のユーザー - const { external_id: myExternalId, id: myUserId } = await makeTestUser( - source, - { - account_id: accountId, - external_id: 'my-author-user-external-id', - role: 'author', - author_id: 'MY_AUTHOR_ID', - }, - ); - - // ワークタイプを作成 - const { id: worktypeId, custom_worktype_id } = await createWorktype( - source, - accountId, - 'worktypeId', - ); - - // テンプレートファイルを作成 - const { id: templateFileId } = await createTemplateFile( - source, - accountId, - 'templateFile', - 'http://blob/url/templateFile.zip', - ); - - // ワークフローを作成 - const { id: workflowId } = await createWorkflow( - source, - accountId, - myUserId, // API実行者のユーザーIDを設定 - worktypeId, - templateFileId, - ); - // ユーザーグループを作成 - const { userGroupId } = await createUserGroupAndMember( - source, - accountId, - 'userGroupName', - typistUserId, // ルーティング先のタイピストのユーザーIDを設定 - ); - // ワークフロータイピストを作成 - await createWorkflowTypist( - source, - workflowId, - undefined, - userGroupId, // ルーティング先のユーザーグループIDを設定 - ); - - // 初期値のジョブナンバーでjob_numberテーブルを作成 - await createJobNumber(source, accountId, '00000000'); - - const blobParam = makeBlobstorageServiceMockValue(); - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - const notificationHubService = module.get( - NotificationhubService, - ); - const result = await service.uploadFinished( - makeContext('trackingId', 'requestId'), - myExternalId, // API実行者のユーザーIDを設定 - 'http://blob/url/file.zip', - authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る - 'file.zip', - '11:22:33', - '2023-05-26T11:22:33.444', - '2023-05-26T11:22:33.444', - '2023-05-26T11:22:33.444', - 256, - '01', - 'DS2', - 'comment', - custom_worktype_id, - optionItemList, - false, - ); - expect(result.jobNumber).toEqual('00000001'); - // 通知処理が想定通りの引数で呼ばれているか確認 - expect(notificationHubService.notify).toHaveBeenCalledWith( - makeContext('trackingId', 'requestId'), - [`user_${typistUserId}`], - { - authorId: 'AUTHOR_ID', - filename: 'file', - priority: 'High', - uploadedAt: '2023-05-26T11:22:33.444', - }, - ); - // 作成したタスクを取得 - const resultTask = await getTaskFromJobNumber(source, result.jobNumber); - // タスクのチェックアウト権限を取得 - const resultCheckoutPermission = await getCheckoutPermissions( - source, - resultTask?.id ?? 0, - ); - // タスクのテンプレートファイルIDを確認 - expect(resultTask?.template_file_id).toEqual(templateFileId); - // タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認 - expect(resultCheckoutPermission.length).toEqual(1); - expect(resultCheckoutPermission[0].user_group_id).toEqual(userGroupId); - }); - it('タスク作成時に、音声ファイルメタ情報のAuthorIDに存在しないものが入っていても自動ルーティングを行うことができる(API実行者のAuthorIDとworkType)', async () => { + it('タスク作成時に、音声ファイルメタ情報のAuthorIDに存在しないIDが入っていた場合自動ルーティングを行うことができない', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); // 音声ファイルの録音者のユーザー @@ -705,7 +580,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { makeContext('trackingId', 'requestId'), myExternalId, // API実行者のユーザーIDを設定 'http://blob/url/file.zip', - 'XXXXXXXXXX', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'XXXXXX', // 存在しないAuthorIDを指定 'file.zip', '11:22:33', '2023-05-26T11:22:33.444', @@ -720,17 +595,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { false, ); expect(result.jobNumber).toEqual('00000001'); - // 通知処理が想定通りの引数で呼ばれているか確認 - expect(notificationHubService.notify).toHaveBeenCalledWith( - makeContext('trackingId', 'requestId'), - [`user_${typistUserId}`], - { - authorId: 'XXXXXXXXXX', - filename: 'file', - priority: 'High', - uploadedAt: '2023-05-26T11:22:33.444', - }, - ); + // 通知処理が呼ばれていないことを確認 + expect(notificationHubService.notify).not.toBeCalled(); // 作成したタスクを取得 const resultTask = await getTaskFromJobNumber(source, result.jobNumber); // タスクのチェックアウト権限を取得 @@ -739,13 +605,12 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { resultTask?.id ?? 0, ); // タスクのテンプレートファイルIDを確認 - expect(resultTask?.template_file_id).toEqual(templateFileId); - // タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認 - expect(resultCheckoutPermission.length).toEqual(1); - expect(resultCheckoutPermission[0].user_group_id).toEqual(userGroupId); + expect(resultTask?.template_file_id).toBeNull(); + // 存在しないAuthorIDを指定してタスクを作成したためルーティングが行われず、タスクのチェックアウト権限は誰にも付与されない + expect(resultCheckoutPermission.length).toEqual(0); }); - it('ワークフローが見つからない場合、タスク作成時に、自動ルーティングを行うことができない', async () => { + it('ワークフローが見つからない場合、タスク作成時に自動ルーティングを行うことができない', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); // 音声ファイルの録音者のユーザー @@ -785,7 +650,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { '01', 'DS2', 'comment', - 'worktypeId', + '', optionItemList, false, ); @@ -802,6 +667,97 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { // 自動ルーティングが行われていないことを確認 expect(resultCheckoutPermission.length).toEqual(0); }); + + it('WorkTypeIDの指定がないワークフローで、タスク作成時に自動ルーティングを行うことができる', async () => { + if (!source) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + // 音声ファイルの録音者のユーザー + const { + external_id: authorExternalId, + author_id: authorAuthorId, + id: authorUserId, + } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + author_id: undefined, + }); + // ワークフローを作成 + const { id: workflowId } = await createWorkflow( + source, + accountId, + authorUserId, + undefined, + ); + // ワークフロータイピストを作成 + await createWorkflowTypist(source, workflowId, typistUserId); + + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + + const blobParam = makeBlobstorageServiceMockValue(); + const notificationParam = makeDefaultNotificationhubServiceMockValue(); + + const module = await makeTestingModuleWithBlobAndNotification( + source, + blobParam, + notificationParam, + ); + if (!module) fail(); + const service = module.get(FilesService); + const notificationHubService = module.get( + NotificationhubService, + ); + + const result = await service.uploadFinished( + makeContext('trackingId', 'requestId'), + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + 256, + '01', + 'DS2', + 'comment', + '', + optionItemList, + false, + ); + expect(result.jobNumber).toEqual('00000001'); + // 通知処理が想定通りの引数で呼ばれているか確認 + expect(notificationHubService.notify).toHaveBeenCalledWith( + makeContext('trackingId', 'requestId'), + [`user_${typistUserId}`], + { + authorId: 'AUTHOR_ID', + filename: 'file', + priority: 'High', + uploadedAt: '2023-05-26T11:22:33.444', + }, + ); + // タスクを取得 + const resultTask = await getTaskFromJobNumber(source, result.jobNumber); + // タスクのチェックアウト権限を取得 + const resultCheckoutPermission = await getCheckoutPermissions( + source, + resultTask?.id ?? 0, + ); + // タスクがあることを確認 + expect(resultTask).not.toBeNull(); + // 自動ルーティングが行われていることを確認 + expect(resultCheckoutPermission.length).toEqual(1); + expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); + }); it('第五階層アカウントのストレージ使用量が閾値と同値の場合、メール送信が行われない', async () => { if (!source) fail(); const module = await makeTestingModule(source); diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 3d0084a..b65ed7b 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -71,7 +71,8 @@ export class FilesService { /** * Uploads finished * @param url アップロード先Blob Storage(ファイル名含む) - * @param authorId 自分自身(ログイン認証)したAuthorID + * @param userId 自分自身(ログイン認証)したUserID + * @param authorId 音声ファイルを管理するAuthorのAuthorID * @param fileName 音声ファイル名 * @param duration 音声ファイルの録音時間(ミリ秒の整数値) * @param createdDate 音声ファイルの録音作成日時(開始日時)(yyyy-mm-ddThh:mm:ss.sss)' @@ -248,7 +249,6 @@ export class FilesService { context, task.audio_file_id, user.account_id, - user.author_id ?? undefined, ); const groupMembers = diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 9671a4a..564eaa3 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -2741,7 +2741,9 @@ describe('checkin', () => { ]; }, }); - const spy = jest.spyOn(service["sendgridService"], "sendMail").mockImplementation(); + const spy = jest + .spyOn(service['sendgridService'], 'sendMail') + .mockImplementation(); const initTask = await getTask(source, taskId); @@ -2834,7 +2836,7 @@ describe('checkin', () => { }, }); let _to = Array(10); - overrideSendgridService(service, { + overrideSendgridService(service, { sendMail: async ( context: Context, to: string[], @@ -2844,7 +2846,7 @@ describe('checkin', () => { text: string, html: string, ) => { - _to = to; + _to = to; }, }); @@ -2860,7 +2862,7 @@ describe('checkin', () => { expect(resultTask?.status).toEqual('Finished'); expect(resultTask?.finished_at).not.toEqual(initTask?.finished_at); //メール送信処理が呼ばれていない - expect(_to.length).toBe(1) + expect(_to.length).toBe(1); expect(_to).toEqual(['author@example.com']); }); }); @@ -3491,7 +3493,7 @@ describe('cancel', () => { // ワークフロータイピストを作成 await createWorkflowTypist(source, workflowId, typistUserId); - const { taskId } = await createTask( + const { taskId, audioFileId } = await createTask( source, accountId, authorUserId, @@ -3510,7 +3512,7 @@ describe('cancel', () => { ); await service.cancel( makeContext('trackingId', 'requestId'), - 1, + audioFileId, 'typist-user-external-id', ['typist', 'standard'], ); @@ -3537,7 +3539,7 @@ describe('cancel', () => { ); }); - it('API実行者のRoleがAdminの場合、自身が文字起こし実行中のタスクをキャンセルし、そのタスクの自動ルーティングを行う(API実行者のAuthorIDと音声ファイルに紐づくWorkType)', async () => { + it('API実行者のRoleがAuthor,Adminの場合、文字起こし実行中のタスクをキャンセルし、そのタスクの自動ルーティングを行う', async () => { if (!source) fail(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); @@ -3594,7 +3596,7 @@ describe('cancel', () => { const { id: workflowId } = await createWorkflow( source, accountId, - myAuthorUserId, + authorUserId, workTypeId, templateFileId, ); @@ -3646,6 +3648,106 @@ describe('cancel', () => { }, ); }); + it('API実行者のRoleがAuthor,Adminの場合、文字起こし実行中のタスクをキャンセルするが、一致するワークフローがない場合は自動ルーティングを行うことができない', async () => { + if (!source) fail(); + const notificationhubServiceMockValue = + makeDefaultNotificationhubServiceMockValue(); + const module = await makeTaskTestingModuleWithNotificaiton( + source, + notificationhubServiceMockValue, + ); + 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: autoRoutingTypistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'auto-routing-typist-user-external-id', + role: 'typist', + }); + // API実行者 + const { + id: myAuthorUserId, + external_id, + role, + author_id: myAuthorId, + } = await makeTestUser(source, { + account_id: accountId, + external_id: 'my-author-user-external-id', + role: 'author admin', + author_id: 'MY_AUTHOR_ID', + }); + // 音声ファイルのアップロード者 + const { id: authorUserId, author_id } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + //ワークタイプIDを作成 + const { id: workTypeId, custom_worktype_id } = await createWorktype( + source, + accountId, + '01', + ); + // テンプレートファイルを作成 + const { id: templateFileId } = await createTemplateFile( + source, + accountId, + 'template-file-name', + 'https://example.com', + ); + // ワークフローを作成 + const { id: workflowId } = await createWorkflow( + source, + accountId, + authorUserId, + workTypeId, + templateFileId, + ); + // ワークフロータイピストを作成 + await createWorkflowTypist(source, workflowId, autoRoutingTypistUserId); + + const { taskId, audioFileId } = await createTask( + source, + accountId, + myAuthorUserId, + myAuthorId ?? '', + custom_worktype_id, + '01', + '00000001', + 'InProgress', + typistUserId, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + const service = module.get(TasksService); + const NotificationHubService = module.get( + NotificationhubService, + ); + await service.cancel( + makeContext('trackingId', 'requestId'), + audioFileId, + external_id, + role.split(' ') as Roles[], + ); + const resultTask = await getTask(source, taskId); + const permisions = await getCheckoutPermissions(source, taskId); + + expect(resultTask?.status).toEqual('Uploaded'); + expect(resultTask?.typist_user_id).toEqual(null); + // タスクのテンプレートファイルIDを確認 + expect(resultTask?.template_file_id).toEqual(null); + // タスクのチェックアウト権限が付与されていないことを確認 + expect(permisions.length).toEqual(0); + // 通知処理が想定通りの引数で呼ばれているか確認 + expect(NotificationHubService.notify).not.toBeCalled(); + }); it('API実行者のRoleがTypistの場合、自身が文字起こし実行中のタスクをキャンセルするが、一致するワークフローがない場合は自動ルーティングを行うことができない', async () => { if (!source) fail(); const notificationhubServiceMockValue = diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index f816b8f..cb28c91 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -630,7 +630,6 @@ export class TasksService { context, audioFileId, user.account_id, - user.author_id ?? undefined, ); // 通知を送信する diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index e43c2cf..5272ee2 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -1215,7 +1215,6 @@ export class TasksRepositoryService { context: Context, audioFileId: number, accountId: number, - myAuthorId?: string, // API実行者のAuthorId ): Promise<{ typistIds: number[]; typistGroupIds: number[] }> { return await this.dataSource.transaction(async (entityManager) => { // 音声ファイルを取得 @@ -1243,6 +1242,11 @@ export class TasksRepositoryService { comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, lock: { mode: 'pessimistic_write' }, }); + if (!authorUser) { + throw new Error( + `author not found. author_id:${audio.author_id}, accountId:${accountId}`, + ); + } // TaskとFileを取得 const taskRepo = entityManager.getRepository(Task); @@ -1277,7 +1281,7 @@ export class TasksRepositoryService { }); // 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了 - if (!worktypeRecord && audioFile.work_type_id !== '') { + if (audioFile.work_type_id !== '' && !worktypeRecord) { throw new Error( `worktype not found. worktype:${audioFile.work_type_id}, accountId:${accountId}`, ); @@ -1291,7 +1295,7 @@ export class TasksRepositoryService { }, where: { account_id: accountId, - author_id: authorUser?.id ?? IsNull(), // authorUserが存在しない場合は、必ずヒットしないようにNULLを設定する + author_id: authorUser.id, worktype_id: worktypeRecord?.id ?? IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, @@ -1299,61 +1303,14 @@ export class TasksRepositoryService { }); // Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する - if (workflow) { - return await this.setCheckoutPermissionAndTemplate( - context, - workflow, - task, - accountId, - entityManager, - userRepo, - ); - } - - // 音声ファイルの情報からルーティングルールを取得できない場合は、 - // API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得する - // API実行者のAuthorIdがない場合はエラーを出して終了 - if (!myAuthorId) { - throw new Error(`There is no AuthorId for the API executor.`); - } - // API実行者のAuthorIdをもとにユーザーを取得 - const myAuthorUser = await userRepo.findOne({ - where: { - author_id: myAuthorId, - account_id: accountId, - }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - lock: { mode: 'pessimistic_write' }, - }); - if (!myAuthorUser) { + if (!workflow) { throw new Error( - `user not found. authorId:${myAuthorId}, accountId:${accountId}`, + `workflow not found. authorUserId:${authorUser.id}, accountId:${accountId}, worktypeId:${worktypeRecord?.id}`, ); } - const defaultWorkflow = await workflowRepo.findOne({ - relations: { - workflowTypists: true, - }, - where: { - account_id: accountId, - author_id: myAuthorUser.id, - worktype_id: worktypeRecord?.id ?? IsNull(), - }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - lock: { mode: 'pessimistic_write' }, - }); - - // API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了 - if (!defaultWorkflow) { - throw new Error( - `workflow not found. authorUserId:${myAuthorUser.id}, accountId:${accountId}, worktypeId:${worktypeRecord?.id}`, - ); - } - - // Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する return await this.setCheckoutPermissionAndTemplate( context, - defaultWorkflow, + workflow, task, accountId, entityManager,