From f209c7359eec876b83e498bb02cbc60f352a1b6f Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 12 Apr 2024 01:36:49 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20865:=20IF=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=83=BB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task4049: IF実装・修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4049) - 音声ファイル名変更APIのIFを実装してリクエストパラメータのテストを実装しました。 - タスク一覧取得APIのレスポンスに生ファイル名を追加しました。 - OpenAPIの更新 ## レビューポイント - ファイル名変更APIのパスは適切でしょうか? - バリデータのチェックは適切でしょうか? ## UIの変更 - なし ## クエリの変更 - IFなのでなし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - 既存テストを実施して、タスク一覧についてはレスポンス期待値を修正。 - タスク一覧画面が正常に見えることを確認 --- dictation_server/src/api/odms/openapi.json | 69 ++++++++++++++++ .../features/files/files.controller.spec.ts | 81 +++++++++++++++++++ .../src/features/files/files.controller.ts | 72 +++++++++++++++++ .../src/features/files/types/types.ts | 16 ++++ .../src/features/tasks/tasks.service.spec.ts | 3 + .../src/features/tasks/types/convert.ts | 1 + .../src/features/tasks/types/types.ts | 2 + 7 files changed, 244 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 1d10bdb..77983dc 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -2903,6 +2903,58 @@ "security": [{ "bearer": [] }] } }, + "/files/rename": { + "post": { + "operationId": "fileRename", + "summary": "", + "description": "音声ファイルの表示ファイル名を変更します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/FileRenameRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/FileRenameResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, "/tasks": { "get": { "operationId": "getTasks", @@ -5283,6 +5335,18 @@ "required": ["name", "url"] }, "TemplateUploadFinishedReqponse": { "type": "object", "properties": {} }, + "FileRenameRequest": { + "type": "object", + "properties": { + "audioFileId": { + "type": "number", + "description": "ファイル名変更対象の音声ファイルID" + }, + "fileName": { "type": "string", "description": "変更するファイル名" } + }, + "required": ["audioFileId", "fileName"] + }, + "FileRenameResponse": { "type": "object", "properties": {} }, "Assignee": { "type": "object", "properties": { @@ -5322,6 +5386,10 @@ "description": "音声ファイルのBlob Storage上での保存場所(ファイル名含む)のURL" }, "fileName": { "type": "string", "description": "音声ファイル名" }, + "rawFileName": { + "type": "string", + "description": "生(Blob Storage上の)音声ファイル名" + }, "audioDuration": { "type": "string", "description": "音声ファイルの録音時間(ミリ秒の整数値)" @@ -5382,6 +5450,7 @@ "optionItemList", "url", "fileName", + "rawFileName", "audioDuration", "audioCreatedDate", "audioFinishedDate", diff --git a/dictation_server/src/features/files/files.controller.spec.ts b/dictation_server/src/features/files/files.controller.spec.ts index 3986b38..39798dc 100644 --- a/dictation_server/src/features/files/files.controller.spec.ts +++ b/dictation_server/src/features/files/files.controller.spec.ts @@ -2,6 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FilesController } from './files.controller'; import { FilesService } from './files.service'; import { ConfigModule } from '@nestjs/config'; +import { FileRenameRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('FilesController', () => { let controller: FilesController; @@ -28,3 +31,81 @@ describe('FilesController', () => { expect(controller).toBeDefined(); }); }); + +describe('valdation FileRenameRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('音声ファイルIDが指定されていない場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイルIDが0の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 0; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイルIDが文字列の場合、リクエストが失敗する', async () => { + class InvalidFileRenameRequest { + audioFileId: string; + fileName: string; + } + + const request = new InvalidFileRenameRequest(); + request.audioFileId = 'invalid'; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイル名が空文字の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = ''; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイル名が50文字の場合、リクエストに成功する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('音声ファイル名が51文字の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5A'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); +}); diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index f8b4328..27906a5 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -27,6 +27,8 @@ import { AudioUploadFinishedResponse, AudioUploadLocationRequest, AudioUploadLocationResponse, + FileRenameRequest, + FileRenameResponse, TemplateDownloadLocationRequest, TemplateDownloadLocationResponse, TemplateUploadFinishedReqponse, @@ -533,4 +535,74 @@ export class FilesController { await this.filesService.templateUploadFinished(context, userId, url, name); return {}; } + + @ApiResponse({ + status: HttpStatus.OK, + type: FileRenameResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'fileRename', + description: '音声ファイルの表示ファイル名を変更します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @Post('rename') + async fileRename( + @Req() req: Request, + @Body() body: FileRenameRequest, + ): Promise { + const { audioFileId, fileName } = body; + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + // TODO: ファイル名変更処理を実装する + return {}; + } } diff --git a/dictation_server/src/features/files/types/types.ts b/dictation_server/src/features/files/types/types.ts index 3545a20..2199a75 100644 --- a/dictation_server/src/features/files/types/types.ts +++ b/dictation_server/src/features/files/types/types.ts @@ -8,6 +8,7 @@ import { IsInt, IsNotEmpty, IsNumberString, + IsString, MaxLength, Min, MinLength, @@ -141,3 +142,18 @@ export class TemplateUploadFinishedRequest { } export class TemplateUploadFinishedReqponse {} + +export class FileRenameRequest { + @ApiProperty({ description: 'ファイル名変更対象の音声ファイルID' }) + @Type(() => Number) + @Min(1) + @IsInt() + audioFileId: number; + @ApiProperty({ description: '変更するファイル名' }) + @IsString() + @MaxLength(50) + @MinLength(1) + fileName: string; +} + +export class FileRenameResponse {} diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 5f85918..0799356 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -104,6 +104,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', @@ -365,6 +366,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', @@ -500,6 +502,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', diff --git a/dictation_server/src/features/tasks/types/convert.ts b/dictation_server/src/features/tasks/types/convert.ts index 2ce6d99..c7fb43e 100644 --- a/dictation_server/src/features/tasks/types/convert.ts +++ b/dictation_server/src/features/tasks/types/convert.ts @@ -66,6 +66,7 @@ const createTask = ( audioFormat: file.audio_format, comment: file.comment ?? '', fileName: file.file_name, + rawFileName: file.raw_file_name, fileSize: file.file_size, isEncrypted: file.is_encrypted, url: file.url, diff --git a/dictation_server/src/features/tasks/types/types.ts b/dictation_server/src/features/tasks/types/types.ts index 33ba3d1..34d4252 100644 --- a/dictation_server/src/features/tasks/types/types.ts +++ b/dictation_server/src/features/tasks/types/types.ts @@ -110,6 +110,8 @@ export class Task { url: string; @ApiProperty({ description: '音声ファイル名' }) fileName: string; + @ApiProperty({ description: '生(Blob Storage上の)音声ファイル名' }) + rawFileName: string; @ApiProperty({ description: '音声ファイルの録音時間(ミリ秒の整数値)', })