Merged PR 493: API作成(アカウント情報取得(未認証時最小アクセス)API)

## 概要
[Task2807: API作成(アカウント情報取得(未認証時最小アクセス)API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2807)

- 未ログインユーザーについて、IDトークンを受け取ってユーザの所属するアカウントの階層情報を返却するAPIを実装しました。

## レビューポイント
- ContorollerでIDトークンをデコードしているが問題ないか?
  - ※ログインAPIを参考にしています。
- テストケースは適切か

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-10-13 05:33:02 +00:00
parent 685a8f6c3e
commit 69ff6f3432
5 changed files with 208 additions and 7 deletions

View File

@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AccountsController } from './accounts.controller';
import { AccountsService } from './accounts.service';
import { ConfigModule } from '@nestjs/config';
import { AuthService } from '../auth/auth.service';
describe('AccountsController', () => {
let controller: AccountsController;
const mockAccountService = {};
const mockAuthService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -16,10 +18,12 @@ describe('AccountsController', () => {
}),
],
controllers: [AccountsController],
providers: [AccountsService],
providers: [AccountsService, AuthService],
})
.overrideProvider(AccountsService)
.useValue(mockAccountService)
.overrideProvider(AuthService)
.useValue(mockAuthService)
.compile();
controller = module.get<AccountsController>(AccountsController);

View File

@ -8,6 +8,7 @@ import {
UseGuards,
Param,
Query,
HttpException,
} from '@nestjs/common';
import {
ApiOperation,
@ -74,12 +75,15 @@ import { AccessToken } from '../../common/token';
import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
import { v4 as uuidv4 } from 'uuid';
import { AuthService } from '../auth/auth.service';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('accounts')
@Controller('accounts')
export class AccountsController {
constructor(
private readonly accountService: AccountsService, //private readonly cryptoService: CryptoService,
private readonly authService: AuthService,
) {}
@Post()
@ -1116,11 +1120,22 @@ export class AccountsController {
async getAccountInfoMinimalAccess(
@Body() body: GetAccountInfoMinimalAccessRequest,
): Promise<GetAccountInfoMinimalAccessResponse> {
const context = makeContext(uuidv4());
// IDトークンの検証
const idToken = await this.authService.getVerifiedIdToken(body.idToken);
const isVerified = await this.authService.isVerifiedUser(idToken);
if (!isVerified) {
throw new HttpException(
makeErrorResponse('E010201'),
HttpStatus.BAD_REQUEST,
);
}
// TODO 仮実装。API実装タスクで本実装する。
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
// await this.accountService.getAccountInfoMinimalAccess(context, idToken);
return;
const context = makeContext(idToken.sub);
const tier = await this.accountService.getAccountInfoMinimalAccess(
context,
idToken.sub,
);
return { tier };
}
}

View File

@ -9,6 +9,7 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktypes.repository.module';
import { AuthService } from '../auth/auth.service';
@Module({
imports: [
@ -22,6 +23,6 @@ import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktype
BlobstorageModule,
],
controllers: [AccountsController],
providers: [AccountsService],
providers: [AccountsService, AuthService],
})
export class AccountsModule {}

View File

@ -5595,3 +5595,127 @@ describe('deleteAccountAndData', () => {
expect(userRecord).toBe(null);
});
});
describe('getAccountInfoMinimalAccess', () => {
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('IDトークンのsub情報からアカウントの階層情報を取得できること(第五階層)', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
tier: 5,
});
const context = makeContext(admin.external_id);
// 作成したデータを確認
{
const tier5Account = await getAccount(source, account.id);
expect(tier5Account.tier).toBe(5);
}
const tier = await service.getAccountInfoMinimalAccess(
context,
admin.external_id,
);
//実行結果を確認
expect(tier).toBe(5);
});
it('IDトークンのSub情報からアカウントの階層情報を取得できること第四階層', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第四階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(admin.external_id);
// 作成したデータを確認
{
const tier5Account = await getAccount(source, account.id);
expect(tier5Account.tier).toBe(4);
}
const tier = await service.getAccountInfoMinimalAccess(
context,
admin.external_id,
);
//実行結果を確認
expect(tier).toBe(4);
});
it('対象のユーザーが存在しない場合、400エラーとなること', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第四階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(admin.external_id);
// 作成したデータを確認
{
const tier5Account = await getAccount(source, account.id);
expect(tier5Account.tier).toBe(4);
}
try {
await service.getAccountInfoMinimalAccess(context, 'fail_external_id');
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010204'));
} else {
fail();
}
}
});
it('DBアクセスに失敗した場合、500エラーとなること', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第四階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(admin.external_id);
// 作成したデータを確認
{
const tier5Account = await getAccount(source, account.id);
expect(tier5Account.tier).toBe(4);
}
//DBアクセスに失敗するようにする
const usersRepositoryService = module.get<UsersRepositoryService>(
UsersRepositoryService,
);
usersRepositoryService.findUserByExternalId = jest
.fn()
.mockRejectedValue('DB failed');
try {
await service.getAccountInfoMinimalAccess(context, admin.external_id);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});

View File

@ -1843,4 +1843,61 @@ export class AccountsService {
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
);
}
/**
* IDトークンのsubからアカウントの階層情報を取得します
* @param context
* @param externalId
* @returns account info minimal access
*/
async getAccountInfoMinimalAccess(
context: Context,
externalId: string,
): Promise<number> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name} | params: { externalId: ${externalId} };`,
);
try {
const { account } = await this.usersRepository.findUserByExternalId(
externalId,
);
if (!account) {
throw new AccountNotFoundError(
`Account not found. externalId: ${externalId}`,
);
}
return account.tier;
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name}`,
);
}
}
}