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: {
sendMail?: (
context: Context,
to: string,
to: string[],
cc: string[],
from: string,
subject: 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, {
createContainer: async () => {
return;
@ -175,6 +194,10 @@ describe('createAccount', () => {
expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion);
expect(user?.account_id).toBe(accountId);
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 () => {
@ -5704,9 +5727,39 @@ describe('アカウント情報更新', () => {
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AccountsService>(AccountsService);
let _subject: string = "";
let _url: string | undefined = "";
overrideSendgridService(service, {
sendMail: async () => {
return;
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;
},
});
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?.primary_admin_user_id).toBe(tier5Accounts.admin.id);
expect(account?.secondary_admin_user_id).toBe(null);
// 想定通りのメールが送られているか確認
expect(_subject).toBe('Account Edit Notification [U-112]');
expect(_url).toBe('http://localhost:8081/');
});
it('アカウント情報を更新する(第五階層以外が実行)', async () => {
if (!source) fail();
@ -6364,7 +6420,27 @@ describe('deleteAccountAndData', () => {
const module = await makeTestingModule(source);
if (!module) fail();
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 {
tier1Accounts: tier1Accounts,
@ -6485,10 +6561,36 @@ describe('deleteAccountAndData', () => {
licensesB[0].id,
);
// ADB2Cユーザーの削除成功
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(),
});
// blobstorageコンテナの削除成功
overrideBlobstorageService(service, {
deleteContainer: jest.fn(),
@ -6559,6 +6661,9 @@ describe('deleteAccountAndData', () => {
const LicenseAllocationHistoryArchive =
await getLicenseAllocationHistoryArchive(source);
expect(LicenseAllocationHistoryArchive.length).toBe(1);
expect(_subject).toBe('Account Deleted Notification [U-111]');
expect(_url).toBe('http://localhost:8081/');
});
it('アカウントの削除に失敗した場合はエラーを返す', async () => {
if (!source) fail();

View File

@ -17,15 +17,15 @@ import {
selectOrderLicense,
} from './test/utility';
import { UsersService } from '../users/users.service';
import { makeContext } from '../../common/log';
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
import { Context, makeContext } from '../../common/log';
import { ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
import {
makeHierarchicalAccounts,
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
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';
describe('ライセンス注文', () => {
@ -672,7 +672,18 @@ describe('ライセンス割り当て', () => {
const module = await makeTestingModule(source);
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, {
account_id: accountId,
external_id: 'userId',
@ -701,7 +712,55 @@ describe('ライセンス割り当て', () => {
);
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();
@ -735,6 +794,9 @@ describe('ライセンス割り当て', () => {
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
expect(_subject).toBe('License Assigned Notification [U-108]');
expect(_url).toBe('http://localhost:8081/');
});
it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => {

View File

@ -108,7 +108,26 @@ describe('UsersService.confirmUser', () => {
});
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のトークン
const token =
@ -149,6 +168,8 @@ describe('UsersService.confirmUser', () => {
delete_order_id: null,
user: null,
});
expect(_subject).toBe('Account Registered Notification [U-101]');
expect(_url).toBe('http://localhost:8081/');
}, 600000);
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(
await service.createUser(
@ -536,6 +576,11 @@ describe('UsersService.createUser', () => {
// 他にユーザーが登録されていないことを確認
const users = await getUsers(source);
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 () => {

View File

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