Merged PR 573: 音声ファイルアップロードAPI修正

## 概要
[Task3069: 音声ファイルアップロードAPI修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3069)
[Task3070: 音声ファイルダウンロードAPI修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3070)
[Task3071: テンプレートファイルダウンロードAPI修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3071)
修正内容かぶるため、3本まとめてレビューお願いします。
- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 音声ファイルアップロードAPIを修正
 ・第五階層の場合のみチェックを追加
  ・アカウントがロックされている場合、エラー
  ・ユーザーにライセンスが未割当の場合、エラー
  ・ユーザーに紐づいたライセンスが有効期限切れの場合、エラー
- 音声ファイルダウンロード、テンプレートファイルダウンロードAPIを修正
 ・第五階層の場合のみチェックを追加
  ・ユーザーにライセンスが未割当の場合、エラー
  ・ユーザーに紐づいたライセンスが有効期限切れの場合、エラー
- 外部連携アプリ側の挙動の変化については考慮しない。
- ログ強化は別タスクで対応中。
- 影響範囲(他の機能にも影響があるか)
ファイル操作以外は影響なし。
旧式のユニットテストを修正。

## レビューポイント
- 音声ファイルアップロードのユニットテストを最新の状態にしたが、不足していないか。
~~- users.repositoryにユーザに紐づくライセンスが現在有効かどうかの判定を入れ込み、共通的に呼び出すようにしたが使いづらくないか(ライセンスが紐づいていない場合と有効期限切れの場合エラーとし、それ以外はtrueが帰る点について)~~

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2023-11-14 13:16:46 +00:00
parent 40162ef3af
commit eb3c7e55bd
13 changed files with 850 additions and 90 deletions

View File

@ -2077,6 +2077,14 @@
}
}
},
"400": {
"description": "不正なパラメータ",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {

View File

@ -39,6 +39,7 @@ export const ErrorCodes = [
'E010501', // アカウント不在エラー
'E010502', // アカウント情報変更不可エラー
'E010503', // 代行操作不許可エラー
'E010504', // アカウントロックエラー
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
'E010602', // タスク変更権限不足エラー
'E010603', // タスク不在エラー
@ -54,6 +55,7 @@ export const ErrorCodes = [
'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
'E010812', // ライセンス未割当エラー
'E010908', // タイピストグループ不在エラー
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー

View File

@ -28,6 +28,7 @@ export const errors: Errors = {
E010501: 'Account not Found Error.',
E010502: 'Account information cannot be changed Error.',
E010503: 'Delegation not allowed Error.',
E010504: 'Account is locked Error.',
E010601: 'Task is not Editable Error',
E010602: 'No task edit permissions Error',
E010603: 'Task not found Error.',
@ -43,6 +44,7 @@ export const errors: Errors = {
E010809: 'Already license order status changed Error',
E010810: 'Cancellation period expired error',
E010811: 'Already license allocated Error',
E010812: 'License not allocated Error',
E010908: 'Typist Group not exist Error',
E011001: 'This WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',

View File

@ -185,6 +185,11 @@ export const overrideBlobstorageService = <TService>(
accountId: number,
country: string,
) => Promise<boolean>;
publishUploadSas?: (
context: Context,
accountId: number,
country: string,
) => Promise<string>;
publishTemplateUploadSas?: (
context: Context,
accountId: number,
@ -212,6 +217,12 @@ export const overrideBlobstorageService = <TService>(
writable: true,
});
}
if (overrides.publishUploadSas) {
Object.defineProperty(obj, obj.publishUploadSas.name, {
value: overrides.publishUploadSas,
writable: true,
});
}
if (overrides.publishTemplateUploadSas) {
Object.defineProperty(obj, obj.publishTemplateUploadSas.name, {
value: overrides.publishTemplateUploadSas,

View File

@ -39,11 +39,15 @@ import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { LicensesService } from '../licenses/licenses.service';
@ApiTags('files')
@Controller('files')
export class FilesController {
constructor(private readonly filesService: FilesService) {}
constructor(
private readonly filesService: FilesService,
private readonly licensesService: LicensesService,
) {}
@ApiResponse({
status: HttpStatus.OK,
@ -140,6 +144,11 @@ export class FilesController {
type: AudioUploadLocationResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '不正なパラメータ',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',

View File

@ -1,13 +1,9 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
makeBlobstorageServiceMockValue,
makeDefaultTasksRepositoryMockValue,
makeDefaultUsersRepositoryMockValue,
makeFilesServiceMock,
} from './test/files.service.mock';
import { makeBlobstorageServiceMockValue } from './test/files.service.mock';
import { DataSource } from 'typeorm';
import {
createLicense,
createTask,
createUserGroupAndMember,
getTaskFromJobNumber,
@ -16,6 +12,7 @@ import {
import { FilesService } from './files.service';
import { makeContext } from '../../common/log';
import {
makeHierarchicalAccounts,
makeTestAccount,
makeTestSimpleAccount,
makeTestUser,
@ -37,89 +34,243 @@ import { TasksRepositoryService } from '../../repositories/tasks/tasks.repositor
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
import { getCheckoutPermissions, getTask } from '../tasks/test/utility';
import { DateWithZeroTime } from '../licenses/types/types';
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
describe('音声ファイルアップロードURL取得', () => {
it('アップロードSASトークンが乗っているURLを返却する', async () => {
const blobParam = makeBlobstorageServiceMockValue();
const userRepoParam = makeDefaultUsersRepositoryMockValue();
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
const service = await makeFilesServiceMock(
blobParam,
userRepoParam,
taskRepoParam,
);
expect(
await service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).toEqual('https://blob-storage?sas-token');
describe('publishUploadSas', () => {
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();
});
it('アカウント専用コンテナが無い場合でも、コンテナ作成しURLを返却する', async () => {
const blobParam = makeBlobstorageServiceMockValue();
const userRepoParam = makeDefaultUsersRepositoryMockValue();
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
blobParam.containerExists = false;
const service = await makeFilesServiceMock(
blobParam,
userRepoParam,
taskRepoParam,
);
expect(
await service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).toEqual('https://blob-storage?sas-token');
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ユーザー情報の取得に失敗した場合、例外エラーを返却する', async () => {
const blobParam = makeBlobstorageServiceMockValue();
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
it('音声アップロードSASトークンが乗っているURLを取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account: account } = await makeTestAccount(source, { tier: 5 });
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
// 本日の日付を作成
let today = new Date();
today.setDate(today.getDate());
today = new DateWithZeroTime(today);
// 有効期限内のライセンスを作成して紐づける
await createLicense(
source,
1,
today,
account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
const context = makeContext(externalId);
const baseUrl = `https://saodmsusdev.blob.core.windows.net/account-${account.id}/${userId}`;
const service = await makeFilesServiceMock(
blobParam,
{
findUserByExternalId: new Error(''),
//SASトークンを返却する
overrideBlobstorageService(service, {
containerExists: async () => true,
publishUploadSas: async () => `${baseUrl}?sas-token`,
});
const url = await service.publishUploadSas(context, externalId);
expect(url).toBe(`${baseUrl}?sas-token`);
});
it('blobストレージにコンテナが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第四階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 4 });
const context = makeContext(admin.external_id);
//Blobコンテナ存在チェックに失敗するようにする
overrideBlobstorageService(service, {
containerExists: async () => false,
publishUploadSas: async () => '',
});
try {
await service.publishUploadSas(context, admin.external_id);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
it('SASトークンの取得に失敗した場合はエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第四階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 4 });
const context = makeContext(admin.external_id);
//BlobのSASトークン生成に失敗するようにする
overrideBlobstorageService(service, {
containerExists: async () => true,
publishUploadSas: async () => {
throw new Error('blob failed');
},
taskRepoParam,
});
try {
await service.publishUploadSas(context, admin.external_id);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
it('アカウントがロックされている場合、エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5, locked: true });
const context = makeContext(admin.external_id);
try {
await service.publishUploadSas(context, admin.external_id);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010504'));
} else {
fail();
}
}
});
it('アップロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => {
if (!source) fail();
// 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない)
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishUploadSas = `${url}?sas-token`;
blobParam.fileExists = false;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
service.publishUploadSas(makeContext('trackingId'), externalId),
).rejects.toEqual(
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST),
);
});
it('コンテナ作成に失敗した場合、例外エラーを返却する', async () => {
const blobParam = makeBlobstorageServiceMockValue();
const taskRepoParam = makeDefaultTasksRepositoryMockValue();
const service = await makeFilesServiceMock(
blobParam,
{
findUserByExternalId: new Error(''),
},
taskRepoParam,
it('アップロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => {
if (!source) fail();
// 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
blobParam.publishUploadSas = new Error('Azure service down');
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
// 昨日の日付を作成
let yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday = new DateWithZeroTime(yesterday);
// 期限切れのライセンスを作成して紐づける
await createLicense(
source,
1,
yesterday,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishUploadSas = `${url}?sas-token`;
blobParam.fileExists = false;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
service.publishUploadSas(makeContext('trackingId'), externalId),
).rejects.toEqual(
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST),
);
});
});
@ -879,7 +1030,76 @@ describe('音声ファイルダウンロードURL取得', () => {
),
).toEqual(`${url}?sas-token`);
});
it('ダウンロードSASトークンが乗っているURLを取得できる第五階層の場合ライセンスのチェックを行う', async () => {
if (!source) fail();
// 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
// 本日の日付を作成
let today = new Date();
today.setDate(today.getDate());
today = new DateWithZeroTime(today);
// 有効期限内のライセンスを作成して紐づける
await createLicense(
source,
1,
today,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
const { audioFileId } = await createTask(
source,
tier5Accounts.account.id,
url,
'test.zip',
'InProgress',
undefined,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishDownloadSas = `${url}?sas-token`;
blobParam.fileExists = true;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
expect(
await service.publishAudioFileDownloadSas(
makeContext('trackingId'),
externalId,
audioFileId,
),
).toEqual(`${url}?sas-token`);
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
@ -1109,6 +1329,133 @@ describe('音声ファイルダウンロードURL取得', () => {
new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST),
);
});
it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => {
if (!source) fail();
// 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない)
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
const { audioFileId } = await createTask(
source,
tier5Accounts.account.id,
url,
'test.zip',
'InProgress',
undefined,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishDownloadSas = `${url}?sas-token`;
blobParam.fileExists = false;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
service.publishAudioFileDownloadSas(
makeContext('trackingId'),
externalId,
audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST),
);
});
it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => {
if (!source) fail();
// 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
// 昨日の日付を作成
let yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday = new DateWithZeroTime(yesterday);
// 期限切れのライセンスを作成して紐づける
await createLicense(
source,
1,
yesterday,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
const { audioFileId } = await createTask(
source,
tier5Accounts.account.id,
url,
'test.zip',
'InProgress',
undefined,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishDownloadSas = `${url}?sas-token`;
blobParam.fileExists = false;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
service.publishAudioFileDownloadSas(
makeContext('trackingId'),
externalId,
audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST),
);
});
});
describe('テンプレートファイルダウンロードURL取得', () => {
@ -1175,7 +1522,76 @@ describe('テンプレートファイルダウンロードURL取得', () => {
),
).toEqual(`${url}?sas-token`);
});
it('ダウンロードSASトークンが乗っているURLを取得できる第五階層の場合ライセンスのチェックを行う', async () => {
if (!source) fail();
// 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
// 本日の日付を作成
let yesterday = new Date();
yesterday.setDate(yesterday.getDate());
yesterday = new DateWithZeroTime(yesterday);
// 有効期限内のライセンスを作成して紐づける
await createLicense(
source,
1,
yesterday,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
const { audioFileId } = await createTask(
source,
tier5Accounts.account.id,
url,
'test.zip',
'InProgress',
undefined,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishDownloadSas = `${url}?sas-token`;
blobParam.fileExists = true;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
expect(
await service.publishTemplateFileDownloadSas(
makeContext('trackingId'),
externalId,
audioFileId,
),
).toEqual(`${url}?sas-token`);
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
@ -1394,6 +1810,133 @@ describe('テンプレートファイルダウンロードURL取得', () => {
new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST),
);
});
it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => {
if (!source) fail();
// 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない)
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
const { audioFileId } = await createTask(
source,
tier5Accounts.account.id,
url,
'test.zip',
'InProgress',
undefined,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishDownloadSas = `${url}?sas-token`;
blobParam.fileExists = false;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
service.publishTemplateFileDownloadSas(
makeContext('trackingId'),
externalId,
audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST),
);
});
it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => {
if (!source) fail();
// 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const {
external_id: externalId,
id: userId,
author_id: authorId,
} = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
external_id: 'author-user-external-id',
role: 'author',
author_id: 'AUTHOR_ID',
});
// 昨日の日付を作成
let yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday = new DateWithZeroTime(yesterday);
// 期限切れのライセンスを作成して紐づける
await createLicense(
source,
1,
yesterday,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
const { audioFileId } = await createTask(
source,
tier5Accounts.account.id,
url,
'test.zip',
'InProgress',
undefined,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
blobParam.publishDownloadSas = `${url}?sas-token`;
blobParam.fileExists = false;
const notificationParam = makeDefaultNotificationhubServiceMockValue();
const module = await makeTestingModuleWithBlobAndNotification(
source,
blobParam,
notificationParam,
);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
service.publishTemplateFileDownloadSas(
makeContext('trackingId'),
externalId,
audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST),
);
});
});
describe('publishTemplateFileUploadSas', () => {

View File

@ -7,6 +7,7 @@ import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types';
import {
OPTION_ITEM_NUM,
TASK_STATUS,
TIERS,
USER_ROLES,
} from '../../constants/index';
import { User } from '../../repositories/users/entity/user.entity';
@ -23,11 +24,19 @@ import {
} from '../../repositories/tasks/errors/types';
import { Context } from '../../common/log';
import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import {
AccountNotFoundError,
AccountLockedError,
} from '../../repositories/accounts/errors/types';
import { Task } from '../../repositories/tasks/entity/task.entity';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
import {
LicenseExpiredError,
LicenseNotAllocatedError,
} from '../../repositories/licenses/errors/types';
import { DateWithZeroTime } from '../licenses/types/types';
@Injectable()
export class FilesService {
@ -269,7 +278,20 @@ export class FilesService {
}
const accountId = user.account_id;
const country = user.account.country;
// 第五階層のみチェック
if (user.account.tier === TIERS.TIER5) {
// アカウントがロックされている場合、エラー
if (user.account.locked) {
throw new AccountLockedError('account is locked.');
}
// ライセンスの有効性をチェック
const { licenseError } = await this.checkLicenseValidityByUserId(
user.id,
);
if (licenseError) {
throw licenseError;
}
}
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
await this.blobStorageService.containerExists(
context,
@ -286,10 +308,28 @@ export class FilesService {
return url;
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
switch (e.constructor) {
case AccountLockedError:
throw new HttpException(
makeErrorResponse('E010504'),
HttpStatus.BAD_REQUEST,
);
case LicenseExpiredError:
throw new HttpException(
makeErrorResponse('E010805'),
HttpStatus.BAD_REQUEST,
);
case LicenseNotAllocatedError:
throw new HttpException(
makeErrorResponse('E010812'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishUploadSas.name}`,
@ -323,6 +363,16 @@ export class FilesService {
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
// 第五階層のみチェック
if (user.account.tier === TIERS.TIER5) {
// ライセンスの有効性をチェック
const { licenseError } = await this.checkLicenseValidityByUserId(
user.id,
);
if (licenseError) {
throw licenseError;
}
}
accountId = user.account.id;
userId = user.id;
country = user.account.country;
@ -330,14 +380,26 @@ export class FilesService {
authorId = user.author_id ?? undefined;
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishAudioFileDownloadSas.name}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
switch (e.constructor) {
case LicenseExpiredError:
throw new HttpException(
makeErrorResponse('E010805'),
HttpStatus.BAD_REQUEST,
);
case LicenseNotAllocatedError:
throw new HttpException(
makeErrorResponse('E010812'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
try {
@ -460,6 +522,16 @@ export class FilesService {
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
// 第五階層のみチェック
if (user.account.tier === TIERS.TIER5) {
// ライセンスの有効性をチェック
const { licenseError } = await this.checkLicenseValidityByUserId(
user.id,
);
if (licenseError) {
throw licenseError;
}
}
accountId = user.account_id;
userId = user.id;
country = user.account.country;
@ -470,10 +542,23 @@ export class FilesService {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishTemplateFileDownloadSas.name}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
switch (e.constructor) {
case LicenseExpiredError:
throw new HttpException(
makeErrorResponse('E010805'),
HttpStatus.BAD_REQUEST,
);
case LicenseNotAllocatedError:
throw new HttpException(
makeErrorResponse('E010812'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
try {
@ -562,6 +647,7 @@ export class FilesService {
makeErrorResponse('E010701'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
@ -679,4 +765,44 @@ export class FilesService {
);
}
}
/**
*
*
* @param userId
* @returns licenseError?
*/
// TODO: TASK3084で共通部品化する
private async checkLicenseValidityByUserId(
userId: number,
): Promise<{ licenseError?: Error }> {
try {
const allocatedLicense = await this.usersRepository.findLicenseByUserId(
userId,
);
if (!allocatedLicense) {
return {
licenseError: new LicenseNotAllocatedError(
'license is not allocated.',
),
};
} else {
const currentDate = new DateWithZeroTime();
if (
allocatedLicense.expiry_date &&
allocatedLicense.expiry_date < currentDate
) {
return {
licenseError: new LicenseExpiredError('license is expired.'),
};
}
}
return {}; // エラーがない場合は空のオブジェクトを返す
} catch (e) {
// リポジトリ層のエラーやその他の例外をハンドリング
return e;
}
}
}

View File

@ -19,6 +19,7 @@ export type BlobstorageServiceMockValue = {
export type UsersRepositoryMockValue = {
findUserByExternalId: User | Error;
isUserLicenseValid: boolean | Error;
};
export type TasksRepositoryMockValue = {
@ -91,13 +92,17 @@ export const makeBlobstorageServiceMock = (
};
export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
const { findUserByExternalId } = value;
const { findUserByExternalId, isUserLicenseValid } = value;
return {
findUserByExternalId:
findUserByExternalId instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(findUserByExternalId)
: jest.fn<Promise<User>, []>().mockResolvedValue(findUserByExternalId),
isUserLicenseValid:
isUserLicenseValid instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(isUserLicenseValid)
: jest.fn<Promise<boolean>, []>().mockResolvedValue(isUserLicenseValid),
};
};
@ -169,6 +174,7 @@ export const makeDefaultUsersRepositoryMockValue =
user: null,
},
},
isUserLicenseValid: true,
};
};

View File

@ -43,6 +43,7 @@ import {
} from '../../tasks/test/tasks.service.mock';
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
import { License } from '../../../repositories/licenses/entity/license.entity';
export const createTask = async (
datasource: DataSource,
@ -205,3 +206,33 @@ export const makeTestingModuleWithBlobAndNotification = async (
console.log(e);
}
};
export const createLicense = async (
datasource: DataSource,
licenseId: number,
expiry_date: Date | null,
accountId: number,
type: string,
status: string,
allocated_user_id: number | null,
order_id: number | null,
deleted_at: Date | null,
delete_order_id: number | null,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId,
expiry_date: expiry_date,
account_id: accountId,
type: type,
status: status,
allocated_user_id: allocated_user_id,
order_id: order_id,
deleted_at: deleted_at,
delete_order_id: delete_order_id,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
identifiers.pop() as License;
};

View File

@ -1,6 +1,5 @@
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { AccessToken } from '../../common/token';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
@ -13,6 +12,7 @@ import {
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import {
DateWithZeroTime,
GetAllocatableLicensesResponse,
IssueCardLicensesResponse,
} from './types/types';

View File

@ -4,3 +4,5 @@ export class AccountNotFoundError extends Error {}
export class DealerAccountNotFoundError extends Error {}
// 管理者ユーザ未存在エラー
export class AdminUserNotFoundError extends Error {}
// アカウントロックエラー
export class AccountLockedError extends Error {}

View File

@ -33,3 +33,6 @@ export class CancellationPeriodExpiredError extends Error {}
// ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
export class AlreadyLicenseAllocatedError extends Error {}
// ライセンス未割当エラー
export class LicenseNotAllocatedError extends Error {}

View File

@ -648,6 +648,23 @@ export class UsersRepositoryService {
return originAccount.delegation_permission;
});
}
/**
*
* @param userId ID
* @returns License
*/
async findLicenseByUserId(userId: number): Promise<License | null> {
const allocatedLicense = await this.dataSource
.getRepository(License)
.findOne({
where: {
allocated_user_id: userId,
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
},
});
return allocatedLicense;
}
/**
*