Merged PR 495: API作成(バージョン更新API)

## 概要
[Task2804: API作成(バージョン更新API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2804)

同意済み利用規約バージョン更新APIを実装しました。

## レビューポイント
なし

## UIの変更
なし

## 動作確認状況
UT,ローカルで動作確認済み

## 補足
なし
This commit is contained in:
oura.a 2023-10-16 01:31:30 +00:00
parent 162470838d
commit 273ba588ce
7 changed files with 222 additions and 8 deletions

View File

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

View File

@ -41,6 +41,7 @@ import {
UpdateAcceptedVersionResponse,
} from './types/types';
import { UsersService } from './users.service';
import { AuthService } from '../auth/auth.service';
import jwt from 'jsonwebtoken';
import { AuthGuard } from '../../common/guards/auth/authguards';
import {
@ -56,7 +57,10 @@ import { v4 as uuidv4 } from 'uuid';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
constructor(
private readonly usersService: UsersService,
private readonly authService: AuthService,
) {}
@ApiResponse({
status: HttpStatus.OK,
@ -495,11 +499,24 @@ export class UsersController {
async updateAcceptedVersion(
@Body() body: UpdateAcceptedVersionRequest,
): Promise<UpdateAcceptedVersionResponse> {
const context = makeContext(uuidv4());
const { idToken, acceptedEULAVersion, acceptedDPAVersion } = body;
// TODO 仮実装。API実装タスクで本実装する。
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
// await this.usersService.updateAcceptedVersion(context, idToken);
const verifiedIdToken = await this.authService.getVerifiedIdToken(idToken);
const context = makeContext(verifiedIdToken.sub);
const isVerified = await this.authService.isVerifiedUser(verifiedIdToken);
if (!isVerified) {
throw new HttpException(
makeErrorResponse('E010201'),
HttpStatus.BAD_REQUEST,
);
}
await this.usersService.updateAcceptedVersion(
context,
verifiedIdToken.sub,
acceptedEULAVersion,
acceptedDPAVersion,
);
return {};
}
}

View File

@ -7,6 +7,7 @@ import { UsersRepositoryModule } from '../../repositories/users/users.repository
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { AuthService } from '../auth/auth.service';
@Module({
imports: [
@ -18,6 +19,6 @@ import { UsersService } from './users.service';
ConfigModule,
],
controllers: [UsersController],
providers: [UsersService],
providers: [UsersService, AuthService],
})
export class UsersModule {}

View File

@ -43,6 +43,7 @@ import {
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
import { v4 as uuidv4 } from 'uuid';
describe('UsersService.confirmUser', () => {
let source: DataSource = null;
@ -2480,3 +2481,91 @@ describe('UsersService.updateUser', () => {
);
});
});
describe('UsersService.updateAcceptedVersion', () => {
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('同意済み利用規約バージョンを更新できる(第五)', async () => {
const module = await makeTestingModule(source);
const { admin } = await makeTestAccount(source, {
tier: 5,
});
const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService);
await service.updateAcceptedVersion(context, admin.external_id, 'v2.0');
const user = await getUser(source, admin.id);
expect(user.accepted_eula_version).toBe('v2.0');
});
it('同意済み利用規約バージョンを更新できる(第一~第四)', async () => {
const module = await makeTestingModule(source);
const { admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService);
await service.updateAcceptedVersion(
context,
admin.external_id,
'v2.0',
'v3.0',
);
const user = await getUser(source, admin.id);
expect(user.accepted_eula_version).toBe('v2.0');
expect(user.accepted_dpa_version).toBe('v3.0');
});
it('パラメータが不在のときエラーとなる(第五)', async () => {
const module = await makeTestingModule(source);
const { admin } = await makeTestAccount(source, {
tier: 5,
});
const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService);
await expect(
service.updateAcceptedVersion(context, admin.external_id, undefined),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST),
);
});
it('パラメータが不在のときエラーとなる(第一~第四)', async () => {
const module = await makeTestingModule(source);
const { admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService);
await expect(
service.updateAcceptedVersion(
context,
admin.external_id,
'v2.0',
undefined,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST),
);
});
});

View File

@ -4,7 +4,7 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { isVerifyError, verify } from '../../common/jwt';
import { getPublicKey } from '../../common/jwt/jwt';
import { makePassword } from '../../common/password/password';
import { AccessToken } from '../../common/token';
import { AccessToken, IDToken } from '../../common/token';
import {
SortDirection,
TaskListSortableAttribute,
@ -30,6 +30,7 @@ import {
EmailAlreadyVerifiedError,
EncryptionPasswordNeedError,
InvalidRoleChangeError,
UpdateTermsVersionNotSetError,
UserNotFoundError,
} from '../../repositories/users/errors/types';
import {
@ -967,4 +968,58 @@ export class UsersService {
);
}
}
/**
*
* @param context
* @param idToken
* @param eulaVersion
* @param dpaVersion
*/
async updateAcceptedVersion(
context: Context,
externalId: string,
eulaVersion: string,
dpaVersion?: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateAcceptedVersion.name} | params: { ` +
`externalId: ${externalId}, ` +
`eulaVersion: ${eulaVersion}, ` +
`dpaVersion: ${dpaVersion}, };`,
);
try {
await this.usersRepository.updateAcceptedTermsVersion(
externalId,
eulaVersion,
dpaVersion,
);
} 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 UpdateTermsVersionNotSetError:
throw new HttpException(
makeErrorResponse('E010001'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.updateAcceptedVersion.name}`,
);
}
}
}

View File

@ -10,3 +10,5 @@ export class InvalidRoleChangeError extends Error {}
export class EncryptionPasswordNeedError extends Error {}
// 利用規約バージョン情報不在エラー
export class TermInfoNotFoundError extends Error {}
// 利用規約バージョンパラメータ不在エラー
export class UpdateTermsVersionNotSetError extends Error {}

View File

@ -13,6 +13,7 @@ import {
InvalidRoleChangeError,
EncryptionPasswordNeedError,
TermInfoNotFoundError,
UpdateTermsVersionNotSetError,
} from './errors/types';
import {
LICENSE_ALLOCATED_STATUS,
@ -475,4 +476,49 @@ export class UsersRepositoryService {
};
});
}
/**
*
* @param externalId
* @param eulaVersion
* @param dpaVersion
* @returns update
*/
async updateAcceptedTermsVersion(
externalId: string,
eulaVersion: string,
dpaVersion: string | undefined,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const userRepo = entityManager.getRepository(User);
const user = await userRepo.findOne({
where: {
external_id: externalId,
},
relations: {
account: true,
},
});
if (!user) {
throw new UserNotFoundError(
`User not found. externalId: ${externalId}`,
);
}
// パラメータが不在の場合はエラーを返却
if (!eulaVersion) {
throw new UpdateTermsVersionNotSetError(`EULA version param not set.`);
}
if (user.account.tier !== TIERS.TIER5 && !dpaVersion) {
throw new UpdateTermsVersionNotSetError(
`DPA version param not set. User's tier: ${user.account.tier}`,
);
}
user.accepted_eula_version = eulaVersion;
user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version;
await userRepo.update({ id: user.id }, user);
});
}
}