Merged PR 208: API実装(音声ファイルDL元)
## 概要 [Task2038: API実装(音声ファイルDL元)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2038) - 音声ファイルのダウンロードURL取得API&テストを実装しました。 - SASトークンの有効期限を2時間にしています。 - タスク作成APIでURLのSASトークンを取り除く処理を追加しています。 ## レビューポイント - URLの生成に問題はないか - テストのためにmodule生成処理を追加したが問題ないか - タスク作成APIでのURL処理は認識通りか ※対象外:テンプレートファイル関連 以下はコード整形による変更なので対象外 - licenses.repository.module.ts - tasks.service.spec.ts ## UIの変更 なし ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
766c995ceb
commit
773c8894e7
@ -13,3 +13,4 @@ TENANT_NAME=adb2codmsdev
|
|||||||
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
||||||
EMAIL_CONFIRM_LIFETIME=86400000
|
EMAIL_CONFIRM_LIFETIME=86400000
|
||||||
APP_DOMAIN=https://10.1.0.10:4443/
|
APP_DOMAIN=https://10.1.0.10:4443/
|
||||||
|
STORAGE_TOKEN_EXPIRE_TIME=2
|
||||||
@ -16,7 +16,6 @@ MAIL_FROM=xxxxx@xxxxx.xxxx
|
|||||||
NOTIFICATION_HUB_NAME=ntf-odms-shared
|
NOTIFICATION_HUB_NAME=ntf-odms-shared
|
||||||
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
|
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
|
||||||
APP_DOMAIN=http://localhost:8081/
|
APP_DOMAIN=http://localhost:8081/
|
||||||
STORAGE_TOKEN_EXPIRE_TIME=30
|
|
||||||
STORAGE_ACCOUNT_NAME_US=saodmsusdev
|
STORAGE_ACCOUNT_NAME_US=saodmsusdev
|
||||||
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
|
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
|
||||||
STORAGE_ACCOUNT_NAME_EU=saodmseudev
|
STORAGE_ACCOUNT_NAME_EU=saodmseudev
|
||||||
|
|||||||
@ -36,4 +36,6 @@ export const ErrorCodes = [
|
|||||||
'E010501', // アカウント不在エラー
|
'E010501', // アカウント不在エラー
|
||||||
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
||||||
'E010602', // タスク変更権限不足エラー
|
'E010602', // タスク変更権限不足エラー
|
||||||
|
'E010603', // タスク不在エラー
|
||||||
|
'E010701', // Blobファイル不在エラー
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@ -25,4 +25,6 @@ export const errors: Errors = {
|
|||||||
E010501: 'Account not Found Error.',
|
E010501: 'Account not Found Error.',
|
||||||
E010601: 'Task is not Editable Error',
|
E010601: 'Task is not Editable Error',
|
||||||
E010602: 'No task edit permissions Error',
|
E010602: 'No task edit permissions Error',
|
||||||
|
E010603: 'Task not found Error.',
|
||||||
|
E010701: 'File not found in Blob Storage Error.',
|
||||||
};
|
};
|
||||||
|
|||||||
2
dictation_server/src/features/files/errors/types.ts
Normal file
2
dictation_server/src/features/files/errors/types.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// 音声ファイル不在エラー
|
||||||
|
export class AudioFileNotFoundError extends Error {}
|
||||||
@ -178,16 +178,24 @@ export class FilesController {
|
|||||||
'指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します',
|
'指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します',
|
||||||
})
|
})
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@UseGuards(
|
||||||
|
RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST] }),
|
||||||
|
)
|
||||||
async downloadLocation(
|
async downloadLocation(
|
||||||
@Headers() headers,
|
@Req() req: Request,
|
||||||
@Query() body: AudioDownloadLocationRequest,
|
@Query() body: AudioDownloadLocationRequest,
|
||||||
): Promise<AudioDownloadLocationResponse> {
|
): Promise<AudioDownloadLocationResponse> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { audioFileId } = body;
|
const { audioFileId } = body;
|
||||||
// コンテナ作成処理の前にアクセストークンの認証を行う
|
|
||||||
//
|
|
||||||
|
|
||||||
return { url: '' };
|
const token = retrieveAuthorizationToken(req);
|
||||||
|
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
|
||||||
|
const url = await this.filesService.publishAudioFileDownloadSas(
|
||||||
|
accessToken.userId,
|
||||||
|
audioFileId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { url };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('template/download-location')
|
@Get('template/download-location')
|
||||||
|
|||||||
@ -6,8 +6,16 @@ import {
|
|||||||
makeDefaultUsersRepositoryMockValue,
|
makeDefaultUsersRepositoryMockValue,
|
||||||
makeFilesServiceMock,
|
makeFilesServiceMock,
|
||||||
} from './test/files.service.mock';
|
} from './test/files.service.mock';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import {
|
||||||
|
createAccount,
|
||||||
|
createTask,
|
||||||
|
createUser,
|
||||||
|
makeTestingModuleWithBlob,
|
||||||
|
} from './test/utility';
|
||||||
|
import { FilesService } from './files.service';
|
||||||
|
|
||||||
describe('FilesService', () => {
|
describe('音声ファイルアップロードURL取得', () => {
|
||||||
it('アップロードSASトークンが乗っているURLを返却する', async () => {
|
it('アップロードSASトークンが乗っているURLを返却する', async () => {
|
||||||
const blobParam = makeBlobstorageServiceMockValue();
|
const blobParam = makeBlobstorageServiceMockValue();
|
||||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||||
@ -95,7 +103,9 @@ describe('FilesService', () => {
|
|||||||
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
|
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('タスク作成', () => {
|
||||||
it('文字起こしタスクを作成できる', async () => {
|
it('文字起こしタスクを作成できる', async () => {
|
||||||
const blobParam = makeBlobstorageServiceMockValue();
|
const blobParam = makeBlobstorageServiceMockValue();
|
||||||
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
const userRepoParam = makeDefaultUsersRepositoryMockValue();
|
||||||
@ -271,6 +281,139 @@ describe('FilesService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('音声ファイルダウンロードURL取得', () => {
|
||||||
|
let source: DataSource = 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 () => {
|
||||||
|
await source.destroy();
|
||||||
|
source = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
|
||||||
|
const { accountId } = await createAccount(source);
|
||||||
|
const { externalId, userId } = await createUser(
|
||||||
|
source,
|
||||||
|
accountId,
|
||||||
|
'author-user-external-id',
|
||||||
|
'author',
|
||||||
|
'AUTHOR_ID',
|
||||||
|
);
|
||||||
|
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
|
||||||
|
|
||||||
|
const { audioFileId } = await createTask(
|
||||||
|
source,
|
||||||
|
accountId,
|
||||||
|
url,
|
||||||
|
'test.zip',
|
||||||
|
);
|
||||||
|
|
||||||
|
const blobParam = makeBlobstorageServiceMockValue();
|
||||||
|
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||||
|
blobParam.fileExists = true;
|
||||||
|
|
||||||
|
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||||
|
const service = module.get<FilesService>(FilesService);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await service.publishAudioFileDownloadSas(externalId, audioFileId),
|
||||||
|
).toEqual(`${url}?sas-token`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Typistの場合、タスクのステータスが[Uploaded,Inprogress,Pending]以外でエラー', async () => {
|
||||||
|
const { accountId } = await createAccount(source);
|
||||||
|
const { externalId, userId } = await createUser(
|
||||||
|
source,
|
||||||
|
accountId,
|
||||||
|
'typist-user-external-id',
|
||||||
|
'typist',
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
|
||||||
|
|
||||||
|
const { audioFileId } = await createTask(
|
||||||
|
source,
|
||||||
|
accountId,
|
||||||
|
url,
|
||||||
|
'test.zip',
|
||||||
|
);
|
||||||
|
|
||||||
|
const blobParam = makeBlobstorageServiceMockValue();
|
||||||
|
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||||
|
blobParam.fileExists = true;
|
||||||
|
|
||||||
|
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||||
|
const service = module.get<FilesService>(FilesService);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await service.publishAudioFileDownloadSas(externalId, audioFileId),
|
||||||
|
).toEqual(`${url}?sas-token`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Taskが存在しない場合はエラーとなる', async () => {
|
||||||
|
const { accountId } = await createAccount(source);
|
||||||
|
const { externalId } = await createUser(
|
||||||
|
source,
|
||||||
|
accountId,
|
||||||
|
'author-user-external-id',
|
||||||
|
'author',
|
||||||
|
'AUTHOR_ID',
|
||||||
|
);
|
||||||
|
|
||||||
|
const blobParam = makeBlobstorageServiceMockValue();
|
||||||
|
|
||||||
|
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||||
|
const service = module.get<FilesService>(FilesService);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.publishAudioFileDownloadSas(externalId, 1),
|
||||||
|
).rejects.toEqual(
|
||||||
|
new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
|
||||||
|
const { accountId } = await createAccount(source);
|
||||||
|
const { externalId, userId } = await createUser(
|
||||||
|
source,
|
||||||
|
accountId,
|
||||||
|
'author-user-external-id',
|
||||||
|
'author',
|
||||||
|
'AUTHOR_ID',
|
||||||
|
);
|
||||||
|
const url = `https://saodmsusdev.blob.core.windows.net/account-${accountId}/${userId}`;
|
||||||
|
|
||||||
|
const { audioFileId } = await createTask(
|
||||||
|
source,
|
||||||
|
accountId,
|
||||||
|
url,
|
||||||
|
'test.zip',
|
||||||
|
);
|
||||||
|
|
||||||
|
const blobParam = makeBlobstorageServiceMockValue();
|
||||||
|
blobParam.publishDownloadSas = `${url}?sas-token`;
|
||||||
|
blobParam.fileExists = false;
|
||||||
|
|
||||||
|
const module = await makeTestingModuleWithBlob(source, blobParam);
|
||||||
|
const service = module.get<FilesService>(FilesService);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.publishAudioFileDownloadSas(externalId, audioFileId),
|
||||||
|
).rejects.toEqual(
|
||||||
|
new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const optionItemList = [
|
const optionItemList = [
|
||||||
{
|
{
|
||||||
optionItemLabel: 'label_01',
|
optionItemLabel: 'label_01',
|
||||||
|
|||||||
@ -5,14 +5,17 @@ import { UsersRepositoryService } from '../../repositories/users/users.repositor
|
|||||||
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
|
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
|
||||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||||
import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types';
|
import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types';
|
||||||
import { OPTION_ITEM_NUM } from '../../constants/index';
|
import { OPTION_ITEM_NUM, USER_ROLES } from '../../constants/index';
|
||||||
import { User } from '../../repositories/users/entity/user.entity';
|
import { User } from '../../repositories/users/entity/user.entity';
|
||||||
|
import { AudioFileNotFoundError } from './errors/types';
|
||||||
|
import { TasksNotFoundError } from '../../repositories/tasks/errors/types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FilesService {
|
export class FilesService {
|
||||||
private readonly logger = new Logger(FilesService.name);
|
private readonly logger = new Logger(FilesService.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersRepository: UsersRepositoryService,
|
private readonly usersRepository: UsersRepositoryService,
|
||||||
|
private readonly tasksRepository: TasksRepositoryService,
|
||||||
private readonly tasksRepositoryService: TasksRepositoryService,
|
private readonly tasksRepositoryService: TasksRepositoryService,
|
||||||
private readonly blobStorageService: BlobstorageService,
|
private readonly blobStorageService: BlobstorageService,
|
||||||
) {}
|
) {}
|
||||||
@ -112,13 +115,19 @@ export class FilesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// URLにSASトークンがついている場合は取り除く
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
urlObj.search = '';
|
||||||
|
const fileUrl = urlObj.toString();
|
||||||
|
this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`);
|
||||||
|
|
||||||
// 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加)
|
// 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加)
|
||||||
// 追加時に末尾のJOBナンバーにインクリメントする
|
// 追加時に末尾のJOBナンバーにインクリメントする
|
||||||
const task = await this.tasksRepositoryService.create(
|
const task = await this.tasksRepositoryService.create(
|
||||||
user.account_id,
|
user.account_id,
|
||||||
user.id,
|
user.id,
|
||||||
priority,
|
priority,
|
||||||
url,
|
fileUrl,
|
||||||
fileName,
|
fileName,
|
||||||
authorId,
|
authorId,
|
||||||
workType,
|
workType,
|
||||||
@ -201,4 +210,98 @@ export class FilesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定したIDの音声ファイルのダウンロードURLを取得する
|
||||||
|
* @param externalId
|
||||||
|
* @param audioFileId
|
||||||
|
* @returns audio file download sas
|
||||||
|
*/
|
||||||
|
async publishAudioFileDownloadSas(
|
||||||
|
externalId: string,
|
||||||
|
audioFileId: number,
|
||||||
|
): Promise<string> {
|
||||||
|
//DBから国情報とアカウントID,ユーザーIDを取得する
|
||||||
|
let accountId: number;
|
||||||
|
let country: string;
|
||||||
|
let userId: number;
|
||||||
|
let isTypist: boolean;
|
||||||
|
try {
|
||||||
|
const user = await this.usersRepository.findUserByExternalId(externalId);
|
||||||
|
accountId = user.account.id;
|
||||||
|
userId = user.id;
|
||||||
|
country = user.account.country;
|
||||||
|
isTypist = user.role === USER_ROLES.TYPIST;
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`error=${e}`);
|
||||||
|
console.log(e);
|
||||||
|
throw new HttpException(
|
||||||
|
makeErrorResponse('E009999'),
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { file } = await this.tasksRepository.getTaskAndAudioFile(
|
||||||
|
audioFileId,
|
||||||
|
accountId,
|
||||||
|
isTypist,
|
||||||
|
);
|
||||||
|
|
||||||
|
// タスクに紐づく音声ファイルだけが消される場合がある。
|
||||||
|
// その場合はダウンロード不可なので不在エラーとして扱う
|
||||||
|
if (!file) {
|
||||||
|
throw new AudioFileNotFoundError(
|
||||||
|
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = `${userId}/${file.file_name}`;
|
||||||
|
|
||||||
|
const isFileExist = await this.blobStorageService.fileExists(
|
||||||
|
accountId,
|
||||||
|
country,
|
||||||
|
filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isFileExist) {
|
||||||
|
throw new AudioFileNotFoundError(
|
||||||
|
`Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${file.file_name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SASトークン発行
|
||||||
|
const url = await this.blobStorageService.publishDownloadSas(
|
||||||
|
accountId,
|
||||||
|
country,
|
||||||
|
filePath,
|
||||||
|
);
|
||||||
|
return url;
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`error=${e}`);
|
||||||
|
if (e instanceof Error) {
|
||||||
|
switch (e.constructor) {
|
||||||
|
case TasksNotFoundError:
|
||||||
|
throw new HttpException(
|
||||||
|
makeErrorResponse('E010603'),
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
case AudioFileNotFoundError:
|
||||||
|
throw new HttpException(
|
||||||
|
makeErrorResponse('E010701'),
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new HttpException(
|
||||||
|
makeErrorResponse('E009999'),
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new HttpException(
|
||||||
|
makeErrorResponse('E009999'),
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,9 @@ import { Task } from '../../../repositories/tasks/entity/task.entity';
|
|||||||
export type BlobstorageServiceMockValue = {
|
export type BlobstorageServiceMockValue = {
|
||||||
createContainer: void | Error;
|
createContainer: void | Error;
|
||||||
containerExists: boolean | Error;
|
containerExists: boolean | Error;
|
||||||
|
fileExists: boolean | Error;
|
||||||
publishUploadSas: string | Error;
|
publishUploadSas: string | Error;
|
||||||
|
publishDownloadSas: string | Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UsersRepositoryMockValue = {
|
export type UsersRepositoryMockValue = {
|
||||||
@ -47,13 +49,23 @@ export const makeFilesServiceMock = async (
|
|||||||
export const makeBlobstorageServiceMock = (
|
export const makeBlobstorageServiceMock = (
|
||||||
value: BlobstorageServiceMockValue,
|
value: BlobstorageServiceMockValue,
|
||||||
) => {
|
) => {
|
||||||
const { containerExists, createContainer, publishUploadSas } = value;
|
const {
|
||||||
|
containerExists,
|
||||||
|
fileExists,
|
||||||
|
createContainer,
|
||||||
|
publishUploadSas,
|
||||||
|
publishDownloadSas,
|
||||||
|
} = value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
containerExists:
|
containerExists:
|
||||||
containerExists instanceof Error
|
containerExists instanceof Error
|
||||||
? jest.fn<Promise<void>, []>().mockRejectedValue(containerExists)
|
? jest.fn<Promise<void>, []>().mockRejectedValue(containerExists)
|
||||||
: jest.fn<Promise<boolean>, []>().mockResolvedValue(containerExists),
|
: jest.fn<Promise<boolean>, []>().mockResolvedValue(containerExists),
|
||||||
|
fileExists:
|
||||||
|
fileExists instanceof Error
|
||||||
|
? jest.fn<Promise<void>, []>().mockRejectedValue(fileExists)
|
||||||
|
: jest.fn<Promise<boolean>, []>().mockResolvedValue(fileExists),
|
||||||
createContainer:
|
createContainer:
|
||||||
createContainer instanceof Error
|
createContainer instanceof Error
|
||||||
? jest.fn<Promise<void>, []>().mockRejectedValue(createContainer)
|
? jest.fn<Promise<void>, []>().mockRejectedValue(createContainer)
|
||||||
@ -62,6 +74,10 @@ export const makeBlobstorageServiceMock = (
|
|||||||
publishUploadSas instanceof Error
|
publishUploadSas instanceof Error
|
||||||
? jest.fn<Promise<void>, []>().mockRejectedValue(publishUploadSas)
|
? jest.fn<Promise<void>, []>().mockRejectedValue(publishUploadSas)
|
||||||
: jest.fn<Promise<string>, []>().mockResolvedValue(publishUploadSas),
|
: jest.fn<Promise<string>, []>().mockResolvedValue(publishUploadSas),
|
||||||
|
publishDownloadSas:
|
||||||
|
publishDownloadSas instanceof Error
|
||||||
|
? jest.fn<Promise<void>, []>().mockRejectedValue(publishDownloadSas)
|
||||||
|
: jest.fn<Promise<string>, []>().mockResolvedValue(publishDownloadSas),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,7 +96,9 @@ export const makeBlobstorageServiceMockValue =
|
|||||||
(): BlobstorageServiceMockValue => {
|
(): BlobstorageServiceMockValue => {
|
||||||
return {
|
return {
|
||||||
containerExists: true,
|
containerExists: true,
|
||||||
|
fileExists: true,
|
||||||
publishUploadSas: 'https://blob-storage?sas-token',
|
publishUploadSas: 'https://blob-storage?sas-token',
|
||||||
|
publishDownloadSas: 'https://blob-storage?sas-token',
|
||||||
createContainer: undefined,
|
createContainer: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
188
dictation_server/src/features/files/test/utility.ts
Normal file
188
dictation_server/src/features/files/test/utility.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { UserGroupsRepositoryModule } from '../../../repositories/user_groups/user_groups.repository.module';
|
||||||
|
import { TasksRepositoryModule } from '../../../repositories/tasks/tasks.repository.module';
|
||||||
|
import { AuthModule } from '../../../features/auth/auth.module';
|
||||||
|
import { AdB2cModule } from '../../../gateways/adb2c/adb2c.module';
|
||||||
|
import { AccountsModule } from '../../../features/accounts/accounts.module';
|
||||||
|
import { UsersModule } from '../../../features/users/users.module';
|
||||||
|
import { FilesModule } from '../../../features/files/files.module';
|
||||||
|
import { TasksModule } from '../../../features/tasks/tasks.module';
|
||||||
|
import { SendGridModule } from '../../../features/../gateways/sendgrid/sendgrid.module';
|
||||||
|
import { LicensesModule } from '../../../features/licenses/licenses.module';
|
||||||
|
import { AccountsRepositoryModule } from '../../../repositories/accounts/accounts.repository.module';
|
||||||
|
import { UsersRepositoryModule } from '../../../repositories/users/users.repository.module';
|
||||||
|
import { LicensesRepositoryModule } from '../../../repositories/licenses/licenses.repository.module';
|
||||||
|
import { AudioFilesRepositoryModule } from '../../../repositories/audio_files/audio_files.repository.module';
|
||||||
|
import { AudioOptionItemsRepositoryModule } from '../../../repositories/audio_option_items/audio_option_items.repository.module';
|
||||||
|
import { CheckoutPermissionsRepositoryModule } from '../../../repositories/checkout_permissions/checkout_permissions.repository.module';
|
||||||
|
import { NotificationModule } from '../../../features//notification/notification.module';
|
||||||
|
import { NotificationhubModule } from '../../../gateways/notificationhub/notificationhub.module';
|
||||||
|
import { BlobstorageModule } from '../../../gateways/blobstorage/blobstorage.module';
|
||||||
|
import { AuthGuardsModule } from '../../../common/guards/auth/authguards.module';
|
||||||
|
import { SortCriteriaRepositoryModule } from '../../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||||
|
import { AuthService } from '../../../features/auth/auth.service';
|
||||||
|
import { AccountsService } from '../../../features/accounts/accounts.service';
|
||||||
|
import { UsersService } from '../../../features/users/users.service';
|
||||||
|
import { NotificationhubService } from '../../../gateways/notificationhub/notificationhub.service';
|
||||||
|
import { FilesService } from '../../../features/files/files.service';
|
||||||
|
import { LicensesService } from '../../../features/licenses/licenses.service';
|
||||||
|
import { TasksService } from '../../../features/tasks/tasks.service';
|
||||||
|
import { Task } from '../../../repositories/tasks/entity/task.entity';
|
||||||
|
import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity';
|
||||||
|
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
|
||||||
|
import {
|
||||||
|
BlobstorageServiceMockValue,
|
||||||
|
makeBlobstorageServiceMock,
|
||||||
|
} from './files.service.mock';
|
||||||
|
import { User } from '../../../repositories/users/entity/user.entity';
|
||||||
|
import { Account } from '../../../repositories/accounts/entity/account.entity';
|
||||||
|
|
||||||
|
export const createAccount = async (
|
||||||
|
datasource: DataSource,
|
||||||
|
): Promise<{ accountId: number }> => {
|
||||||
|
const { identifiers } = await datasource.getRepository(Account).insert({
|
||||||
|
tier: 5,
|
||||||
|
country: 'US',
|
||||||
|
delegation_permission: false,
|
||||||
|
locked: false,
|
||||||
|
company_name: 'test inc.',
|
||||||
|
verified: true,
|
||||||
|
deleted_at: '',
|
||||||
|
created_by: 'test_runner',
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_by: 'updater',
|
||||||
|
updated_at: new Date(),
|
||||||
|
});
|
||||||
|
const account = identifiers.pop() as Account;
|
||||||
|
return { accountId: account.id };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createUser = async (
|
||||||
|
datasource: DataSource,
|
||||||
|
accountId: number,
|
||||||
|
external_id: string,
|
||||||
|
role: string,
|
||||||
|
author_id?: string | undefined,
|
||||||
|
): Promise<{ userId: number; externalId: string }> => {
|
||||||
|
const { identifiers } = await datasource.getRepository(User).insert({
|
||||||
|
account_id: accountId,
|
||||||
|
external_id: external_id,
|
||||||
|
role: role,
|
||||||
|
accepted_terms_version: '1.0',
|
||||||
|
author_id: author_id,
|
||||||
|
email_verified: true,
|
||||||
|
auto_renew: true,
|
||||||
|
license_alert: true,
|
||||||
|
notification: true,
|
||||||
|
created_by: 'test_runner',
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_by: 'updater',
|
||||||
|
updated_at: new Date(),
|
||||||
|
});
|
||||||
|
const user = identifiers.pop() as User;
|
||||||
|
return { userId: user.id, externalId: external_id };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTask = async (
|
||||||
|
datasource: DataSource,
|
||||||
|
account_id: number,
|
||||||
|
url: string,
|
||||||
|
filename: string,
|
||||||
|
): Promise<{ audioFileId: number }> => {
|
||||||
|
const { identifiers: audioFileIdentifiers } = await datasource
|
||||||
|
.getRepository(AudioFile)
|
||||||
|
.insert({
|
||||||
|
account_id: account_id,
|
||||||
|
owner_user_id: 1,
|
||||||
|
url: url,
|
||||||
|
file_name: filename,
|
||||||
|
author_id: 'AUTHOR_ID',
|
||||||
|
work_type_id: 'work_type_id',
|
||||||
|
started_at: new Date(),
|
||||||
|
duration: '100000',
|
||||||
|
finished_at: new Date(),
|
||||||
|
uploaded_at: new Date(),
|
||||||
|
file_size: 10000,
|
||||||
|
priority: '00',
|
||||||
|
audio_format: 'audio_format',
|
||||||
|
is_encrypted: true,
|
||||||
|
});
|
||||||
|
const audioFile = audioFileIdentifiers.pop() as AudioFile;
|
||||||
|
const { identifiers: taskIdentifiers } = await datasource
|
||||||
|
.getRepository(Task)
|
||||||
|
.insert({
|
||||||
|
job_number: '00000001',
|
||||||
|
account_id: account_id,
|
||||||
|
is_job_number_enabled: true,
|
||||||
|
audio_file_id: audioFile.id,
|
||||||
|
status: 'Uploaded',
|
||||||
|
priority: '01',
|
||||||
|
started_at: new Date().toISOString(),
|
||||||
|
created_at: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { audioFileId: audioFile.id };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeTestingModuleWithBlob = async (
|
||||||
|
datasource: DataSource,
|
||||||
|
blobStorageService: BlobstorageServiceMockValue,
|
||||||
|
): Promise<TestingModule> => {
|
||||||
|
try {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
envFilePath: ['.env.local', '.env'],
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
AuthModule,
|
||||||
|
AdB2cModule,
|
||||||
|
AccountsModule,
|
||||||
|
UsersModule,
|
||||||
|
FilesModule,
|
||||||
|
TasksModule,
|
||||||
|
UsersModule,
|
||||||
|
SendGridModule,
|
||||||
|
LicensesModule,
|
||||||
|
AccountsRepositoryModule,
|
||||||
|
UsersRepositoryModule,
|
||||||
|
LicensesRepositoryModule,
|
||||||
|
AudioFilesRepositoryModule,
|
||||||
|
AudioOptionItemsRepositoryModule,
|
||||||
|
TasksRepositoryModule,
|
||||||
|
CheckoutPermissionsRepositoryModule,
|
||||||
|
UserGroupsRepositoryModule,
|
||||||
|
UserGroupsRepositoryModule,
|
||||||
|
NotificationModule,
|
||||||
|
NotificationhubModule,
|
||||||
|
BlobstorageModule,
|
||||||
|
AuthGuardsModule,
|
||||||
|
SortCriteriaRepositoryModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
AccountsService,
|
||||||
|
UsersService,
|
||||||
|
NotificationhubService,
|
||||||
|
FilesService,
|
||||||
|
TasksService,
|
||||||
|
LicensesService,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.useMocker(async (token) => {
|
||||||
|
switch (token) {
|
||||||
|
case DataSource:
|
||||||
|
return datasource;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.overrideProvider(BlobstorageService)
|
||||||
|
.useValue(makeBlobstorageServiceMock(blobStorageService))
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
return module;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -19,7 +19,6 @@ import {
|
|||||||
} from './test/utility';
|
} from './test/utility';
|
||||||
import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service';
|
import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service';
|
||||||
import { makeTestingModule } from '../../common/test/modules';
|
import { makeTestingModule } from '../../common/test/modules';
|
||||||
import { TasksNotFoundError } from '../../repositories/tasks/errors/types';
|
|
||||||
|
|
||||||
describe('TasksService', () => {
|
describe('TasksService', () => {
|
||||||
it('タスク一覧を取得できる(admin)', async () => {
|
it('タスク一覧を取得できる(admin)', async () => {
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import {
|
|||||||
StorageSharedKeyCredential,
|
StorageSharedKeyCredential,
|
||||||
ContainerClient,
|
ContainerClient,
|
||||||
ContainerSASPermissions,
|
ContainerSASPermissions,
|
||||||
|
BlobSASPermissions,
|
||||||
generateBlobSASQueryParameters,
|
generateBlobSASQueryParameters,
|
||||||
|
BlobClient,
|
||||||
} from '@azure/storage-blob';
|
} from '@azure/storage-blob';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
@ -83,6 +85,27 @@ export class BlobstorageService {
|
|||||||
return exists;
|
return exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Files exists
|
||||||
|
* @param accountId
|
||||||
|
* @param country
|
||||||
|
* @param path
|
||||||
|
* @param fileName
|
||||||
|
* @param containerUrl
|
||||||
|
* @returns exists
|
||||||
|
*/
|
||||||
|
async fileExists(
|
||||||
|
accountId: number,
|
||||||
|
country: string,
|
||||||
|
filePath: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const containerClient = this.getContainerClient(accountId, country);
|
||||||
|
const blob = containerClient.getBlobClient(`${filePath}`);
|
||||||
|
const exists = await blob.exists();
|
||||||
|
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SASトークン付きのBlobStorageアップロードURLを生成し返却します
|
* SASトークン付きのBlobStorageアップロードURLを生成し返却します
|
||||||
* @param accountId
|
* @param accountId
|
||||||
@ -109,10 +132,9 @@ export class BlobstorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//SASの有効期限を設定
|
//SASの有効期限を設定
|
||||||
//TODO 有効期限は仮で30分
|
|
||||||
const expiryDate = new Date();
|
const expiryDate = new Date();
|
||||||
expiryDate.setMinutes(
|
expiryDate.setHours(
|
||||||
expiryDate.getMinutes() +
|
expiryDate.getHours() +
|
||||||
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -137,6 +159,63 @@ export class BlobstorageService {
|
|||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SASトークン付きのBlobStorageダウンロードURLを生成し返却します
|
||||||
|
* @param accountId
|
||||||
|
* @param country
|
||||||
|
* @param path
|
||||||
|
* @param fileName
|
||||||
|
* @returns download sas
|
||||||
|
*/
|
||||||
|
async publishDownloadSas(
|
||||||
|
accountId: number,
|
||||||
|
country: string,
|
||||||
|
filePath: string,
|
||||||
|
): Promise<string> {
|
||||||
|
this.logger.log(`[IN] ${this.publishDownloadSas.name}`);
|
||||||
|
let containerClient: ContainerClient;
|
||||||
|
let blobClient: BlobClient;
|
||||||
|
let sharedKeyCredential: StorageSharedKeyCredential;
|
||||||
|
try {
|
||||||
|
// コンテナ名を指定してClientを取得
|
||||||
|
containerClient = this.getContainerClient(accountId, country);
|
||||||
|
// コンテナ内のBlobパス名を指定してClientを取得
|
||||||
|
blobClient = containerClient.getBlobClient(`${filePath}`);
|
||||||
|
// 国に対応したリージョンの接続情報を取得する
|
||||||
|
sharedKeyCredential = this.getSharedKeyCredential(country);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`error=${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
//SASの有効期限を設定
|
||||||
|
const expiryDate = new Date();
|
||||||
|
expiryDate.setHours(
|
||||||
|
expiryDate.getHours() +
|
||||||
|
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||||
|
);
|
||||||
|
|
||||||
|
//SASの権限を設定(ダウンロードのため読み取り許可)
|
||||||
|
const permissions = new BlobSASPermissions();
|
||||||
|
permissions.read = true;
|
||||||
|
|
||||||
|
//SASを発行
|
||||||
|
const sasToken = generateBlobSASQueryParameters(
|
||||||
|
{
|
||||||
|
containerName: containerClient.containerName,
|
||||||
|
blobName: blobClient.name,
|
||||||
|
permissions: permissions,
|
||||||
|
startsOn: new Date(),
|
||||||
|
expiresOn: expiryDate,
|
||||||
|
},
|
||||||
|
sharedKeyCredential,
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = new URL(blobClient.url);
|
||||||
|
url.search = `${sasToken}`;
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets container client
|
* Gets container client
|
||||||
* @param companyName
|
* @param companyName
|
||||||
|
|||||||
@ -1,10 +1,22 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { CardLicense, CardLicenseIssue, License, LicenseOrder } from './entity/license.entity';
|
import {
|
||||||
|
CardLicense,
|
||||||
|
CardLicenseIssue,
|
||||||
|
License,
|
||||||
|
LicenseOrder,
|
||||||
|
} from './entity/license.entity';
|
||||||
import { LicensesRepositoryService } from './licenses.repository.service';
|
import { LicensesRepositoryService } from './licenses.repository.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([LicenseOrder,License,CardLicense,CardLicenseIssue])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
LicenseOrder,
|
||||||
|
License,
|
||||||
|
CardLicense,
|
||||||
|
CardLicenseIssue,
|
||||||
|
]),
|
||||||
|
],
|
||||||
providers: [LicensesRepositoryService],
|
providers: [LicensesRepositoryService],
|
||||||
exports: [LicensesRepositoryService],
|
exports: [LicensesRepositoryService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
JoinColumn,
|
JoinColumn,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { TemplateFile } from '../../template_files/entity/template_file.entity';
|
||||||
|
|
||||||
@Entity({ name: 'tasks' })
|
@Entity({ name: 'tasks' })
|
||||||
export class Task {
|
export class Task {
|
||||||
@ -44,4 +45,6 @@ export class Task {
|
|||||||
@OneToOne(() => User, (user) => user.id)
|
@OneToOne(() => User, (user) => user.id)
|
||||||
@JoinColumn({ name: 'typist_user_id' })
|
@JoinColumn({ name: 'typist_user_id' })
|
||||||
typist_user?: User;
|
typist_user?: User;
|
||||||
|
@JoinColumn({ name: 'template_file_id' })
|
||||||
|
template_file?: TemplateFile;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,43 @@ import { Roles } from '../../common/types/role';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class TasksRepositoryService {
|
export class TasksRepositoryService {
|
||||||
constructor(private dataSource: DataSource) {}
|
constructor(private dataSource: DataSource) {}
|
||||||
|
/**
|
||||||
|
* 音声ファイルと紐づいたTaskを取得する
|
||||||
|
* @param audioFileId
|
||||||
|
* @param account_id
|
||||||
|
* @returns task and audio file
|
||||||
|
*/
|
||||||
|
async getTaskAndAudioFile(
|
||||||
|
audioFileId: number,
|
||||||
|
account_id: number,
|
||||||
|
isTypist: boolean
|
||||||
|
): Promise<Task> {
|
||||||
|
const status = isTypist
|
||||||
|
? [TASK_STATUS.UPLOADED, TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING]
|
||||||
|
: [TASK_STATUS.UPLOADED, TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING, TASK_STATUS.FINISHED, TASK_STATUS.BACKUP];
|
||||||
|
|
||||||
|
return await this.dataSource.transaction(async (entityManager) => {
|
||||||
|
const taskRepo = entityManager.getRepository(Task);
|
||||||
|
// 指定した音声ファイルIDに紐づくTaskの中でAuthorIDが一致するものを取得
|
||||||
|
const task = await taskRepo.findOne({
|
||||||
|
relations: {
|
||||||
|
file: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
audio_file_id: audioFileId,
|
||||||
|
account_id: account_id,
|
||||||
|
status: In(status),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!task) {
|
||||||
|
throw new TasksNotFoundError(
|
||||||
|
`task not found. audio_file_id:${audioFileId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return task;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 音声ファイルIDに紐づいたTaskを取得する
|
* 音声ファイルIDに紐づいたTaskを取得する
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ name: 'audio_files' })
|
||||||
|
export class TemplateFile {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
@Column()
|
||||||
|
account_id: number;
|
||||||
|
@Column()
|
||||||
|
url: string;
|
||||||
|
@Column()
|
||||||
|
file_name: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { TemplateFilesRepositoryService } from './template_files.repository.service';
|
||||||
|
import { TemplateFile } from './entity/template_file.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([TemplateFile])],
|
||||||
|
providers: [TemplateFilesRepositoryService],
|
||||||
|
exports: [TemplateFilesRepositoryService],
|
||||||
|
})
|
||||||
|
export class TemplateFilesRepositoryModule {}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TemplateFilesRepositoryService {
|
||||||
|
constructor(private dataSource: DataSource) {}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user