Merged PR 732: [改善]認証用URLについて、ドメイン名の末尾に/が必要となることへの対応

## 概要
[Task3625: [改善]認証用URLについて、ドメイン名の末尾に/が必要となることへの対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3625)

- URLクラスとpathクラスを用いてURLを構築するよう修正
- 送信するメールに関わるテストを追加

## レビューポイント
- 修正内容は妥当であるか
- 漏れていそうなURL系の処理はないか
- 工数面を考慮したコスト対効果の観点から、メール送信を行うテスト全てに送信メール内容のチェックを行うテストコードは入れなかったが許容可能か

## 動作確認状況
- npm run testは通過
  - `.env.test` の `APP_DOMAIN` の末尾 `/` を付けて通過 & 消して通過 するかを確認
  - **一応追試をお願いしたいです**
This commit is contained in:
湯本 開 2024-02-06 05:03:45 +00:00
parent 84b0da1f95
commit a1b7505035
5 changed files with 250 additions and 29 deletions

View File

@ -82,7 +82,8 @@ export const overrideSendgridService = <TService>(
overrides: { overrides: {
sendMail?: ( sendMail?: (
context: Context, context: Context,
to: string, to: string[],
cc: string[],
from: string, from: string,
subject: string, subject: string,
text: string, text: string,

View File

@ -134,7 +134,26 @@ describe('createAccount', () => {
}, },
}); });
overrideSendgridService(service, {}); let _subject: string = "";
let _url: string | undefined = "";
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
const urlPattern = /https?:\/\/[^\s]+/g;
const urls = text.match(urlPattern);
const url = urls?.pop();
_subject = subject;
_url = url;
},
});
overrideBlobstorageService(service, { overrideBlobstorageService(service, {
createContainer: async () => { createContainer: async () => {
return; return;
@ -175,6 +194,10 @@ describe('createAccount', () => {
expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion); expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion);
expect(user?.account_id).toBe(accountId); expect(user?.account_id).toBe(accountId);
expect(user?.role).toBe(role); expect(user?.role).toBe(role);
// 想定通りのメールが送られているか確認
expect(_subject).toBe('User Registration Notification [U-102]');
expect(_url?.startsWith('http://localhost:8081/mail-confirm?verify=')).toBeTruthy();
}); });
it('アカウントを作成がAzure AD B2Cへの通信失敗によって失敗すると500エラーが発生する', async () => { it('アカウントを作成がAzure AD B2Cへの通信失敗によって失敗すると500エラーが発生する', async () => {
@ -5704,9 +5727,39 @@ describe('アカウント情報更新', () => {
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
if (!module) fail(); if (!module) fail();
const service = module.get<AccountsService>(AccountsService); const service = module.get<AccountsService>(AccountsService);
let _subject: string = "";
let _url: string | undefined = "";
overrideSendgridService(service, { overrideSendgridService(service, {
sendMail: async () => { sendMail: async (
return; context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
const urlPattern = /https?:\/\/[^\s]+/g;
const urls = text.match(urlPattern);
const url = urls?.pop();
_subject = subject;
_url = url;
},
});
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'TEMP' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
}, },
}); });
@ -5733,6 +5786,9 @@ describe('アカウント情報更新', () => {
expect(account?.delegation_permission).toBe(true); expect(account?.delegation_permission).toBe(true);
expect(account?.primary_admin_user_id).toBe(tier5Accounts.admin.id); expect(account?.primary_admin_user_id).toBe(tier5Accounts.admin.id);
expect(account?.secondary_admin_user_id).toBe(null); expect(account?.secondary_admin_user_id).toBe(null);
// 想定通りのメールが送られているか確認
expect(_subject).toBe('Account Edit Notification [U-112]');
expect(_url).toBe('http://localhost:8081/');
}); });
it('アカウント情報を更新する(第五階層以外が実行)', async () => { it('アカウント情報を更新する(第五階層以外が実行)', async () => {
if (!source) fail(); if (!source) fail();
@ -6364,7 +6420,27 @@ describe('deleteAccountAndData', () => {
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
if (!module) fail(); if (!module) fail();
const service = module.get<AccountsService>(AccountsService); const service = module.get<AccountsService>(AccountsService);
overrideSendgridService(service, {}); let _subject: string = '';
let _url: string | undefined = '';
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
const urlPattern = /https?:\/\/[^\s]+/g;
const urls = text.match(urlPattern);
const url = urls?.pop();
_subject = subject;
_url = url;
},
});
// 第一~第四階層のアカウント作成 // 第一~第四階層のアカウント作成
const { const {
tier1Accounts: tier1Accounts, tier1Accounts: tier1Accounts,
@ -6485,10 +6561,36 @@ describe('deleteAccountAndData', () => {
licensesB[0].id, licensesB[0].id,
); );
// ADB2Cユーザーの削除成功
overrideAdB2cService(service, { overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'TEMP' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) => {
return externalIds.map((x) => ({
displayName: 'admin',
id: x,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: `mail+${x}@example.com`,
},
],
}));
},
deleteUsers: jest.fn(), deleteUsers: jest.fn(),
}); });
// blobstorageコンテナの削除成功 // blobstorageコンテナの削除成功
overrideBlobstorageService(service, { overrideBlobstorageService(service, {
deleteContainer: jest.fn(), deleteContainer: jest.fn(),
@ -6559,6 +6661,9 @@ describe('deleteAccountAndData', () => {
const LicenseAllocationHistoryArchive = const LicenseAllocationHistoryArchive =
await getLicenseAllocationHistoryArchive(source); await getLicenseAllocationHistoryArchive(source);
expect(LicenseAllocationHistoryArchive.length).toBe(1); expect(LicenseAllocationHistoryArchive.length).toBe(1);
expect(_subject).toBe('Account Deleted Notification [U-111]');
expect(_url).toBe('http://localhost:8081/');
}); });
it('アカウントの削除に失敗した場合はエラーを返す', async () => { it('アカウントの削除に失敗した場合はエラーを返す', async () => {
if (!source) fail(); if (!source) fail();

View File

@ -17,15 +17,15 @@ import {
selectOrderLicense, selectOrderLicense,
} from './test/utility'; } from './test/utility';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';
import { makeContext } from '../../common/log'; import { Context, makeContext } from '../../common/log';
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants'; import { ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
import { import {
makeHierarchicalAccounts, makeHierarchicalAccounts,
makeTestSimpleAccount, makeTestSimpleAccount,
makeTestUser, makeTestUser,
} from '../../common/test/utility'; } from '../../common/test/utility';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { overrideSendgridService } from '../../common/test/overrides'; import { overrideAdB2cService, overrideSendgridService } from '../../common/test/overrides';
import { truncateAllTable } from '../../common/test/init'; import { truncateAllTable } from '../../common/test/init';
describe('ライセンス注文', () => { describe('ライセンス注文', () => {
@ -672,7 +672,18 @@ describe('ライセンス割り当て', () => {
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
if (!module) fail(); if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source); const { id: dealerId } = await makeTestSimpleAccount(source, { company_name: "DEALER_COMPANY", tier: 4 });
const { id: dealerAdminId } = await makeTestUser(source, {
account_id: dealerId,
external_id: 'userId_admin',
role: 'admin',
author_id: undefined,
});
const { id: accountId } = await makeTestSimpleAccount(source, {
parent_account_id: dealerId,
tier: 5
});
const { id: userId } = await makeTestUser(source, { const { id: userId } = await makeTestUser(source, {
account_id: accountId, account_id: accountId,
external_id: 'userId', external_id: 'userId',
@ -701,7 +712,55 @@ describe('ライセンス割り当て', () => {
); );
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {}); let _subject: string = '';
let _url: string | undefined = '';
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
return {
displayName: 'TEMP' + externalId,
id: externalId,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: 'mail@example.com',
},
],
};
},
getUsers: async (context, externalIds) => {
return externalIds.map((x) => ({
displayName: 'admin',
id: x,
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'xxxxxx',
issuerAssignedId: `mail+${x}@example.com`,
},
],
}));
}
});
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
const urlPattern = /https?:\/\/[^\s]+/g;
const urls = text.match(urlPattern);
const url = urls?.pop();
_subject = subject;
_url = url;
},
});
const expiry_date = new NewAllocatedLicenseExpirationDate(); const expiry_date = new NewAllocatedLicenseExpirationDate();
@ -735,6 +794,9 @@ describe('ライセンス割り当て', () => {
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe( expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId, accountId,
); );
expect(_subject).toBe('License Assigned Notification [U-108]');
expect(_url).toBe('http://localhost:8081/');
}); });
it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => { it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => {

View File

@ -108,7 +108,26 @@ describe('UsersService.confirmUser', () => {
}); });
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {}); let _subject: string = '';
let _url: string | undefined = '';
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
const urlPattern = /https?:\/\/[^\s]+/g;
const urls = text.match(urlPattern);
const url = urls?.pop();
_subject = subject;
_url = url;
},
});
// account id:1, user id: 2のトークン // account id:1, user id: 2のトークン
const token = const token =
@ -149,6 +168,8 @@ describe('UsersService.confirmUser', () => {
delete_order_id: null, delete_order_id: null,
user: null, user: null,
}); });
expect(_subject).toBe('Account Registered Notification [U-101]');
expect(_url).toBe('http://localhost:8081/');
}, 600000); }, 600000);
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => { it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => {
@ -506,7 +527,26 @@ describe('UsersService.createUser', () => {
}; };
}, },
}); });
overrideSendgridService(service, {}); let _subject: string = '';
let _url: string | undefined = '';
overrideSendgridService(service, {
sendMail: async (
context: Context,
to: string[],
cc: string[],
from: string,
subject: string,
text: string,
html: string,
) => {
const urlPattern = /https?:\/\/[^\s]+/g;
const urls = text.match(urlPattern);
const url = urls?.pop();
_subject = subject;
_url = url;
},
});
expect( expect(
await service.createUser( await service.createUser(
@ -536,6 +576,11 @@ describe('UsersService.createUser', () => {
// 他にユーザーが登録されていないことを確認 // 他にユーザーが登録されていないことを確認
const users = await getUsers(source); const users = await getUsers(source);
expect(users.length).toEqual(2); expect(users.length).toEqual(2);
expect(_subject).toBe('User Registration Notification [U-114]');
expect(
_url?.startsWith('http://localhost:8081/mail-confirm/user?verify='),
).toBeTruthy();
}); });
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => { it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => {

View File

@ -21,6 +21,7 @@ import {
VERIFY_LINK, VERIFY_LINK,
TEMPORARY_PASSWORD, TEMPORARY_PASSWORD,
} from '../../templates/constants'; } from '../../templates/constants';
import { URL } from 'node:url';
@Injectable() @Injectable()
export class SendGridService { export class SendGridService {
@ -204,12 +205,13 @@ export class SendGridService {
); );
try { try {
const subject = 'Account Registered Notification [U-101]'; const subject = 'Account Registered Notification [U-101]';
const url = new URL(this.appDomain).href;
const html = this.templateU101Html const html = this.templateU101Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
const text = this.templateU101Text const text = this.templateU101Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
await this.sendMail( await this.sendMail(
context, context,
@ -255,8 +257,9 @@ export class SendGridService {
this.emailConfirmLifetime, this.emailConfirmLifetime,
privateKey, privateKey,
); );
const path = 'mail-confirm/'; const paths = path.join('mail-confirm');
const verifyUrl = `${this.appDomain}${path}?verify=${token}`; const url = new URL(paths, this.appDomain).href;
const verifyUrl = `${url}?verify=${token}`;
const subject = 'User Registration Notification [U-102]'; const subject = 'User Registration Notification [U-102]';
const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl); const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl);
@ -466,6 +469,7 @@ export class SendGridService {
); );
try { try {
const subject = 'License Assigned Notification [U-108]'; const subject = 'License Assigned Notification [U-108]';
const url = new URL(this.appDomain).href;
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU108Html const html = this.templateU108Html
@ -473,13 +477,13 @@ export class SendGridService {
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail) .replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
const text = this.templateU108Text const text = this.templateU108Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail) .replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail]; const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail];
@ -574,17 +578,18 @@ export class SendGridService {
); );
try { try {
const subject = 'Account Deleted Notification [U-111]'; const subject = 'Account Deleted Notification [U-111]';
const url = new URL(this.appDomain).href;
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU111Html const html = this.templateU111Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
const text = this.templateU111Text const text = this.templateU111Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -627,6 +632,7 @@ export class SendGridService {
let html: string; let html: string;
let text: string; let text: string;
const url = new URL(this.appDomain).href;
// 親アカウントがない場合は別のテンプレートを使用する // 親アカウントがない場合は別のテンプレートを使用する
if (dealerAccountName === null) { if (dealerAccountName === null) {
@ -634,22 +640,22 @@ export class SendGridService {
html = this.templateU112NoParentHtml html = this.templateU112NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
text = this.templateU112NoParentText text = this.templateU112NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
} else { } else {
html = this.templateU112Html html = this.templateU112Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
text = this.templateU112Text text = this.templateU112Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
} }
// メールを送信する // メールを送信する
@ -745,18 +751,20 @@ export class SendGridService {
this.emailConfirmLifetime, this.emailConfirmLifetime,
privateKey, privateKey,
); );
const path = 'mail-confirm/user/';
const verifyLink = `${this.appDomain}${path}?verify=${token}`; const paths = path.join('mail-confirm', '/user');
const url = new URL(paths, this.appDomain);
const verifyUrl = `${url}?verify=${token}`;
const subject = 'User Registration Notification [U-114]'; const subject = 'User Registration Notification [U-114]';
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU114Html const html = this.templateU114Html
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(VERIFY_LINK, verifyLink); .replaceAll(VERIFY_LINK, verifyUrl);
const text = this.templateU114Text const text = this.templateU114Text
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(VERIFY_LINK, verifyLink); .replaceAll(VERIFY_LINK, verifyUrl);
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(