Fix base email template footer link & logo aspect ratio (#16233)

* fix email base template footer link

* fix logo box to be square

* clean up unused classes & attributes

* only add url if user has app access

* add test for notifications service

* re-add database mock

* attempt to fix mock in test

* mock PUBLIC_URL

Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
This commit is contained in:
Azri Kahar
2022-11-09 23:52:44 +08:00
committed by GitHub
parent 4b1789afa1
commit 24f1e539ba
3 changed files with 228 additions and 8 deletions

View File

@@ -125,14 +125,14 @@ blockquote > p {
<tbody>
<tr>
<td align="left" valign="top" style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; mso-table-lspace:0pt; mso-table-rspace:0pt; padding:0 0 30px 0">
<table><tbody><tr><td align="center" valign="middle" style="background-color:{{ projectColor }};width:48px;height:48px;border-radius:4px;padding:6px;">
<img id="logo" src="{{ projectLogo }}" alt="{{ projectName }} Logo" width="40" height="auto" border="0" style="-ms-interpolation-mode:bicubic; border:0; height:auto; line-height:100%; outline:none; text-decoration:none; display:block; width:40px;object-fit:contain;">
<table><tbody><tr><td align="center" valign="middle" style="background-color:{{ projectColor }};max-width:48px;max-height:48px;border-radius:4px;padding:6px;">
<img id="logo" src="{{ projectLogo }}" alt="{{ projectName }} Logo" width="40" height="auto" border="0" style="-ms-interpolation-mode:bicubic; border:0; height:40px; line-height:100%; outline:none; text-decoration:none; display:block; width:40px; object-fit:contain;">
</td></tr></tbody></table>
</td>
</tr>
<tr>
<td id="content" align="left" valign="top" style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; mso-table-lspace:0pt; mso-table-rspace:0pt; padding:40px 50px 50px 50px; font-family:Open Sans, Helvetica, Arial, sans-serif; border-radius:4px; box-shadow:0 4px 0 #15253A; background-color:#FFFFFE; color:#172940; font-size:15px; line-height:26px; margin:0" bgcolor="#FFFFFE">
<div id="hs_cos_wrapper_email_template_main_email_body" class="hs_cos_wrapper hs_cos_wrapper_widget hs_cos_wrapper_type_module" style="color: inherit; font-size: inherit; line-height: inherit;" data-hs-cos-general-type="widget" data-hs-cos-type="module">
<div style="color: inherit; font-size: inherit; line-height: inherit;">
{% block content %}{{ html }}{% endblock %}
@@ -142,7 +142,7 @@ blockquote > p {
<tr>
<td align="center" valign="middle" style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; mso-table-lspace:0pt; mso-table-rspace:0pt; padding:25px 0; font-family:Open Sans, Helvetica, Arial, sans-serif; color:#FFFFFE">
<p style="margin-bottom: 1em; color: #A2B5CD;font-size: 12px; line-height: 16px;">
Sent by the team at {{ projectName }} — <a style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; text-decoration:none; color:#A2B5CD" class="unsubscribe" data-unsubscribe="true" href="{{ url }}" data-hs-link-id="0" target="_blank">Manage Emails</a><br>
Sent by the team at {{ projectName }}{% if url %} — <a style="-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; text-decoration:none; color:#A2B5CD" href="{{ url }}" target="_blank">Manage Your Account</a>{% endif %}<br>
{% block footer %}{% endblock %}
</p>
</td>

View File

@@ -0,0 +1,215 @@
import { NotificationsService, ItemsService } from '.';
jest.mock('../../src/env', () => ({
...jest.requireActual('../../src/env').default,
PUBLIC_URL: '/',
}));
jest.mock('../../src/database/index', () => {
return { __esModule: true, default: jest.fn(), getDatabaseClient: jest.fn().mockReturnValue('postgres') };
});
describe('Integration Tests', () => {
describe('Services / Notifications', () => {
let service: NotificationsService;
beforeEach(() => {
service = new NotificationsService({
schema: { collections: {}, relations: [] },
});
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('createOne', () => {
let superCreateOneSpy: jest.SpyInstance;
let thisSendEmailSpy: jest.SpyInstance;
beforeEach(() => {
superCreateOneSpy = jest.spyOn(ItemsService.prototype, 'createOne').mockImplementation(jest.fn());
thisSendEmailSpy = jest.spyOn(NotificationsService.prototype, 'sendEmail').mockImplementation(jest.fn());
});
it('create a notification and send email', async () => {
const data = {
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
};
await service.createOne(data);
expect(superCreateOneSpy).toHaveBeenCalled();
expect(superCreateOneSpy).toBeCalledWith(data, undefined);
expect(thisSendEmailSpy).toHaveBeenCalled();
expect(thisSendEmailSpy).toBeCalledWith(data);
});
});
describe('createMany', () => {
let superCreateManySpy: jest.SpyInstance;
let thisSendEmailSpy: jest.SpyInstance;
beforeEach(() => {
superCreateManySpy = jest.spyOn(ItemsService.prototype, 'createMany').mockImplementation(jest.fn());
thisSendEmailSpy = jest.spyOn(NotificationsService.prototype, 'sendEmail').mockImplementation(jest.fn());
});
it('create many notifications and send email for notification', async () => {
const data = [
{
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
},
{
recipient: '51260c36-e944-4b0a-a370-dc83070d9d2b',
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
},
];
await service.createMany(data);
expect(superCreateManySpy).toBeCalledTimes(1);
expect(superCreateManySpy).toBeCalledWith(data, undefined);
expect(thisSendEmailSpy).toBeCalledTimes(data.length);
});
});
describe('sendEmail', () => {
let usersServiceReadOneSpy: jest.SpyInstance;
let mailServiceSendSpy: jest.SpyInstance;
beforeEach(() => {
usersServiceReadOneSpy = jest.spyOn(service.usersService, 'readOne').mockImplementation(jest.fn());
mailServiceSendSpy = jest.spyOn(service.mailService, 'send').mockImplementation(jest.fn());
});
it('do nothing when there is no recipient', async () => {
await service.sendEmail({
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
});
expect(usersServiceReadOneSpy).not.toHaveBeenCalled();
});
it('read recipient detail from userService when there is recipient', async () => {
usersServiceReadOneSpy.mockReturnValue(Promise.resolve({ id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d' }));
await service.sendEmail({
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
});
expect(usersServiceReadOneSpy).toHaveBeenCalled();
});
it('do not send email when user does not have email', async () => {
usersServiceReadOneSpy.mockReturnValue(Promise.resolve({ id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d' }));
await service.sendEmail({
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
});
expect(usersServiceReadOneSpy).toHaveBeenCalled();
expect(mailServiceSendSpy).not.toHaveBeenCalled();
});
it('do not send email when user have email but disabled email notification', async () => {
usersServiceReadOneSpy.mockReturnValue(
Promise.resolve({
id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
email: 'user@example.com',
email_notifications: false,
})
);
await service.sendEmail({
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
});
expect(usersServiceReadOneSpy).toHaveBeenCalled();
expect(mailServiceSendSpy).not.toHaveBeenCalled();
});
it('send email without url when user role does not have app access', async () => {
const userDetail = {
id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
email: 'user@example.com',
email_notifications: true,
role: {
app_access: false,
},
};
usersServiceReadOneSpy.mockReturnValue(Promise.resolve(userDetail));
const notificationDetail = {
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
};
await service.sendEmail(notificationDetail);
expect(usersServiceReadOneSpy).toHaveBeenCalled();
expect(mailServiceSendSpy).toHaveBeenCalled();
expect(mailServiceSendSpy).toHaveBeenCalledWith({
template: {
name: 'base',
data: { html: `<p>${notificationDetail.message}</p>\n` },
},
to: userDetail.email,
subject: notificationDetail.subject,
});
});
it('send email with url when user role have app access', async () => {
const userDetail = {
id: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
email: 'user@example.com',
email_notifications: true,
role: {
app_access: true,
},
};
usersServiceReadOneSpy.mockReturnValue(Promise.resolve(userDetail));
const notificationDetail = {
recipient: '5aa7ffb5-bd54-46ab-8654-6dfead39694d',
sender: null,
subject: 'Notification Subject',
message: 'Notification Message',
};
await service.sendEmail(notificationDetail);
expect(usersServiceReadOneSpy).toHaveBeenCalled();
expect(mailServiceSendSpy).toHaveBeenCalled();
expect(mailServiceSendSpy).toHaveBeenCalledWith({
template: {
name: 'base',
data: {
url: `/admin/users/${userDetail.id}`,
html: `<p>${notificationDetail.message}</p>\n`,
},
},
to: userDetail.email,
subject: notificationDetail.subject,
});
});
});
});
});

View File

@@ -2,9 +2,11 @@ import { AbstractServiceOptions, PrimaryKey, MutationOptions } from '../types';
import { ItemsService } from './items';
import { Notification } from '@directus/shared/types';
import { md } from '../utils/md';
import { Url } from '../utils/url';
import { UsersService } from './users';
import { MailService } from './mail';
import logger from '../logger';
import env from '../env';
export class NotificationsService extends ItemsService {
usersService: UsersService;
@@ -36,16 +38,19 @@ export class NotificationsService extends ItemsService {
async sendEmail(data: Partial<Notification>) {
if (data.recipient) {
const user = await this.usersService.readOne(data.recipient, { fields: ['email', 'email_notifications'] });
const user = await this.usersService.readOne(data.recipient, {
fields: ['id', 'email', 'email_notifications', 'role.app_access'],
});
const manageUserAccountUrl = new Url(env.PUBLIC_URL).addPath('admin', 'users', user.id).toString();
const html = data.message ? md(data.message) : '';
if (user.email && user.email_notifications === true) {
try {
await this.mailService.send({
template: {
name: 'base',
data: {
html: data.message ? md(data.message) : '',
},
data: user.role?.app_access ? { url: manageUserAccountUrl, html } : { html },
},
to: user.email,
subject: data.subject,