Merged PR 432: API実装(テンプレートファイルアップロード先取得API)

## 概要
[Task2654: API実装(テンプレートファイルアップロード先取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2654)

- テンプレートファイルアップロード先取得APIとテストを実装しました。
  - フォルダパス+SASトークンの形式で返却する。

## レビューポイント
- 返却URLは適切か
- BlobServiceでSASトークン発行を既存のメソッドとは別で用意したが構成は適切か
- UT用にBlobServiceのoverrideにメソッドを追加したが問題ないか。
- テストケースは適切か

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-09-25 05:45:29 +00:00
parent 3f4d4ec436
commit f994c23b51
5 changed files with 245 additions and 13 deletions

View File

@ -173,6 +173,16 @@ export const overrideBlobstorageService = <TService>(
accountId: number,
country: string,
) => Promise<void>;
containerExists?: (
context: Context,
accountId: number,
country: string,
) => Promise<boolean>;
publishTemplateUploadSas?: (
context: Context,
accountId: number,
country: string,
) => Promise<string>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -189,6 +199,18 @@ export const overrideBlobstorageService = <TService>(
writable: true,
});
}
if (overrides.containerExists) {
Object.defineProperty(obj, obj.containerExists.name, {
value: overrides.containerExists,
writable: true,
});
}
if (overrides.publishTemplateUploadSas) {
Object.defineProperty(obj, obj.publishTemplateUploadSas.name, {
value: overrides.publishTemplateUploadSas,
writable: true,
});
}
};
/**

View File

@ -290,13 +290,16 @@ export class FilesController {
@Req() req: Request,
): Promise<TemplateUploadLocationResponse> {
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(accessToken.userId);
const context = makeContext(userId);
console.log(context.trackingId);
const url = await this.filesService.publishTemplateFileUploadSas(
context,
userId,
);
return { url: '' };
return { url };
}
@ApiResponse({

View File

@ -10,7 +10,13 @@ import { DataSource } from 'typeorm';
import { createTask, makeTestingModuleWithBlob } from './test/utility';
import { FilesService } from './files.service';
import { makeContext } from '../../common/log';
import { makeTestSimpleAccount, makeTestUser } from '../../common/test/utility';
import {
makeTestAccount,
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
import { makeTestingModule } from '../../common/test/modules';
import { overrideBlobstorageService } from '../../common/test/overrides';
describe('音声ファイルアップロードURL取得', () => {
it('アップロードSASトークンが乗っているURLを返却する', async () => {
@ -779,6 +785,102 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
});
describe('publishTemplateFileUploadSas', () => {
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 module = await makeTestingModule(source);
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const context = makeContext(admin.external_id);
const baseUrl = `https://saodmsusdev.blob.core.windows.net/account-${account.id}/Templates`;
//SASトークンを返却する
overrideBlobstorageService(service, {
containerExists: async () => true,
publishTemplateUploadSas: async () => `${baseUrl}?sas-token`,
});
const url = await service.publishTemplateFileUploadSas(
context,
admin.external_id,
);
expect(url).toBe(`${baseUrl}?sas-token`);
});
it('blobストレージにコンテナが存在しない場合はエラーとなる', async () => {
const module = await makeTestingModule(source);
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
const context = makeContext(admin.external_id);
//Blobコンテナ存在チェックに失敗するようにする
overrideBlobstorageService(service, {
containerExists: async () => false,
publishTemplateUploadSas: async () => '',
});
try {
await service.publishTemplateFileUploadSas(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 () => {
const module = await makeTestingModule(source);
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
const context = makeContext(admin.external_id);
//BlobのSASトークン生成に失敗するようにする
overrideBlobstorageService(service, {
containerExists: async () => true,
publishTemplateUploadSas: async () => {
throw new Error('blob failed');
},
});
try {
await service.publishTemplateFileUploadSas(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();
}
}
});
});
const optionItemList = [
{
optionItemLabel: 'label_01',

View File

@ -535,4 +535,53 @@ export class FilesService {
);
}
}
/**
* URLを取得する
* @param context
* @param externalId
* @returns template file upload sas
*/
async publishTemplateFileUploadSas(
context: Context,
externalId: string,
): Promise<string> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name} | params: { externalId: ${externalId} };`,
);
try {
const {
account: { id: accountId, country },
} = await this.usersRepository.findUserByExternalId(externalId);
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
const isContainerExists = await this.blobStorageService.containerExists(
context,
accountId,
country,
);
if (!isContainerExists) {
throw new Error('container not found.');
}
// SASトークン発行
const url = await this.blobStorageService.publishTemplateUploadSas(
context,
accountId,
country,
);
return url;
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name}`,
);
}
}
}

View File

@ -25,6 +25,7 @@ export class BlobstorageService {
private readonly sharedKeyCredentialUS: StorageSharedKeyCredential;
private readonly sharedKeyCredentialAU: StorageSharedKeyCredential;
private readonly sharedKeyCredentialEU: StorageSharedKeyCredential;
private readonly sasTokenExpireHour: number;
constructor(private readonly configService: ConfigService) {
this.sharedKeyCredentialUS = new StorageSharedKeyCredential(
this.configService.get('STORAGE_ACCOUNT_NAME_US'),
@ -50,6 +51,14 @@ export class BlobstorageService {
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_EU'),
this.sharedKeyCredentialEU,
);
const expireTime = Number(
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
);
if (Number.isNaN(expireTime)) {
throw new Error(`STORAGE_TOKEN_EXPIRE_TIME is invalid value NaN`);
}
this.sasTokenExpireHour = expireTime;
}
/**
@ -206,10 +215,7 @@ export class BlobstorageService {
//SASの有効期限を設定
const expiryDate = new Date();
expiryDate.setHours(
expiryDate.getHours() +
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
);
expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour);
//SASの権限を設定。Pendingにしたものを再アップロードする運用をするため、上書き可能にする
const permissions = new ContainerSASPermissions();
@ -237,6 +243,59 @@ export class BlobstorageService {
return url.toString();
}
/**
* SASトークン付きのBlobStorageテンプレートファイルアップロードURLを生成し返却します
* @param accountId
* @param country
* @returns template upload sas
*/
async publishTemplateUploadSas(
context: Context,
accountId: number,
country: string,
): Promise<string> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.publishTemplateUploadSas.name}`,
);
try {
// コンテナ名を指定してClientを取得
const containerClient = this.getContainerClient(accountId, country);
// 国に対応したリージョンの接続情報を取得する
const sharedKeyCredential = this.getSharedKeyCredential(country);
//SASの有効期限を設定
const expiryDate = new Date();
expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour);
//SASの権限を設定。同名ファイルを再アップロードできる運用をするため、上書き可能にする
const permissions = new ContainerSASPermissions();
permissions.write = true;
//SASを発行
const sasToken = generateBlobSASQueryParameters(
{
containerName: containerClient.containerName,
permissions: permissions,
startsOn: new Date(),
expiresOn: expiryDate,
},
sharedKeyCredential,
);
const url = new URL('Templates', containerClient.url);
url.search = `${sasToken}`;
return url.toString();
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.publishTemplateUploadSas.name}`,
);
}
}
/**
* SASトークン付きのBlobStorageダウンロードURLを生成し返却します
* @param accountId
@ -274,10 +333,7 @@ export class BlobstorageService {
//SASの有効期限を設定
const expiryDate = new Date();
expiryDate.setHours(
expiryDate.getHours() +
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
);
expiryDate.setHours(expiryDate.getHours() + this.sasTokenExpireHour);
//SASの権限を設定ダウンロードのため読み取り許可
const permissions = new BlobSASPermissions();