Merged PR 681: タスク削除API実装
## 概要 [Task3457: タスク削除API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3457) - タスク削除APIとUTを実装しました。 ## レビューポイント - テストケースは適切でしょうか? - リポジトリでの削除処理は適切でしょうか? - エラー時のコード使い分けは適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
8793606070
commit
d08c6c99af
@ -66,12 +66,10 @@ const AuthPage: React.FC = (): JSX.Element => {
|
||||
clearToken();
|
||||
return;
|
||||
}
|
||||
|
||||
const loginResult = await instance.handleRedirectPromise();
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除(eslint-disable含めて)する。
|
||||
|
||||
if (loginResult && loginResult.account) {
|
||||
const { homeAccountId, idTokenClaims } = loginResult.account;
|
||||
if (idTokenClaims && idTokenClaims.aud) {
|
||||
@ -85,11 +83,11 @@ const AuthPage: React.FC = (): JSX.Element => {
|
||||
localStorageKeyforIdToken,
|
||||
})
|
||||
);
|
||||
|
||||
// トークン取得と設定を行う
|
||||
navigate("/login");
|
||||
}
|
||||
}
|
||||
// ログインページに遷移し、トークン取得と設定を行う
|
||||
// 何らかの原因で、loginResultがnullの場合でも、ログイン画面に遷移する(ログイン画面でトップページに戻る)
|
||||
navigate("/login");
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.log({ e }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除(eslint-disable含めて)する。
|
||||
|
||||
5
dictation_server/db/migrations/052-add-task-index.sql
Normal file
5
dictation_server/db/migrations/052-add-task-index.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- +migrate Up
|
||||
ALTER TABLE `tasks` ADD INDEX `idx_account_id_and_audio_file_id` (account_id,audio_file_id);
|
||||
|
||||
-- +migrate Down
|
||||
ALTER TABLE `tasks` DROP INDEX `idx_account_id_and_audio_file_id`;
|
||||
@ -158,6 +158,12 @@ export const overrideBlobstorageService = <TService>(
|
||||
accountId: number,
|
||||
country: string,
|
||||
) => Promise<void>;
|
||||
deleteFile?: (
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
fileName: string,
|
||||
) => Promise<void>;
|
||||
containerExists?: (
|
||||
context: Context,
|
||||
accountId: number,
|
||||
@ -189,6 +195,12 @@ export const overrideBlobstorageService = <TService>(
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.deleteFile) {
|
||||
Object.defineProperty(obj, obj.deleteFile.name, {
|
||||
value: overrides.deleteFile,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.containerExists) {
|
||||
Object.defineProperty(obj, obj.containerExists.name, {
|
||||
value: overrides.containerExists,
|
||||
|
||||
@ -828,8 +828,7 @@ export class TasksController {
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
// TODO: Task削除処理を実装する
|
||||
console.log(audioFileId);
|
||||
await this.taskService.deleteTask(context, userId, audioFileId);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_
|
||||
import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module';
|
||||
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
|
||||
import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module';
|
||||
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -18,6 +19,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r
|
||||
AdB2cModule,
|
||||
NotificationhubModule,
|
||||
SendGridModule,
|
||||
BlobstorageModule,
|
||||
],
|
||||
providers: [TasksService],
|
||||
controllers: [TasksController],
|
||||
|
||||
@ -14,6 +14,8 @@ import {
|
||||
createCheckoutPermissions,
|
||||
createTask,
|
||||
createUserGroup,
|
||||
getAudioFile,
|
||||
getAudioOptionItems,
|
||||
getCheckoutPermissions,
|
||||
getTask,
|
||||
makeTaskTestingModuleWithNotificaiton,
|
||||
@ -37,6 +39,8 @@ import { createTemplateFile } from '../templates/test/utility';
|
||||
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
|
||||
import { Roles } from '../../common/types/role';
|
||||
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
|
||||
import { overrideBlobstorageService } from '../../common/test/overrides';
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
|
||||
describe('TasksService', () => {
|
||||
it('タスク一覧を取得できる(admin)', async () => {
|
||||
@ -3775,3 +3779,531 @@ describe('getNextTask', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTask', () => {
|
||||
let source: DataSource | null = 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 () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('管理者として、アカウント内のタスクを削除できる', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const authorId = 'AUTHOR_ID';
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
const { taskId, audioFileId } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
authorId,
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task?.id).toBe(taskId);
|
||||
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
|
||||
expect(task?.audio_file_id).toBe(audioFileId);
|
||||
|
||||
expect(audioFile?.id).toBe(audioFileId);
|
||||
expect(audioFile?.file_name).toBe('x.zip');
|
||||
expect(audioFile?.author_id).toBe(authorId);
|
||||
|
||||
expect(checkoutPermissions.length).toBe(1);
|
||||
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
|
||||
|
||||
expect(optionItems.length).toBe(10);
|
||||
}
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const blobStorageService =
|
||||
module.get<BlobstorageService>(BlobstorageService);
|
||||
const context = makeContext(admin.external_id, 'requestId');
|
||||
|
||||
overrideBlobstorageService(service, {
|
||||
deleteFile: jest.fn(),
|
||||
});
|
||||
|
||||
await service.deleteTask(context, admin.external_id, audioFileId);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task).toBe(null);
|
||||
expect(audioFile).toBe(null);
|
||||
expect(checkoutPermissions.length).toBe(0);
|
||||
expect(optionItems.length).toBe(0);
|
||||
|
||||
// Blob削除メソッドが呼ばれているか確認
|
||||
expect(blobStorageService.deleteFile).toBeCalledWith(
|
||||
context,
|
||||
account.id,
|
||||
account.country,
|
||||
'x.zip',
|
||||
);
|
||||
}
|
||||
});
|
||||
it('Authorとして、自身が追加したタスクを削除できる', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account } = await makeTestAccount(source, { tier: 5 });
|
||||
const authorId = 'AUTHOR_ID';
|
||||
const { id: authorUserId, external_id: authorExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
const { taskId, audioFileId } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
authorId,
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task?.id).toBe(taskId);
|
||||
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
|
||||
expect(task?.audio_file_id).toBe(audioFileId);
|
||||
|
||||
expect(audioFile?.id).toBe(audioFileId);
|
||||
expect(audioFile?.file_name).toBe('x.zip');
|
||||
expect(audioFile?.author_id).toBe(authorId);
|
||||
|
||||
expect(checkoutPermissions.length).toBe(1);
|
||||
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
|
||||
|
||||
expect(optionItems.length).toBe(10);
|
||||
}
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const blobStorageService =
|
||||
module.get<BlobstorageService>(BlobstorageService);
|
||||
const context = makeContext(authorExternalId, 'requestId');
|
||||
|
||||
overrideBlobstorageService(service, {
|
||||
deleteFile: jest.fn(),
|
||||
});
|
||||
|
||||
await service.deleteTask(context, authorExternalId, audioFileId);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task).toBe(null);
|
||||
expect(audioFile).toBe(null);
|
||||
expect(checkoutPermissions.length).toBe(0);
|
||||
expect(optionItems.length).toBe(0);
|
||||
|
||||
// Blob削除メソッドが呼ばれているか確認
|
||||
expect(blobStorageService.deleteFile).toBeCalledWith(
|
||||
context,
|
||||
account.id,
|
||||
account.country,
|
||||
'x.zip',
|
||||
);
|
||||
}
|
||||
});
|
||||
it('ステータスがInProgressのタスクを削除しようとした場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account } = await makeTestAccount(source, { tier: 5 });
|
||||
const authorId = 'AUTHOR_ID';
|
||||
const { id: authorUserId, external_id: authorExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
const { taskId, audioFileId } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
authorId,
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.IN_PROGRESS,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task?.id).toBe(taskId);
|
||||
expect(task?.status).toBe(TASK_STATUS.IN_PROGRESS);
|
||||
expect(task?.audio_file_id).toBe(audioFileId);
|
||||
|
||||
expect(audioFile?.id).toBe(audioFileId);
|
||||
expect(audioFile?.file_name).toBe('x.zip');
|
||||
expect(audioFile?.author_id).toBe(authorId);
|
||||
|
||||
expect(checkoutPermissions.length).toBe(1);
|
||||
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
|
||||
|
||||
expect(optionItems.length).toBe(10);
|
||||
}
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(authorExternalId, 'requestId');
|
||||
|
||||
overrideBlobstorageService(service, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
deleteFile: async () => {},
|
||||
});
|
||||
|
||||
try {
|
||||
await service.deleteTask(context, authorExternalId, audioFileId);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010601'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('Authorが自身が作成したタスク以外を削除しようとした場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account } = await makeTestAccount(source, { tier: 5 });
|
||||
const authorId1 = 'AUTHOR_ID1';
|
||||
const authorId2 = 'AUTHOR_ID2';
|
||||
|
||||
const { id: authorUserId1 } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: authorId1,
|
||||
external_id: 'author-user-external-id1',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { external_id: authorExternalId2 } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: authorId2,
|
||||
external_id: 'author-user-external-id2',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
const { taskId, audioFileId } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId1,
|
||||
authorId1,
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task?.id).toBe(taskId);
|
||||
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
|
||||
expect(task?.audio_file_id).toBe(audioFileId);
|
||||
|
||||
expect(audioFile?.id).toBe(audioFileId);
|
||||
expect(audioFile?.file_name).toBe('x.zip');
|
||||
expect(audioFile?.author_id).toBe(authorId1);
|
||||
|
||||
expect(checkoutPermissions.length).toBe(1);
|
||||
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
|
||||
|
||||
expect(optionItems.length).toBe(10);
|
||||
}
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(authorExternalId2, 'requestId');
|
||||
|
||||
overrideBlobstorageService(service, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
deleteFile: async () => {},
|
||||
});
|
||||
|
||||
try {
|
||||
await service.deleteTask(context, authorExternalId2, audioFileId);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010602'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('削除対象タスクが存在しない場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { admin } = await makeTestAccount(source, { tier: 5 });
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(admin.external_id, 'requestId');
|
||||
|
||||
overrideBlobstorageService(service, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
deleteFile: async () => {},
|
||||
});
|
||||
|
||||
try {
|
||||
await service.deleteTask(context, admin.external_id, 1);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010603'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('タスクのDB削除に失敗した場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account } = await makeTestAccount(source, { tier: 5 });
|
||||
const authorId = 'AUTHOR_ID';
|
||||
const { id: authorUserId, external_id: authorExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
const { taskId, audioFileId } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
authorId,
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task?.id).toBe(taskId);
|
||||
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
|
||||
expect(task?.audio_file_id).toBe(audioFileId);
|
||||
|
||||
expect(audioFile?.id).toBe(audioFileId);
|
||||
expect(audioFile?.file_name).toBe('x.zip');
|
||||
expect(audioFile?.author_id).toBe(authorId);
|
||||
|
||||
expect(checkoutPermissions.length).toBe(1);
|
||||
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
|
||||
|
||||
expect(optionItems.length).toBe(10);
|
||||
}
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(authorExternalId, 'requestId');
|
||||
|
||||
// DBアクセスに失敗するようにする
|
||||
const tasksRepositoryService = module.get<TasksRepositoryService>(
|
||||
TasksRepositoryService,
|
||||
);
|
||||
tasksRepositoryService.deleteTask = jest
|
||||
.fn()
|
||||
.mockRejectedValue('DB failed');
|
||||
|
||||
overrideBlobstorageService(service, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
deleteFile: async () => {},
|
||||
});
|
||||
|
||||
try {
|
||||
await service.deleteTask(context, authorExternalId, audioFileId);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('blobストレージからの音声ファイル削除に失敗した場合でも、エラーとならないこと', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const authorId = 'AUTHOR_ID';
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
const { taskId, audioFileId } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
authorId,
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task?.id).toBe(taskId);
|
||||
expect(task?.status).toBe(TASK_STATUS.UPLOADED);
|
||||
expect(task?.audio_file_id).toBe(audioFileId);
|
||||
|
||||
expect(audioFile?.id).toBe(audioFileId);
|
||||
expect(audioFile?.file_name).toBe('x.zip');
|
||||
expect(audioFile?.author_id).toBe(authorId);
|
||||
|
||||
expect(checkoutPermissions.length).toBe(1);
|
||||
expect(checkoutPermissions[0].user_id).toBe(typistUserId);
|
||||
|
||||
expect(optionItems.length).toBe(10);
|
||||
}
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
|
||||
const context = makeContext(admin.external_id, 'requestId');
|
||||
|
||||
overrideBlobstorageService(service, {
|
||||
deleteFile: async () => {
|
||||
throw new Error('blob failed');
|
||||
},
|
||||
});
|
||||
|
||||
await service.deleteTask(context, admin.external_id, audioFileId);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
const task = await getTask(source, taskId);
|
||||
const audioFile = await getAudioFile(source, audioFileId);
|
||||
const checkoutPermissions = await getCheckoutPermissions(source, taskId);
|
||||
const optionItems = await getAudioOptionItems(source, taskId);
|
||||
|
||||
expect(task).toBe(null);
|
||||
expect(audioFile).toBe(null);
|
||||
expect(checkoutPermissions.length).toBe(0);
|
||||
expect(optionItems.length).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
|
||||
import { Assignee, Task } from './types/types';
|
||||
import { Assignee, PostDeleteTaskRequest, Task } from './types/types';
|
||||
import { Task as TaskEntity } from '../../repositories/tasks/entity/task.entity';
|
||||
import { createTasks } from './types/convert';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
@ -9,7 +9,12 @@ import {
|
||||
SortDirection,
|
||||
TaskListSortableAttribute,
|
||||
} from '../../common/types/sort';
|
||||
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
|
||||
import {
|
||||
ADMIN_ROLES,
|
||||
MANUAL_RECOVERY_REQUIRED,
|
||||
TASK_STATUS,
|
||||
USER_ROLES,
|
||||
} from '../../constants';
|
||||
import {
|
||||
AdB2cService,
|
||||
Adb2cTooManyRequestsError,
|
||||
@ -36,6 +41,7 @@ import { User } from '../../repositories/users/entity/user.entity';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
|
||||
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
|
||||
@Injectable()
|
||||
export class TasksService {
|
||||
@ -48,6 +54,7 @@ export class TasksService {
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly sendgridService: SendGridService,
|
||||
private readonly notificationhubService: NotificationhubService,
|
||||
private readonly blobStorageService: BlobstorageService,
|
||||
) {}
|
||||
|
||||
async getTasks(
|
||||
@ -848,6 +855,107 @@ export class TasksService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した音声ファイルに紐づくタスクを削除します
|
||||
* @param context
|
||||
* @param externalId 実行ユーザーの外部ID
|
||||
* @param audioFileId 削除対象のタスクのaudio_file_id
|
||||
* @returns task
|
||||
*/
|
||||
async deleteTask(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
audioFileId: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteTask.name
|
||||
} | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
|
||||
);
|
||||
|
||||
// 実行ユーザーの情報を取得する
|
||||
const user = await this.usersRepository.findUserByExternalId(
|
||||
context,
|
||||
externalId,
|
||||
);
|
||||
|
||||
const account = user.account;
|
||||
if (!account) {
|
||||
throw new Error(`account not found. externalId: ${externalId}`);
|
||||
}
|
||||
|
||||
// 削除対象の音声ファイル情報を取得する
|
||||
const task = await this.taskRepository.getTaskAndAudioFile(
|
||||
context,
|
||||
audioFileId,
|
||||
user.account_id,
|
||||
Object.values(TASK_STATUS),
|
||||
);
|
||||
|
||||
const targetFileName = task.file?.file_name;
|
||||
if (!targetFileName) {
|
||||
throw new Error(`target file not found. audioFileId: ${audioFileId}`);
|
||||
}
|
||||
|
||||
// DBからタスクと紐づくデータを削除する
|
||||
await this.taskRepository.deleteTask(context, user.id, audioFileId);
|
||||
|
||||
// Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行するため、try-catchで囲む
|
||||
try {
|
||||
// BlobStorageから音声ファイルを削除する
|
||||
await this.blobStorageService.deleteFile(
|
||||
context,
|
||||
account.id,
|
||||
account.country,
|
||||
targetFileName,
|
||||
);
|
||||
} catch (e) {
|
||||
// Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
|
||||
this.logger.log(`[${context.getTrackingId()}] ${e}`);
|
||||
this.logger.log(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete Blob: accountId: ${
|
||||
account.id
|
||||
}, fileName: ${targetFileName}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case StatusNotMatchError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010601'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case TaskAuthorIdNotMatchError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010602'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case TasksNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010603'),
|
||||
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.getTrackingId()}] ${this.deleteTask.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 通知を送信するプライベートメソッド
|
||||
private async sendNotify(
|
||||
context: Context,
|
||||
|
||||
@ -17,6 +17,7 @@ import { NotificationhubService } from '../../../gateways/notificationhub/notifi
|
||||
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
|
||||
import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service';
|
||||
import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service';
|
||||
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
|
||||
|
||||
export type TasksRepositoryMockValue = {
|
||||
getTasksFromAccountId:
|
||||
@ -92,6 +93,8 @@ export const makeTasksServiceMock = async (
|
||||
// メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。
|
||||
case SendGridService:
|
||||
return {};
|
||||
case BlobstorageService:
|
||||
return {};
|
||||
}
|
||||
})
|
||||
.compile();
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
NotificationhubServiceMockValue,
|
||||
makeNotificationhubServiceMock,
|
||||
} from './tasks.service.mock';
|
||||
import { AudioOptionItem } from '../../../repositories/audio_option_items/entity/audio_option_item.entity';
|
||||
|
||||
export const makeTaskTestingModuleWithNotificaiton = async (
|
||||
datasource: DataSource,
|
||||
@ -145,6 +146,60 @@ export const createTask = async (
|
||||
created_at: new Date(),
|
||||
});
|
||||
const task = taskIdentifiers.pop() as Task;
|
||||
|
||||
await datasource.getRepository(AudioOptionItem).insert([
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label01',
|
||||
value: 'value01',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label02',
|
||||
value: 'value02',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label03',
|
||||
value: 'value03',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label04',
|
||||
value: 'value04',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label05',
|
||||
value: 'value05',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label06',
|
||||
value: 'value06',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label07',
|
||||
value: 'value07',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label08',
|
||||
value: 'value08',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label09',
|
||||
value: 'value09',
|
||||
},
|
||||
{
|
||||
audio_file_id: audioFile.id,
|
||||
label: 'label10',
|
||||
value: 'value10',
|
||||
},
|
||||
]);
|
||||
|
||||
return { taskId: task.id, audioFileId: audioFile.id };
|
||||
};
|
||||
/**
|
||||
@ -229,3 +284,23 @@ export const getCheckoutPermissions = async (
|
||||
});
|
||||
return permissions;
|
||||
};
|
||||
|
||||
export const getAudioFile = async (
|
||||
datasource: DataSource,
|
||||
audio_file_id: number,
|
||||
): Promise<AudioFile | null> => {
|
||||
const audioFile = await datasource.getRepository(AudioFile).findOne({
|
||||
where: { id: audio_file_id },
|
||||
});
|
||||
return audioFile;
|
||||
};
|
||||
|
||||
export const getAudioOptionItems = async (
|
||||
datasource: DataSource,
|
||||
audio_file_id: number,
|
||||
): Promise<AudioOptionItem[]> => {
|
||||
const audioOptionItems = await datasource
|
||||
.getRepository(AudioOptionItem)
|
||||
.find({ where: { audio_file_id: audio_file_id } });
|
||||
return audioOptionItems;
|
||||
};
|
||||
|
||||
@ -142,6 +142,60 @@ export class BlobstorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定されたファイルを削除します。
|
||||
* @param context
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @param fileName
|
||||
* @returns file
|
||||
*/
|
||||
async deleteFile(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string,
|
||||
fileName: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.deleteFile.name} | params: { ` +
|
||||
`accountId: ${accountId} ` +
|
||||
`country: ${country} ` +
|
||||
`fileName: ${fileName} };`,
|
||||
);
|
||||
|
||||
try {
|
||||
// 国に応じたリージョンでコンテナ名を指定してClientを取得
|
||||
const containerClient = this.getContainerClient(
|
||||
context,
|
||||
accountId,
|
||||
country,
|
||||
);
|
||||
// コンテナ内のBlobパス名を指定してClientを取得
|
||||
const blobClient = containerClient.getBlobClient(fileName);
|
||||
|
||||
const { succeeded, errorCode, date } = await blobClient.deleteIfExists();
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`,
|
||||
);
|
||||
|
||||
// 失敗時、Blobが存在しない場合以外はエラーとして例外をスローする
|
||||
// Blob不在の場合のエラーコードは「BlobNotFound」以下を参照
|
||||
// https://learn.microsoft.com/ja-jp/rest/api/storageservices/blob-service-error-codes
|
||||
if (!succeeded && errorCode !== 'BlobNotFound') {
|
||||
throw new Error(
|
||||
`delete blob failed. succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteFile.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Containers exists
|
||||
* @param country
|
||||
|
||||
@ -10,7 +10,12 @@ import {
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
import { Task } from './entity/task.entity';
|
||||
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
|
||||
import {
|
||||
ADMIN_ROLES,
|
||||
NODE_ENV_TEST,
|
||||
TASK_STATUS,
|
||||
USER_ROLES,
|
||||
} from '../../constants';
|
||||
import { AudioOptionItem as ParamOptionItem } from '../../features/files/types/types';
|
||||
import { AudioFile } from '../audio_files/entity/audio_file.entity';
|
||||
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
|
||||
@ -1280,6 +1285,121 @@ export class TasksRepositoryService {
|
||||
);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Deletes task
|
||||
* @param context
|
||||
* @param userId 削除を行うユーザーID
|
||||
* @param audioFileId 削除を行うタスクの音声ファイルID
|
||||
* @returns task
|
||||
*/
|
||||
async deleteTask(
|
||||
context: Context,
|
||||
userId: number,
|
||||
audioFileId: number,
|
||||
): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
|
||||
// 削除を行うユーザーとアカウントを取得
|
||||
const user = await userRepo.findOne({
|
||||
where: { id: userId },
|
||||
relations: { account: true },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!user) {
|
||||
throw new Error(`user not found. userId:${userId}`);
|
||||
}
|
||||
const account = user.account;
|
||||
if (!account) {
|
||||
throw new Error(`account not found. userId:${userId}`);
|
||||
}
|
||||
|
||||
// ユーザーがアカウントの管理者であるかを確認
|
||||
const isAdmin =
|
||||
account.primary_admin_user_id === userId ||
|
||||
account.secondary_admin_user_id === userId;
|
||||
|
||||
// 削除を行うタスクを取得
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
const task = await taskRepo.findOne({
|
||||
where: {
|
||||
account_id: account.id,
|
||||
audio_file_id: audioFileId,
|
||||
},
|
||||
relations: { file: true },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
// テスト環境の場合はロックを行わない(sqliteがlockに対応していないため)
|
||||
lock:
|
||||
process.env.NODE_ENV !== NODE_ENV_TEST
|
||||
? { mode: 'pessimistic_write' }
|
||||
: undefined,
|
||||
});
|
||||
if (!task) {
|
||||
throw new TasksNotFoundError(
|
||||
`task not found. audio_file_id:${audioFileId}`,
|
||||
);
|
||||
}
|
||||
if (!task.file) {
|
||||
throw new Error(`audio file not found. audio_file_id:${audioFileId}`);
|
||||
}
|
||||
|
||||
// ユーザーが管理者でない場合は、タスクのAuthorIdとユーザーのAuthorIdが一致するかを確認
|
||||
if (!isAdmin) {
|
||||
// ユーザーがAuthorである場合
|
||||
if (user.role === USER_ROLES.AUTHOR) {
|
||||
if (task.file.author_id !== user.author_id) {
|
||||
throw new TaskAuthorIdNotMatchError(
|
||||
`Task authorId not match. userId:${userId}, authorId:${user.author_id}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// ユーザーが管理者でもAuthorでもない場合はエラー
|
||||
throw new Error(`The user is not admin or author. userId:${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// タスクのステータスがInProgressの場合はエラー
|
||||
if (task.status === TASK_STATUS.IN_PROGRESS) {
|
||||
throw new StatusNotMatchError(
|
||||
`task status is InProgress. audio_file_id:${audioFileId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// タスクに紐づくオプションアイテムを削除
|
||||
const optionItemRepo = entityManager.getRepository(AudioOptionItem);
|
||||
await deleteEntity(
|
||||
optionItemRepo,
|
||||
{ audio_file_id: task.audio_file_id },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// タスクに紐づくチェックアウト候補を削除
|
||||
const checkoutRepo = entityManager.getRepository(CheckoutPermission);
|
||||
await deleteEntity(
|
||||
checkoutRepo,
|
||||
{ task_id: task.id },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// タスクを削除
|
||||
await deleteEntity(
|
||||
taskRepo,
|
||||
{ audio_file_id: audioFileId },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// タスクに紐づく音声ファイル情報を削除
|
||||
const audioFileRepo = entityManager.getRepository(AudioFile);
|
||||
await deleteEntity(
|
||||
audioFileRepo,
|
||||
{ id: audioFileId },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* workflowに紐づけられているタイピスト・タイピストグループで、タスクのチェックアウト権限を設定
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user