From f386a8f7e0a29fec6fd2bca42d95d9680ee9192e Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Mon, 11 Mar 2024 01:29:55 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20817:=20API=20IF=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E8=A6=AA=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E5=A4=89=E6=9B=B4API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3852: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3852) - 親アカウント変更APIのIFを実装し、OpenAPIの生成もしました。 - 影響範囲(他の機能にも影響があるか) - なし ## レビューポイント - controllerのメソッド名にほか良い案ないか? - validationに過不足や間違いないか? - ~~controllerのテストは正常系ひとつだけ追加しているが、他にあったほうがいいものあるか?~~ - ~~個人的には、テスト追加してもnpmライブラリのvalidatorのテストになるだけな気がするため不要では?と思っています。~~ - 「npmライブラリのvalidatorを正しいパラメータで正しく利用しているか」が目的であるとの認識を得たため異常系も追加しました。 ## 動作確認状況 - apigenを実行してOpenAPI生成できることを確認、controllerテスト通ることを確認。 - 行った修正がデグレを発生させていないことを確認できるか - 新規APIのため無し --- dictation_server/src/api/odms/openapi.json | 70 ++++++++++++++++ .../accounts/accounts.controller.spec.ts | 38 +++++++++ .../features/accounts/accounts.controller.ts | 82 +++++++++++++++++++ .../src/features/accounts/types/types.ts | 23 ++++++ 4 files changed, 213 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index c279e1f..c1b24bf 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1664,6 +1664,59 @@ "security": [{ "bearer": [] }] } }, + "/accounts/parent/switch": { + "post": { + "operationId": "switchParent", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SwitchParentRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchParentResponse" + } + } + } + }, + "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": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/users/confirm": { "post": { "operationId": "confirmUser", @@ -4541,6 +4594,23 @@ "required": ["accountId", "restricted"] }, "UpdateRestrictionStatusResponse": { "type": "object", "properties": {} }, + "SwitchParentRequest": { + "type": "object", + "properties": { + "to": { + "type": "number", + "description": "切り替え先の親アカウントID" + }, + "children": { + "minItems": 1, + "description": "親を変更したいアカウントIDのリスト", + "type": "array", + "items": { "type": "integer" } + } + }, + "required": ["to", "children"] + }, + "SwitchParentResponse": { "type": "object", "properties": {} }, "ConfirmRequest": { "type": "object", "properties": { "token": { "type": "string" } }, diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index f69ece1..0ccef3d 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -3,6 +3,9 @@ import { AccountsController } from './accounts.controller'; import { AccountsService } from './accounts.service'; import { ConfigModule } from '@nestjs/config'; import { AuthService } from '../auth/auth.service'; +import { SwitchParentRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('AccountsController', () => { let controller: AccountsController; @@ -32,4 +35,39 @@ describe('AccountsController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation switchParentRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = [2]; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('子アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = []; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('子アカウントが重複指定されている場合、リクエストが失敗する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = [2, 2]; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); }); diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index dceff54..3cb553a 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -77,6 +77,8 @@ import { UpdateFileDeleteSettingResponse, UpdateRestrictionStatusRequest, UpdateRestrictionStatusResponse, + SwitchParentRequest, + SwitchParentResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -2319,4 +2321,84 @@ export class AccountsController { return {}; } + + @ApiResponse({ + status: HttpStatus.OK, + type: SwitchParentResponse, + 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: 'switchParent' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2], + }), + ) + @Post('parent/switch') + async switchParent( + @Req() req: Request, + @Body() body: SwitchParentRequest, + ): Promise { + 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:service層を呼び出す。本実装時に以下は削除する。 + const { to, children } = body; + this.logger.log( + `[${context.getTrackingId()}] to : ${to}, children : ${children.join( + ', ', + )}`, + ); + + return {}; + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 22da103..110d25d 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -369,6 +369,27 @@ export class UpdateRestrictionStatusRequest { restricted: boolean; } +export class SwitchParentRequest { + @ApiProperty({ description: '切り替え先の親アカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + to: number; + + @ApiProperty({ + minItems: 1, + isArray: true, + type: 'integer', + description: '親を変更したいアカウントIDのリスト', + }) + @ArrayMinSize(1) + @IsArray() + @IsInt({ each: true }) + @Min(1, { each: true }) + @IsUnique() + children: number[]; +} + // ============================== // RESPONSE // ============================== @@ -686,6 +707,8 @@ export class UpdateFileDeleteSettingResponse {} export class UpdateRestrictionStatusResponse {} +export class SwitchParentResponse {} + // ============================== // Request/Response外の型 // TODO: Request/Response/その他の型を別ファイルに分ける