Merged PR 906: ユーザー認証API修正

## 概要
[Task4182: ユーザー認証API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4182)

- 認証済みチェックをパスワード変更より先に行うように修正
- パスワード変更に失敗したら、認証済みフラグをfalseにするリカバリ処理追加
  - リカバリに失敗したら手動復旧ログを出力
- メール送信に失敗したらエラーを返すように修正
  - メール送信に失敗したらリカバリ処理を行うように修正
    - リカバリに失敗したら手動復旧ログを出力
- テスト修正
  - リカバリ処理を考慮したケースを追加

## レビューポイント
- リカバリ処理の記述
- メール送信でエラーが起きたときにエラーを握りつぶさないようにしたが問題ないか
  - メール送信で失敗したときにエラーを握りつぶすと、ユーザーは届かないメールを待つしかなくなる
    - 失敗を伝えて、リカバリをしてあげると再実行してもらうことができる。

## クエリの変更
- クエリの変更はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存のテストケースをDBを使うテストに置き換え
    - 結果は変えずに通ることを確認
  - テストケースを追加し、新たな観点でテストを作成

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2024-05-30 00:18:59 +00:00
parent 9736d23653
commit 0e68f26c57
5 changed files with 682 additions and 225 deletions

View File

@ -15,11 +15,8 @@ const UserVerifyPage: React.FC = (): JSX.Element => {
const jwt = query.get("verify") ?? "";
useEffect(() => {
if (!jwt) {
navigate("/mail-confirm/failed");
}
dispatch(userVerifyAsync({ jwt }));
}, [navigate, dispatch, jwt]);
}, [dispatch, jwt]);
const verifyState = useSelector(VerifyStateSelector);

View File

@ -35,6 +35,11 @@ export const overrideAdB2cService = <TService>(
externalIds: string[],
) => Promise<AdB2cUser[]>;
getUser?: (context: Context, externalId: string) => Promise<AdB2cUser>;
changePassword?: (
context: Context,
externalId: string,
password: string,
) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -69,6 +74,12 @@ export const overrideAdB2cService = <TService>(
writable: true,
});
}
if (overrides.changePassword) {
Object.defineProperty(obj, obj.changePassword.name, {
value: overrides.changePassword,
writable: true,
});
}
};
/**
@ -122,6 +133,8 @@ export const overrideUsersRepositoryService = <TService>(
overrides: {
createNormalUser?: (user: newUser) => Promise<User>;
deleteNormalUser?: (userId: number) => Promise<void>;
updateUserVerified?: (context: Context, userId: number) => Promise<void>;
updateUserUnverified?: (context: Context, userId: number) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -138,6 +151,18 @@ export const overrideUsersRepositoryService = <TService>(
writable: true,
});
}
if (overrides.updateUserVerified) {
Object.defineProperty(obj, obj.updateUserVerified.name, {
value: overrides.updateUserVerified,
writable: true,
});
}
if (overrides.updateUserUnverified) {
Object.defineProperty(obj, obj.updateUserUnverified.name, {
value: overrides.updateUserUnverified,
writable: true,
});
}
};
/**

View File

@ -8,7 +8,6 @@ import {
makeDefaultUsersRepositoryMockValue,
makeUsersServiceMock,
} from './test/users.service.mock';
import { EmailAlreadyVerifiedError } from '../../repositories/users/errors/types';
import {
createLicense,
createUserGroup,
@ -22,6 +21,7 @@ import {
LICENSE_ALLOCATED_STATUS,
LICENSE_EXPIRATION_THRESHOLD_DAYS,
LICENSE_TYPE,
MANUAL_RECOVERY_REQUIRED,
TASK_STATUS,
USER_AUDIO_FORMAT,
USER_LICENSE_EXPIRY_STATUS,
@ -59,6 +59,7 @@ import { createTask } from '../files/test/utility';
import { createCheckoutPermissions } from '../tasks/test/utility';
import { MultipleImportErrors } from './types/types';
import { TestLogger } from '../../common/test/logger';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
describe('UsersService.confirmUser', () => {
let source: DataSource | null = null;
@ -254,216 +255,564 @@ describe('UsersService.confirmUser', () => {
});
describe('UsersService.confirmUserAndInitPassword', () => {
let source: DataSource | null = null;
beforeAll(async () => {
if (source == null) {
source = await (async () => {
const s = new DataSource({
type: 'mysql',
host: 'test_mysql_db',
port: 3306,
username: 'user',
password: 'password',
database: 'odms',
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
logger: new TestLogger('none'),
logging: true,
});
return await s.initialize();
})();
}
});
beforeEach(async () => {
if (source) {
await truncateAllTable(source);
}
});
afterAll(async () => {
await source?.destroy();
source = null;
});
it('ユーザーが発行されたパスワードでログインできるようにする', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserById = {
id: 1,
external_id: 'TEST9999',
account_id: 1,
role: 'None',
accepted_eula_version: 'string',
accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
created_at: new Date(),
updated_by: 'string;',
updated_at: new Date(),
auto_renew: true,
notification: true,
encryption: false,
prompt: false,
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
};
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendGridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adminExternalId = 'ADMIN0001';
const { account } = await makeTestAccount(
source,
{},
{ external_id: adminExternalId },
);
const { id: accountId } = account;
// ユーザー作成
await makeTestUser(source, {
account_id: accountId,
external_id: 'externalId_user1',
role: USER_ROLES.NONE,
author_id: undefined,
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
email_verified: false,
});
overrideAdB2cService(service, {
changePassword: jest.fn(),
getUser: async () => {
return {
id: adminExternalId,
displayName: 'admin',
};
},
});
let _subject: string = '';
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
_subject = subject;
},
});
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
expect(
await service.confirmUserAndInitPassword(
makeContext('trackingId', 'requestId'),
token,
),
).toEqual(undefined);
);
expect(_subject).toBe('Temporary password [U-113]');
const user = await getUserFromExternalId(source, 'externalId_user1');
expect(user?.email_verified).toBe(true);
});
it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const sendgridService = module.get<SendGridService>(SendGridService);
const adB2cService = module.get<AdB2cService>(AdB2cService);
const adminExternalId = 'ADMIN0001';
const { account } = await makeTestAccount(
source,
{},
{ external_id: adminExternalId },
);
const { id: accountId } = account;
// ユーザー作成
await makeTestUser(source, {
account_id: accountId,
external_id: 'externalId_user1',
role: USER_ROLES.NONE,
author_id: undefined,
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
email_verified: false,
});
it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserById = {
id: 1,
external_id: 'TEST9999',
account_id: 1,
role: 'None',
accepted_eula_version: 'string',
accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
created_at: new Date(),
updated_by: 'string;',
updated_at: new Date(),
auto_renew: true,
notification: true,
encryption: false,
prompt: false,
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
overrideAdB2cService(service, {
changePassword: jest.fn(),
getUser: async () => {
return {
id: adminExternalId,
displayName: 'admin',
};
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendGridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
},
});
overrideSendgridService(service, {
sendMail: jest.fn(),
});
const token = 'invalid.id.token';
await expect(
service.confirmUserAndInitPassword(
try {
await service.confirmUserAndInitPassword(
makeContext('trackingId', 'requestId'),
token,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST),
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E000101'));
} else {
fail();
}
}
const user = await getUserFromExternalId(source, 'externalId_user1');
// ユーザーが認証されていないことを確認
expect(user?.email_verified).toBe(false);
// メールが送信されていないことを確認
expect(sendgridService.sendMail).toBeCalledTimes(0);
// パスワードが変更されていないことを確認
expect(adB2cService.changePassword).toBeCalledTimes(0);
});
it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。(メール認証API)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserById = {
id: 1,
external_id: 'TEST9999',
account_id: 1,
role: 'None',
accepted_eula_version: 'string',
accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string',
email_verified: true,
created_by: 'string;',
created_at: new Date(),
updated_by: 'string;',
updated_at: new Date(),
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const sendgridService = module.get<SendGridService>(SendGridService);
const adB2cService = module.get<AdB2cService>(AdB2cService);
const adminExternalId = 'ADMIN0001';
const { account } = await makeTestAccount(
source,
{},
{ external_id: adminExternalId },
);
const { id: accountId } = account;
// ユーザー作成
await makeTestUser(source, {
account_id: accountId,
external_id: 'externalId_user1',
role: USER_ROLES.NONE,
author_id: undefined,
auto_renew: true,
notification: true,
encryption: false,
encryption_password: undefined,
prompt: false,
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
};
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendGridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
usersRepositoryMockValue.updateUserVerified = new EmailAlreadyVerifiedError(
`Email already verified user`,
);
email_verified: true, // emailを認証済みにする
});
overrideAdB2cService(service, {
changePassword: jest.fn(),
getUser: async () => {
return {
id: adminExternalId,
displayName: 'admin',
};
},
});
overrideSendgridService(service, {
sendMail: jest.fn(),
});
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendGridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await expect(
service.confirmUserAndInitPassword(
try {
await service.confirmUserAndInitPassword(
makeContext('trackingId', 'requestId'),
token,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST),
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010202'));
} else {
fail();
}
}
const user = await getUserFromExternalId(source, 'externalId_user1');
// ユーザーが認証されていることを確認
expect(user?.email_verified).toBe(true);
// メールが送信されていないことを確認
expect(sendgridService.sendMail).toBeCalledTimes(0);
// パスワードが変更されていないことを確認
expect(adB2cService.changePassword).toBeCalledTimes(0);
});
it('ADB2Cユーザーのパスワード更新に失敗した場合、リカバリ処理を行い、メールを未認証のままにする。(メール認証API)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const sendgridService = module.get<SendGridService>(SendGridService);
const adminExternalId = 'ADMIN0001';
const { account } = await makeTestAccount(
source,
{},
{ external_id: adminExternalId },
);
const { id: accountId } = account;
// ユーザー作成
await makeTestUser(source, {
account_id: accountId,
external_id: 'externalId_user1',
role: USER_ROLES.NONE,
author_id: undefined,
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
email_verified: false,
});
overrideAdB2cService(service, {
changePassword: async () => {
throw new Error('ADB2C Error');
},
getUser: async () => {
return {
id: adminExternalId,
displayName: 'admin',
};
},
});
overrideSendgridService(service, {
sendMail: jest.fn(),
});
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
try {
await service.confirmUserAndInitPassword(
makeContext('trackingId', 'requestId'),
token,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
const user = await getUserFromExternalId(source, 'externalId_user1');
// ユーザーが認証されていないことを確認
expect(user?.email_verified).toBe(false);
// メールが送信されていないことを確認
expect(sendgridService.sendMail).toBeCalledTimes(0);
});
it('ADB2Cユーザーのパスワード更新に失敗した場合、リカバリ処理を行うが、リカバリ処理に失敗すると認証のままになる(メール認証API)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const sendgridService = module.get<SendGridService>(SendGridService);
const adminExternalId = 'ADMIN0001';
const { account } = await makeTestAccount(
source,
{},
{ external_id: adminExternalId },
);
const { id: accountId } = account;
// ユーザー作成
await makeTestUser(source, {
account_id: accountId,
external_id: 'externalId_user1',
role: USER_ROLES.NONE,
author_id: undefined,
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
email_verified: false,
});
const loggerSpy = jest
.spyOn(service['logger'], 'error')
.mockImplementation();
overrideAdB2cService(service, {
changePassword: async () => {
throw new Error('ADB2C Error');
},
getUser: async () => {
return {
id: adminExternalId,
displayName: 'admin',
};
},
});
overrideUsersRepositoryService(service, {
updateUserUnverified: async () => {
throw new Error('DB Error');
},
});
overrideSendgridService(service, {
sendMail: jest.fn(),
});
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
try {
await service.confirmUserAndInitPassword(
makeContext('trackingId', 'requestId'),
token,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
const user = await getUserFromExternalId(source, 'externalId_user1');
// ユーザーが認証されたままであることを確認
expect(user?.email_verified).toBe(true);
// メールが送信されていないことを確認
expect(sendgridService.sendMail).toBeCalledTimes(0);
// loggerSpyがスパイしているlogger.logメソッドが出力したログを確認目視確認用
const logs = loggerSpy.mock.calls.map((call) => call[0]);
// 手動復旧が必要なエラーログが出力されていること
expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe(true);
});
it('DBネットワークエラーとなる場合、エラーとなる。(メール認証API)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
usersRepositoryMockValue.findUserById = {
id: 1,
external_id: 'TEST9999',
account_id: 1,
role: 'None',
accepted_eula_version: 'string',
accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string',
email_verified: false,
created_by: 'string;',
created_at: new Date(),
updated_by: 'string;',
updated_at: new Date(),
auto_renew: true,
notification: true,
encryption: false,
prompt: false,
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
};
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
const sendGridMockValue = makeDefaultSendGridlValue();
usersRepositoryMockValue.updateUserVerified = new Error('DB error');
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
adb2cParam,
sendGridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const sendgridService = module.get<SendGridService>(SendGridService);
const adB2cService = module.get<AdB2cService>(AdB2cService);
const adminExternalId = 'ADMIN0001';
const { account } = await makeTestAccount(
source,
{},
{ external_id: adminExternalId },
);
const { id: accountId } = account;
// ユーザー作成
await makeTestUser(source, {
account_id: accountId,
external_id: 'externalId_user1',
role: USER_ROLES.NONE,
author_id: undefined,
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
email_verified: false,
});
overrideAdB2cService(service, {
changePassword: jest.fn(),
getUser: async () => {
return {
id: adminExternalId,
displayName: 'admin',
};
},
});
overrideSendgridService(service, {
sendMail: jest.fn(),
});
// DBエラーを発生させる
overrideUsersRepositoryService(service, {
updateUserVerified: async () => {
throw new Error('DB Error');
},
});
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
await expect(
service.confirmUserAndInitPassword(
try {
await service.confirmUserAndInitPassword(
makeContext('trackingId', 'requestId'),
token,
),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
const user = await getUserFromExternalId(source, 'externalId_user1');
// ユーザーが認証されていないことを確認
expect(user?.email_verified).toBe(false);
// メールが送信されていないことを確認
expect(sendgridService.sendMail).toBeCalledTimes(0);
// パスワードが変更されていないことを確認
expect(adB2cService.changePassword).toBeCalledTimes(0);
});
it('メール送信に失敗した場合、リカバリ処理を行い、メールを未認証の状態にする。(メール認証API)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adb2cService = module.get<AdB2cService>(AdB2cService);
const adminExternalId = 'ADMIN0001';
const { account } = await makeTestAccount(
source,
{},
{ external_id: adminExternalId },
);
const { id: accountId } = account;
// ユーザー作成
await makeTestUser(source, {
account_id: accountId,
external_id: 'externalId_user1',
role: USER_ROLES.NONE,
author_id: undefined,
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
email_verified: false,
});
overrideAdB2cService(service, {
changePassword: jest.fn(),
getUser: async () => {
return {
id: adminExternalId,
displayName: 'admin',
};
},
});
overrideSendgridService(service, {
sendMail: async () => {
throw new Error('SendGrid Error');
},
});
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
try {
await service.confirmUserAndInitPassword(
makeContext('trackingId', 'requestId'),
token,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
const user = await getUserFromExternalId(source, 'externalId_user1');
// ユーザーが認証されていないことを確認
expect(user?.email_verified).toBe(false);
// ADB2Cのパスワードが変更されていることを確認(パスワードは変更されても、ユーザーにメールが届いていないので問題ない)
expect(adb2cService.changePassword).toBeCalledTimes(1);
});
it('メール送信に失敗した場合、リカバリ処理を行うが、リカバリ処理に失敗するとADB2Cのパスワードが変更され、DB上も認証された状態になる(メール認証API)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adB2cService = module.get<AdB2cService>(AdB2cService);
const adminExternalId = 'ADMIN0001';
const { account } = await makeTestAccount(
source,
{},
{ external_id: adminExternalId },
);
const { id: accountId } = account;
// ユーザー作成
await makeTestUser(source, {
account_id: accountId,
external_id: 'externalId_user1',
role: USER_ROLES.NONE,
author_id: undefined,
auto_renew: true,
encryption: false,
encryption_password: undefined,
prompt: false,
email_verified: false,
});
const loggerSpy = jest
.spyOn(service['logger'], 'error')
.mockImplementation();
overrideAdB2cService(service, {
changePassword: jest.fn(),
getUser: async () => {
return {
id: adminExternalId,
displayName: 'admin',
};
},
});
overrideUsersRepositoryService(service, {
updateUserUnverified: async () => {
throw new Error('DB Error');
},
});
overrideSendgridService(service, {
sendMail: async () => {
throw new Error('SendGrid Error');
},
});
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
try {
await service.confirmUserAndInitPassword(
makeContext('trackingId', 'requestId'),
token,
);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
const user = await getUserFromExternalId(source, 'externalId_user1');
// ユーザーが認証されたままであることを確認
expect(user?.email_verified).toBe(true);
// ADB2Cのパスワードが変更されていることを確認
expect(adB2cService.changePassword).toBeCalledTimes(1);
// loggerSpyがスパイしているlogger.logメソッドが出力したログを確認目視確認用
const logs = loggerSpy.mock.calls.map((call) => call[0]);
// 手動復旧が必要なエラーログが出力されていること
expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe(true);
});
});

View File

@ -516,10 +516,30 @@ export class UsersService {
);
}
const { accountId, userId, email } = decodedToken;
try {
// ユーザを認証済みにする
await this.usersRepository.updateUserVerified(context, userId);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case EmailAlreadyVerifiedError:
throw new HttpException(
makeErrorResponse('E010202'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
// ランダムなパスワードを生成する
const ramdomPassword = makePassword();
const { accountId, userId, email } = decodedToken;
try {
// ユーザー情報からAzure AD B2CのIDを特定する
const user = await this.usersRepository.findUserById(context, userId);
@ -530,8 +550,20 @@ export class UsersService {
extarnalId,
ramdomPassword,
);
// ユーザを認証済みにする
await this.usersRepository.updateUserVerified(context, userId);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(
`[${context.getTrackingId()}] change password failed. userId=${userId}`,
);
// リカバリー処理
// ユーザを未認証に戻す
await this.updateUserUnverified(context, userId);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メール送信処理
try {
@ -553,25 +585,13 @@ export class UsersService {
ramdomPassword,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case EmailAlreadyVerifiedError:
throw new HttpException(
makeErrorResponse('E010202'),
HttpStatus.BAD_REQUEST,
);
default:
// リカバリー処理
// ユーザーを未認証に戻す
await this.updateUserUnverified(context, userId);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${
@ -1791,4 +1811,36 @@ export class UsersService {
return primaryAdmin;
}
/**
*
* @param context
* @param userId
* @returns void
*/
private async updateUserUnverified(
context: Context,
userId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateUserUnverified.name
} | params: { userId: ${userId} };`,
);
try {
await this.usersRepository.updateUserUnverified(context, userId);
this.logger.log(
`[${context.getTrackingId()}] update user unverified: ${userId}`,
);
} catch (error) {
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to update user unverified: ${userId}`,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updateUserUnverified.name}`,
);
}
}
}

View File

@ -399,7 +399,7 @@ export class UsersRepositoryService {
}
/**
*
*
* @param user
* @returns update
*/
@ -437,6 +437,40 @@ export class UsersRepositoryService {
});
}
/**
*
* @param user
* @param context
* @param id
* @returns void
*/
async updateUserUnverified(context: Context, id: number): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const userRepo = entityManager.getRepository(User);
const targetUser = await userRepo.findOne({
where: {
id: id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
if (!targetUser) {
throw new UserNotFoundError(`User not Found.`);
}
targetUser.email_verified = false;
await updateEntity(
userRepo,
{ id: targetUser.id },
targetUser,
this.isCommentOut,
context,
);
});
}
/**
* Emailを認証済みにして
* @param id