Merged PR 921: API修正

## 概要
[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環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2024-09-18 01:35:28 +00:00
parent 6690302ac3
commit f1b75a7ff0
5 changed files with 222 additions and 208 deletions

View File

@ -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>(FilesService);
const notificationHubService = module.get<NotificationhubService>(
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>(FilesService);
const notificationHubService = module.get<NotificationhubService>(
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);

View File

@ -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 =

View File

@ -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>(TasksService);
const NotificationHubService = module.get<NotificationhubService>(
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 =

View File

@ -630,7 +630,6 @@ export class TasksService {
context,
audioFileId,
user.account_id,
user.author_id ?? undefined,
);
// 通知を送信する

View File

@ -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,