@stamhoofd/backend 2.137.4 → 2.138.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -17
- package/src/boot.ts +5 -0
- package/src/crons/balance-emails.ts +2 -1
- package/src/crons/delete-expired-mfa-tokens.ts +35 -0
- package/src/crons/drip-emails.ts +2 -1
- package/src/crons/index.ts +1 -0
- package/src/crons/invoices.ts +5 -3
- package/src/crons.ts +4 -2
- package/src/email-recipient-loaders/orders.ts +2 -1
- package/src/email-recipient-loaders/organizations.ts +3 -2
- package/src/email-recipient-loaders/payments.ts +3 -2
- package/src/endpoints/auth/ConfirmTOTPEndpoint.ts +84 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -2
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +61 -1
- package/src/endpoints/auth/CreateTokenEndpoint.ts +135 -7
- package/src/endpoints/auth/DeletePasskeyEndpoint.ts +61 -0
- package/src/endpoints/auth/DeleteTOTPEndpoint.ts +62 -0
- package/src/endpoints/auth/DeleteUserEndpoint.ts +1 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +4 -2
- package/src/endpoints/auth/GetMFAChallengeEndpoint.ts +56 -0
- package/src/endpoints/auth/GetMFAStatusEndpoint.ts +31 -0
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -1
- package/src/endpoints/auth/MFA.security.test.ts +688 -0
- package/src/endpoints/auth/MFA.test.ts +1398 -0
- package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +2 -2
- package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
- package/src/endpoints/auth/RegenerateRecoveryCodesEndpoint.ts +51 -0
- package/src/endpoints/auth/RegisterPasskeyEndpoint.ts +99 -0
- package/src/endpoints/auth/RegisterPasskeyOptionsEndpoint.ts +49 -0
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
- package/src/endpoints/auth/SetupTOTPEndpoint.ts +54 -0
- package/src/endpoints/auth/SignupEndpoint.ts +17 -4
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +16 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +5 -3
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +5 -3
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +2 -1
- package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -1
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +11 -6
- package/src/endpoints/global/files/UploadFile.test.ts +206 -0
- package/src/endpoints/global/files/UploadFile.ts +31 -6
- package/src/endpoints/global/files/UploadImage.test.ts +177 -0
- package/src/endpoints/global/files/UploadImage.ts +23 -4
- package/src/endpoints/global/files/upload-security.test.ts +837 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.ts +2 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +4 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +35 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +8 -0
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +92 -0
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +32 -3
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +100 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +5 -5
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +160 -0
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.ts +44 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
- package/src/excel-loaders/balance-items.ts +1 -1
- package/src/excel-loaders/event-notifications.ts +6 -7
- package/src/excel-loaders/members.test.ts +2 -2
- package/src/excel-loaders/members.ts +9 -10
- package/src/excel-loaders/organizations.ts +14 -20
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/platform-memberships.ts +7 -7
- package/src/excel-loaders/platform-sheets.test.ts +113 -0
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +9 -9
- package/src/helpers/AdminPermissionChecker.ts +1 -1
- package/src/helpers/AuthenticatedStructures.ts +21 -10
- package/src/helpers/Context.ts +114 -4
- package/src/helpers/EmailBuilder.test.ts +287 -0
- package/src/helpers/EmailBuilder.ts +811 -0
- package/src/helpers/EmailResumer.ts +2 -1
- package/src/helpers/ForwardHandler.ts +2 -1
- package/src/helpers/MFAEncryption.ts +34 -0
- package/src/helpers/MembershipCharger.ts +3 -2
- package/src/helpers/RecoveryCodeHelper.ts +81 -0
- package/src/helpers/TOTPHelper.ts +79 -0
- package/src/helpers/TenantContext.test.ts +96 -0
- package/src/helpers/TenantContext.ts +67 -0
- package/src/helpers/TwoFactorHelper.ts +356 -0
- package/src/helpers/WebauthnHelper.ts +211 -0
- package/src/helpers/data/aaguids.json +1006 -0
- package/src/middleware/TenantScopeMiddleware.test.ts +41 -0
- package/src/middleware/TenantScopeMiddleware.ts +24 -0
- package/src/seeds/1785417302-fill-user-last-active-at.sql +6 -0
- package/src/services/AdminSessionService.ts +41 -0
- package/src/services/BalanceItemService.ts +2 -1
- package/src/services/DocumentRenderService.test.ts +229 -0
- package/src/services/DocumentRenderService.ts +180 -0
- package/src/services/EmailPreviewService.test.ts +300 -0
- package/src/services/EmailPreviewService.ts +239 -0
- package/src/services/EmailSendService.test.ts +1218 -0
- package/src/services/EmailSendService.ts +705 -0
- package/src/services/EventNotificationService.ts +2 -1
- package/src/services/FileSignService.test.ts +85 -0
- package/src/services/FileSignService.ts +23 -1
- package/src/services/InvoicePdfService.ts +3 -3
- package/src/services/InvoiceService.ts +4 -2
- package/src/services/InvoiceXMLService.ts +2 -1
- package/src/services/OrderService.test.ts +308 -0
- package/src/services/OrderService.ts +215 -0
- package/src/services/OrganizationAdminService.test.ts +47 -0
- package/src/services/OrganizationAdminService.ts +139 -0
- package/src/services/OrganizationDNSService.test.ts +177 -0
- package/src/services/OrganizationDNSService.ts +282 -0
- package/src/services/OrganizationEmailService.test.ts +57 -0
- package/src/services/OrganizationEmailService.ts +212 -0
- package/src/services/PasswordForgotService.test.ts +99 -0
- package/src/services/PasswordForgotService.ts +38 -0
- package/src/services/PaymentService.ts +5 -3
- package/src/services/PlatformMembershipService.test.ts +180 -0
- package/src/services/PlatformMembershipService.ts +281 -4
- package/src/services/ReferralService.ts +3 -2
- package/src/services/RegistrationService.ts +4 -2
- package/src/services/SSOService.ts +106 -14
- package/src/services/STPackageService.test.ts +191 -0
- package/src/services/STPackageService.ts +115 -1
- package/src/services/TwoFactorAuditLogService.ts +64 -0
- package/src/services/VerificationCodeService.test.ts +71 -0
- package/src/services/VerificationCodeService.ts +101 -0
- package/tests/e2e/api-rate-limits.test.ts +1 -1
- package/tests/e2e/documents.test.ts +2 -1
- package/tests/e2e/private-files.test.ts +77 -14
- package/tests/helpers/MFATestHelper.ts +42 -0
- package/tests/helpers/TestServer.ts +4 -0
- package/tests/helpers/index.ts +1 -0
- package/tsconfig.build.json +2 -1
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
import type { LimitedFilteredRequest } from '@stamhoofd/structures';
|
|
2
|
+
import { BalanceItemType, EmailRecipientFilter, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailRecipientSubfilter, EmailStatus, OrganizationMetaData, PaginatedResponse } from '@stamhoofd/structures';
|
|
3
|
+
import { EmailMocker } from '@stamhoofd/email';
|
|
4
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
5
|
+
import { BalanceItemFactory, CachedBalance, Email, EmailRecipient, MemberFactory, OrganizationFactory, Platform, UserFactory } from '@stamhoofd/models';
|
|
6
|
+
import { EmailSendService } from './EmailSendService.js';
|
|
7
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
8
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
9
|
+
|
|
10
|
+
async function buildEmail(data: {
|
|
11
|
+
recipients: EmailRecipientStruct[];
|
|
12
|
+
} & Partial<Email>) {
|
|
13
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
|
|
14
|
+
fetch: async (query: LimitedFilteredRequest) => {
|
|
15
|
+
return Promise.resolve(
|
|
16
|
+
new PaginatedResponse<EmailRecipientStruct[], LimitedFilteredRequest>({
|
|
17
|
+
results: data.recipients,
|
|
18
|
+
next: undefined,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
count: async (query: LimitedFilteredRequest) => {
|
|
24
|
+
return data.recipients.length;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const model = new Email();
|
|
29
|
+
model.userId = null;
|
|
30
|
+
model.organizationId = data.organizationId ?? null;
|
|
31
|
+
model.recipientFilter = EmailRecipientFilter.create({
|
|
32
|
+
filters: [
|
|
33
|
+
EmailRecipientSubfilter.create({
|
|
34
|
+
type: EmailRecipientFilterType.Members,
|
|
35
|
+
filter: { id: { $in: ['test'] } },
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
model.subject = data.subject ?? 'This is a test email';
|
|
41
|
+
model.html = data.html ?? '<p>This is a test email</p> {{unsubscribeUrl}}';
|
|
42
|
+
model.text = data.text ?? 'This is a test email {{unsubscribeUrl}}';
|
|
43
|
+
model.json = data.json ?? {};
|
|
44
|
+
model.status = data.status ?? EmailStatus.Draft;
|
|
45
|
+
model.attachments = [];
|
|
46
|
+
model.fromAddress = data.fromAddress ?? 'test@stamhoofd.be';
|
|
47
|
+
model.fromName = data.fromName ?? null;
|
|
48
|
+
model.emailType = data.emailType ?? null;
|
|
49
|
+
|
|
50
|
+
await model.save();
|
|
51
|
+
|
|
52
|
+
return model;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* An email that won't be able to fetch recipients
|
|
57
|
+
*/
|
|
58
|
+
async function buildFailEmail(data: {
|
|
59
|
+
recipients: EmailRecipientStruct[];
|
|
60
|
+
} & Partial<Email>) {
|
|
61
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
|
|
62
|
+
fetch: async (query: LimitedFilteredRequest) => {
|
|
63
|
+
throw new Error('This is a simulated error while fetching recipients');
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
count: async (query: LimitedFilteredRequest) => {
|
|
67
|
+
return data.recipients.length;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const model = new Email();
|
|
72
|
+
model.userId = null;
|
|
73
|
+
model.organizationId = data.organizationId ?? null;
|
|
74
|
+
model.recipientFilter = EmailRecipientFilter.create({
|
|
75
|
+
filters: [
|
|
76
|
+
EmailRecipientSubfilter.create({
|
|
77
|
+
type: EmailRecipientFilterType.Members,
|
|
78
|
+
filter: { id: { $in: ['test'] } },
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
model.subject = data.subject ?? 'This is a test email';
|
|
84
|
+
model.html = data.html ?? '<p>This is a test email</p> {{unsubscribeUrl}}';
|
|
85
|
+
model.text = data.text ?? 'This is a test email {{unsubscribeUrl}}';
|
|
86
|
+
model.json = data.json ?? {};
|
|
87
|
+
model.status = data.status ?? EmailStatus.Draft;
|
|
88
|
+
model.attachments = [];
|
|
89
|
+
model.fromAddress = data.fromAddress ?? 'test@stamhoofd.be';
|
|
90
|
+
model.fromName = data.fromName ?? null;
|
|
91
|
+
model.emailType = data.emailType ?? null;
|
|
92
|
+
|
|
93
|
+
await model.save();
|
|
94
|
+
|
|
95
|
+
return model;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe('EmailSendService', () => {
|
|
99
|
+
it('Correctly whitelists email recipients', async () => {
|
|
100
|
+
TestUtils.setEnvironment('WHITELISTED_EMAIL_DESTINATIONS', ['*@stamhoofd-allowed-domain-tests.be']);
|
|
101
|
+
const model = await buildEmail({
|
|
102
|
+
recipients: [
|
|
103
|
+
EmailRecipientStruct.create({
|
|
104
|
+
firstName: 'Test',
|
|
105
|
+
lastName: 'Test',
|
|
106
|
+
email: 'example@not-whitelisted-domain.be',
|
|
107
|
+
}),
|
|
108
|
+
EmailRecipientStruct.create({
|
|
109
|
+
firstName: 'Test',
|
|
110
|
+
lastName: 'Test',
|
|
111
|
+
email: 'example2@stamhoofd-allowed-domain-tests.be',
|
|
112
|
+
}),
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
await EmailSendService.queueForSending(model, true);
|
|
116
|
+
await model.refresh();
|
|
117
|
+
|
|
118
|
+
// Check if it was sent correctly
|
|
119
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
120
|
+
expect(model.emailRecipientsCount).toBe(2);
|
|
121
|
+
expect(model.succeededCount).toBe(1);
|
|
122
|
+
expect(model.failedCount).toBe(1);
|
|
123
|
+
expect(model.emailErrors).toBe(null);
|
|
124
|
+
expect(model.recipientsErrors).toBe(null);
|
|
125
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
126
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
127
|
+
expect(await EmailMocker.getFailedCount()).toBe(0); // never tried to send any failed emails (whitelist)
|
|
128
|
+
|
|
129
|
+
// Load recipietns
|
|
130
|
+
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
131
|
+
expect(recipients).toIncludeSameMembers([
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
email: 'example2@stamhoofd-allowed-domain-tests.be',
|
|
134
|
+
sentAt: expect.any(Date),
|
|
135
|
+
failCount: 0,
|
|
136
|
+
failErrorMessage: null,
|
|
137
|
+
firstFailedAt: null,
|
|
138
|
+
lastFailedAt: null,
|
|
139
|
+
}),
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
email: 'example@not-whitelisted-domain.be',
|
|
142
|
+
sentAt: null,
|
|
143
|
+
failCount: 1,
|
|
144
|
+
failErrorMessage: 'All recipients are filtered due to environment',
|
|
145
|
+
firstFailedAt: expect.any(Date),
|
|
146
|
+
lastFailedAt: expect.any(Date),
|
|
147
|
+
}),
|
|
148
|
+
]);
|
|
149
|
+
}, 15_000);
|
|
150
|
+
|
|
151
|
+
it('Retries emails on network errors', async () => {
|
|
152
|
+
const model = await buildEmail({
|
|
153
|
+
recipients: [
|
|
154
|
+
EmailRecipientStruct.create({
|
|
155
|
+
firstName: 'Test',
|
|
156
|
+
lastName: 'Test',
|
|
157
|
+
email: 'example@domain.be',
|
|
158
|
+
}),
|
|
159
|
+
EmailRecipientStruct.create({
|
|
160
|
+
firstName: 'Test',
|
|
161
|
+
lastName: 'Test',
|
|
162
|
+
email: 'example2@domain.be',
|
|
163
|
+
}),
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Only one failure (the first email)
|
|
168
|
+
// It should automatically retry to send the email
|
|
169
|
+
EmailMocker.broadcast.failNext(new Error('This is a simulated network error'));
|
|
170
|
+
|
|
171
|
+
await EmailSendService.queueForSending(model, true);
|
|
172
|
+
await model.refresh();
|
|
173
|
+
|
|
174
|
+
// Check if it was sent correctly
|
|
175
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
176
|
+
expect(model.emailRecipientsCount).toBe(2);
|
|
177
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
178
|
+
expect(model.succeededCount).toBe(2);
|
|
179
|
+
expect(model.failedCount).toBe(0);
|
|
180
|
+
expect(model.softFailedCount).toBe(0);
|
|
181
|
+
|
|
182
|
+
// Both have succeeded
|
|
183
|
+
expect(await EmailMocker.getSucceededCount()).toBe(2);
|
|
184
|
+
expect(await EmailMocker.getFailedCount()).toBe(1); // One retry
|
|
185
|
+
|
|
186
|
+
// Load recipietns
|
|
187
|
+
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
188
|
+
expect(recipients).toIncludeSameMembers([
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
email: 'example@domain.be',
|
|
191
|
+
sentAt: expect.any(Date),
|
|
192
|
+
failCount: 0,
|
|
193
|
+
}),
|
|
194
|
+
expect.objectContaining({
|
|
195
|
+
email: 'example2@domain.be',
|
|
196
|
+
sentAt: expect.any(Date),
|
|
197
|
+
failCount: 0,
|
|
198
|
+
}),
|
|
199
|
+
]);
|
|
200
|
+
}, 15_000);
|
|
201
|
+
|
|
202
|
+
it('Email changes to failed state if email recipients fails to build', async () => {
|
|
203
|
+
const model = await buildFailEmail({
|
|
204
|
+
recipients: [
|
|
205
|
+
EmailRecipientStruct.create({
|
|
206
|
+
firstName: 'Test',
|
|
207
|
+
lastName: 'Test',
|
|
208
|
+
email: 'example@domain.be',
|
|
209
|
+
}),
|
|
210
|
+
EmailRecipientStruct.create({
|
|
211
|
+
firstName: 'Test',
|
|
212
|
+
lastName: 'Test',
|
|
213
|
+
email: 'example2@domain.be',
|
|
214
|
+
}),
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await expect(EmailSendService.queueForSending(model, true)).rejects.toThrow();
|
|
219
|
+
await model.refresh();
|
|
220
|
+
|
|
221
|
+
// Check if it was sent correctly
|
|
222
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.NotCreated);
|
|
223
|
+
expect(model.recipientsErrors).toEqual(STExpect.simpleErrors([{
|
|
224
|
+
code: 'unknown_error',
|
|
225
|
+
message: 'This is a simulated error while fetching recipients',
|
|
226
|
+
}]));
|
|
227
|
+
|
|
228
|
+
expect(model.emailRecipientsCount).toBe(null);
|
|
229
|
+
expect(model.status).toBe(EmailStatus.Failed);
|
|
230
|
+
expect(model.succeededCount).toBe(0);
|
|
231
|
+
expect(model.failedCount).toBe(0);
|
|
232
|
+
expect(model.softFailedCount).toBe(0);
|
|
233
|
+
|
|
234
|
+
// Both have succeeded
|
|
235
|
+
expect(await EmailMocker.getSucceededCount()).toBe(0);
|
|
236
|
+
expect(await EmailMocker.getFailedCount()).toBe(0); // One retry
|
|
237
|
+
}, 15_000);
|
|
238
|
+
|
|
239
|
+
it('Marks email recipient as failed if fails three times', async () => {
|
|
240
|
+
const model = await buildEmail({
|
|
241
|
+
recipients: [
|
|
242
|
+
EmailRecipientStruct.create({
|
|
243
|
+
firstName: 'Test',
|
|
244
|
+
lastName: 'Test',
|
|
245
|
+
email: 'example@domain.be',
|
|
246
|
+
}),
|
|
247
|
+
EmailRecipientStruct.create({
|
|
248
|
+
firstName: 'Test',
|
|
249
|
+
lastName: 'Test',
|
|
250
|
+
email: 'example2@domain.be',
|
|
251
|
+
}),
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
EmailMocker.broadcast.failNext(new Error('This is a simulated network error 1'));
|
|
256
|
+
EmailMocker.broadcast.failNext(new Error('This is a simulated network error 2'));
|
|
257
|
+
EmailMocker.broadcast.failNext(new Error('This is a simulated network error 3'));
|
|
258
|
+
EmailMocker.broadcast.failNext(new Error('This is a simulated network error 4'));
|
|
259
|
+
EmailMocker.broadcast.failNext(new Error('This is a simulated network error 5'));
|
|
260
|
+
EmailMocker.broadcast.failNext(new Error('This is a simulated network error 6'));
|
|
261
|
+
|
|
262
|
+
await EmailSendService.queueForSending(model, true);
|
|
263
|
+
await model.refresh();
|
|
264
|
+
|
|
265
|
+
// Check if it was sent correctly
|
|
266
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
267
|
+
expect(model.emailRecipientsCount).toBe(2);
|
|
268
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
269
|
+
expect(model.succeededCount).toBe(0);
|
|
270
|
+
expect(model.failedCount).toBe(2);
|
|
271
|
+
expect(model.softFailedCount).toBe(0);
|
|
272
|
+
|
|
273
|
+
// Both have succeeded
|
|
274
|
+
expect(await EmailMocker.getSucceededCount()).toBe(0);
|
|
275
|
+
expect(await EmailMocker.getFailedCount()).toBe(6); // Two retries for each recipient
|
|
276
|
+
|
|
277
|
+
// Load recipietns
|
|
278
|
+
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
279
|
+
expect(recipients).toIncludeSameMembers([
|
|
280
|
+
expect.objectContaining({
|
|
281
|
+
email: 'example@domain.be',
|
|
282
|
+
sentAt: null,
|
|
283
|
+
failCount: 1,
|
|
284
|
+
failError: STExpect.simpleErrors([{
|
|
285
|
+
message: /This is a simulated network error ([3-6])/,
|
|
286
|
+
}]),
|
|
287
|
+
firstFailedAt: expect.any(Date),
|
|
288
|
+
lastFailedAt: expect.any(Date),
|
|
289
|
+
}),
|
|
290
|
+
expect.objectContaining({
|
|
291
|
+
email: 'example2@domain.be',
|
|
292
|
+
sentAt: null,
|
|
293
|
+
failCount: 1,
|
|
294
|
+
failError: STExpect.simpleErrors([{
|
|
295
|
+
message: /This is a simulated network error ([3-6])/,
|
|
296
|
+
}]),
|
|
297
|
+
firstFailedAt: expect.any(Date),
|
|
298
|
+
lastFailedAt: expect.any(Date),
|
|
299
|
+
}),
|
|
300
|
+
]);
|
|
301
|
+
}, 15_000);
|
|
302
|
+
|
|
303
|
+
it('Can handle recipients without name', async () => {
|
|
304
|
+
const model = await buildEmail({
|
|
305
|
+
recipients: [
|
|
306
|
+
EmailRecipientStruct.create({
|
|
307
|
+
email: 'example@domain.be',
|
|
308
|
+
}),
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
await EmailSendService.queueForSending(model, true);
|
|
313
|
+
await model.refresh();
|
|
314
|
+
|
|
315
|
+
// Check if it was sent correctly
|
|
316
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
317
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
318
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
319
|
+
expect(model.succeededCount).toBe(1);
|
|
320
|
+
expect(model.failedCount).toBe(0);
|
|
321
|
+
expect(model.softFailedCount).toBe(0);
|
|
322
|
+
|
|
323
|
+
// Both have succeeded
|
|
324
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
325
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
326
|
+
|
|
327
|
+
// Load recipietns
|
|
328
|
+
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
329
|
+
expect(recipients).toIncludeSameMembers([
|
|
330
|
+
expect.objectContaining({
|
|
331
|
+
email: 'example@domain.be',
|
|
332
|
+
sentAt: expect.any(Date),
|
|
333
|
+
failCount: 0,
|
|
334
|
+
}),
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
// Check to header
|
|
338
|
+
expect(EmailMocker.getSucceededEmail(0).to).toEqual('example@domain.be');
|
|
339
|
+
}, 15_000);
|
|
340
|
+
|
|
341
|
+
it('Includes recipient names in mail header', async () => {
|
|
342
|
+
const model = await buildEmail({
|
|
343
|
+
recipients: [
|
|
344
|
+
EmailRecipientStruct.create({
|
|
345
|
+
firstName: 'John',
|
|
346
|
+
lastName: 'Von Doe',
|
|
347
|
+
email: 'example@domain.be',
|
|
348
|
+
}),
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await EmailSendService.queueForSending(model, true);
|
|
353
|
+
await model.refresh();
|
|
354
|
+
|
|
355
|
+
// Check if it was sent correctly
|
|
356
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
357
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
358
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
359
|
+
expect(model.succeededCount).toBe(1);
|
|
360
|
+
expect(model.failedCount).toBe(0);
|
|
361
|
+
expect(model.softFailedCount).toBe(0);
|
|
362
|
+
|
|
363
|
+
// Both have succeeded
|
|
364
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
365
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
366
|
+
|
|
367
|
+
// Load recipietns
|
|
368
|
+
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
369
|
+
expect(recipients).toIncludeSameMembers([
|
|
370
|
+
expect.objectContaining({
|
|
371
|
+
firstName: 'John',
|
|
372
|
+
lastName: 'Von Doe',
|
|
373
|
+
email: 'example@domain.be',
|
|
374
|
+
sentAt: expect.any(Date),
|
|
375
|
+
failCount: 0,
|
|
376
|
+
}),
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
// Check to header
|
|
380
|
+
expect(EmailMocker.getSucceededEmail(0).to).toEqual('"John Von Doe" <example@domain.be>');
|
|
381
|
+
}, 15_000);
|
|
382
|
+
|
|
383
|
+
it('Skips invalid email addresses', async () => {
|
|
384
|
+
const model = await buildEmail({
|
|
385
|
+
recipients: [
|
|
386
|
+
EmailRecipientStruct.create({
|
|
387
|
+
firstName: 'John',
|
|
388
|
+
lastName: 'Von Doe',
|
|
389
|
+
email: 'invalid',
|
|
390
|
+
}),
|
|
391
|
+
],
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await EmailSendService.queueForSending(model, true);
|
|
395
|
+
await model.refresh();
|
|
396
|
+
|
|
397
|
+
// Check if it was sent correctly
|
|
398
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
399
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
400
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
401
|
+
expect(model.succeededCount).toBe(0);
|
|
402
|
+
expect(model.failedCount).toBe(1);
|
|
403
|
+
expect(model.softFailedCount).toBe(0);
|
|
404
|
+
|
|
405
|
+
// Both have succeeded
|
|
406
|
+
expect(await EmailMocker.getSucceededCount()).toBe(0);
|
|
407
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
408
|
+
|
|
409
|
+
// Load recipietns
|
|
410
|
+
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
411
|
+
expect(recipients).toIncludeSameMembers([
|
|
412
|
+
expect.objectContaining({
|
|
413
|
+
firstName: 'John',
|
|
414
|
+
lastName: 'Von Doe',
|
|
415
|
+
email: 'invalid',
|
|
416
|
+
failCount: 1,
|
|
417
|
+
failError: STExpect.simpleErrors([{
|
|
418
|
+
code: 'invalid_email_address',
|
|
419
|
+
message: 'Invalid email address',
|
|
420
|
+
}]),
|
|
421
|
+
firstFailedAt: expect.any(Date),
|
|
422
|
+
lastFailedAt: expect.any(Date),
|
|
423
|
+
}),
|
|
424
|
+
]);
|
|
425
|
+
}, 15_000);
|
|
426
|
+
|
|
427
|
+
it('Skips empty email addresses while creating recipients', async () => {
|
|
428
|
+
const model = await buildEmail({
|
|
429
|
+
recipients: [
|
|
430
|
+
EmailRecipientStruct.create({
|
|
431
|
+
firstName: '',
|
|
432
|
+
lastName: '',
|
|
433
|
+
email: '',
|
|
434
|
+
}),
|
|
435
|
+
EmailRecipientStruct.create({
|
|
436
|
+
firstName: '',
|
|
437
|
+
lastName: '',
|
|
438
|
+
email: 'valid@example.com',
|
|
439
|
+
}),
|
|
440
|
+
],
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await EmailSendService.queueForSending(model, true);
|
|
444
|
+
await model.refresh();
|
|
445
|
+
|
|
446
|
+
// Check if it was sent correctly
|
|
447
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
448
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
449
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
450
|
+
expect(model.succeededCount).toBe(1);
|
|
451
|
+
expect(model.failedCount).toBe(0);
|
|
452
|
+
expect(model.softFailedCount).toBe(0);
|
|
453
|
+
|
|
454
|
+
// Both have succeeded
|
|
455
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
456
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
457
|
+
|
|
458
|
+
// Load recipietns
|
|
459
|
+
const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
|
|
460
|
+
expect(recipients).toIncludeSameMembers([
|
|
461
|
+
expect.objectContaining({
|
|
462
|
+
email: 'valid@example.com',
|
|
463
|
+
sentAt: expect.any(Date),
|
|
464
|
+
failError: null,
|
|
465
|
+
}),
|
|
466
|
+
]);
|
|
467
|
+
}, 15_000);
|
|
468
|
+
|
|
469
|
+
it('Platform can send from default domain', async () => {
|
|
470
|
+
TestUtils.setEnvironment('domains', {
|
|
471
|
+
...STAMHOOFD.domains,
|
|
472
|
+
defaultTransactionalEmail: {
|
|
473
|
+
'': 'my-platform.com',
|
|
474
|
+
},
|
|
475
|
+
defaultBroadcastEmail: {
|
|
476
|
+
'': 'broadcast.my-platform.com',
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const model = await buildEmail({
|
|
481
|
+
recipients: [
|
|
482
|
+
EmailRecipientStruct.create({
|
|
483
|
+
email: 'example@domain.be',
|
|
484
|
+
}),
|
|
485
|
+
],
|
|
486
|
+
fromAddress: 'info@my-platform.com',
|
|
487
|
+
fromName: 'My Platform',
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await EmailSendService.queueForSending(model, true);
|
|
491
|
+
await model.refresh();
|
|
492
|
+
|
|
493
|
+
// Check if it was sent correctly
|
|
494
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
495
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
496
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
497
|
+
expect(model.succeededCount).toBe(1);
|
|
498
|
+
expect(model.failedCount).toBe(0);
|
|
499
|
+
expect(model.softFailedCount).toBe(0);
|
|
500
|
+
|
|
501
|
+
// Both have succeeded
|
|
502
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
503
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
504
|
+
|
|
505
|
+
// Check to header
|
|
506
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
507
|
+
to: 'example@domain.be',
|
|
508
|
+
from: '"My Platform" <info@my-platform.com>',
|
|
509
|
+
replyTo: undefined,
|
|
510
|
+
});
|
|
511
|
+
}, 15_000);
|
|
512
|
+
|
|
513
|
+
it('Platform cannot send from unsupported domain', async () => {
|
|
514
|
+
TestUtils.setEnvironment('domains', {
|
|
515
|
+
...STAMHOOFD.domains,
|
|
516
|
+
defaultTransactionalEmail: {
|
|
517
|
+
'': 'my-platform.com',
|
|
518
|
+
},
|
|
519
|
+
defaultBroadcastEmail: {
|
|
520
|
+
'': 'broadcast.my-platform.com',
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const model = await buildEmail({
|
|
525
|
+
recipients: [
|
|
526
|
+
EmailRecipientStruct.create({
|
|
527
|
+
email: 'example@domain.be',
|
|
528
|
+
}),
|
|
529
|
+
],
|
|
530
|
+
fromAddress: 'info@other-platform.com',
|
|
531
|
+
fromName: 'My Platform',
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
await EmailSendService.queueForSending(model, true);
|
|
535
|
+
await model.refresh();
|
|
536
|
+
|
|
537
|
+
// Check if it was sent correctly
|
|
538
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
539
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
540
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
541
|
+
expect(model.succeededCount).toBe(1);
|
|
542
|
+
expect(model.failedCount).toBe(0);
|
|
543
|
+
expect(model.softFailedCount).toBe(0);
|
|
544
|
+
|
|
545
|
+
// Both have succeeded
|
|
546
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
547
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
548
|
+
|
|
549
|
+
// Check to header
|
|
550
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
551
|
+
to: 'example@domain.be',
|
|
552
|
+
from: '"My Platform" <noreply@broadcast.my-platform.com>', // domain has changed here
|
|
553
|
+
replyTo: '"My Platform" <info@other-platform.com>', // Reply to should be set
|
|
554
|
+
});
|
|
555
|
+
}, 15_000);
|
|
556
|
+
|
|
557
|
+
it('Organization cannot send from platform domain', async () => {
|
|
558
|
+
TestUtils.setEnvironment('domains', {
|
|
559
|
+
...STAMHOOFD.domains,
|
|
560
|
+
defaultTransactionalEmail: {
|
|
561
|
+
'': 'my-platform.com',
|
|
562
|
+
},
|
|
563
|
+
defaultBroadcastEmail: {
|
|
564
|
+
'': 'broadcast.my-platform.com',
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const organization = await new OrganizationFactory({
|
|
569
|
+
uri: 'uritest',
|
|
570
|
+
}).create();
|
|
571
|
+
|
|
572
|
+
const model = await buildEmail({
|
|
573
|
+
organizationId: organization.id,
|
|
574
|
+
recipients: [
|
|
575
|
+
EmailRecipientStruct.create({
|
|
576
|
+
email: 'example@domain.be',
|
|
577
|
+
}),
|
|
578
|
+
],
|
|
579
|
+
fromAddress: 'info@my-platform.com',
|
|
580
|
+
fromName: 'My Platform',
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await EmailSendService.queueForSending(model, true);
|
|
584
|
+
await model.refresh();
|
|
585
|
+
|
|
586
|
+
// Check if it was sent correctly
|
|
587
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
588
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
589
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
590
|
+
expect(model.succeededCount).toBe(1);
|
|
591
|
+
expect(model.failedCount).toBe(0);
|
|
592
|
+
expect(model.softFailedCount).toBe(0);
|
|
593
|
+
|
|
594
|
+
// Both have succeeded
|
|
595
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
596
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
597
|
+
|
|
598
|
+
// Check to header
|
|
599
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
600
|
+
to: 'example@domain.be',
|
|
601
|
+
from: '"My Platform" <noreply-uritest@broadcast.my-platform.com>', // domain has changed here
|
|
602
|
+
replyTo: '"My Platform" <info@my-platform.com>', // Reply to should be set
|
|
603
|
+
});
|
|
604
|
+
}, 15_000);
|
|
605
|
+
|
|
606
|
+
it('Organization can send from platform domain if default membership organization', async () => {
|
|
607
|
+
TestUtils.setEnvironment('domains', {
|
|
608
|
+
...STAMHOOFD.domains,
|
|
609
|
+
defaultTransactionalEmail: {
|
|
610
|
+
'': 'my-platform.com',
|
|
611
|
+
},
|
|
612
|
+
defaultBroadcastEmail: {
|
|
613
|
+
'': 'broadcast.my-platform.com',
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const organization = await new OrganizationFactory({}).create();
|
|
618
|
+
|
|
619
|
+
const platform = await Platform.getForEditing();
|
|
620
|
+
platform.membershipOrganizationId = organization.id;
|
|
621
|
+
await platform.save();
|
|
622
|
+
|
|
623
|
+
const model = await buildEmail({
|
|
624
|
+
organizationId: organization.id,
|
|
625
|
+
recipients: [
|
|
626
|
+
EmailRecipientStruct.create({
|
|
627
|
+
email: 'example@domain.be',
|
|
628
|
+
}),
|
|
629
|
+
],
|
|
630
|
+
fromAddress: 'info@my-platform.com',
|
|
631
|
+
fromName: 'My Platform',
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
await EmailSendService.queueForSending(model, true);
|
|
635
|
+
await model.refresh();
|
|
636
|
+
|
|
637
|
+
// Check if it was sent correctly
|
|
638
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
639
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
640
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
641
|
+
expect(model.succeededCount).toBe(1);
|
|
642
|
+
expect(model.failedCount).toBe(0);
|
|
643
|
+
expect(model.softFailedCount).toBe(0);
|
|
644
|
+
|
|
645
|
+
// Both have succeeded
|
|
646
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
647
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
648
|
+
|
|
649
|
+
// Check to header
|
|
650
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
651
|
+
to: 'example@domain.be',
|
|
652
|
+
from: '"My Platform" <info@my-platform.com>', // domain has changed here
|
|
653
|
+
replyTo: undefined,
|
|
654
|
+
});
|
|
655
|
+
}, 15_000);
|
|
656
|
+
|
|
657
|
+
describe('Replacements', () => {
|
|
658
|
+
it('[Regression] When sending an email the organization fromAddress is replaced to the reply-to if it is not allowed to be sent from', async () => {
|
|
659
|
+
TestUtils.setEnvironment('domains', {
|
|
660
|
+
...STAMHOOFD.domains,
|
|
661
|
+
defaultTransactionalEmail: {
|
|
662
|
+
'': 'my-platform.com',
|
|
663
|
+
},
|
|
664
|
+
defaultBroadcastEmail: {
|
|
665
|
+
'': 'broadcast.my-platform.com',
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const organization = await new OrganizationFactory({}).create();
|
|
670
|
+
|
|
671
|
+
const model = await buildEmail({
|
|
672
|
+
organizationId: organization.id,
|
|
673
|
+
recipients: [
|
|
674
|
+
EmailRecipientStruct.create({
|
|
675
|
+
email: 'example@domain.be',
|
|
676
|
+
}),
|
|
677
|
+
],
|
|
678
|
+
fromAddress: 'custom@customdomain.com',
|
|
679
|
+
fromName: 'Custom Name',
|
|
680
|
+
html: '{{fromAddress}}',
|
|
681
|
+
text: '{{fromAddress}}',
|
|
682
|
+
subject: '{{fromAddress}}',
|
|
683
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
await EmailSendService.queueForSending(model, true);
|
|
687
|
+
await model.refresh();
|
|
688
|
+
|
|
689
|
+
// Check if it was sent correctly
|
|
690
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
691
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
692
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
693
|
+
|
|
694
|
+
// Both have succeeded
|
|
695
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
696
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
697
|
+
|
|
698
|
+
// Check to header
|
|
699
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
700
|
+
to: 'example@domain.be',
|
|
701
|
+
from: '"Custom Name" <noreply-' + organization.uri + '@broadcast.my-platform.com>',
|
|
702
|
+
replyTo: '"Custom Name" <custom@customdomain.com>', // domain has changed here
|
|
703
|
+
subject: 'custom@customdomain.com',
|
|
704
|
+
html: 'custom@customdomain.com',
|
|
705
|
+
text: 'custom@customdomain.com',
|
|
706
|
+
});
|
|
707
|
+
}, 15_000);
|
|
708
|
+
|
|
709
|
+
it('Default organization replacements work as expected', async () => {
|
|
710
|
+
TestUtils.setEnvironment('domains', {
|
|
711
|
+
...STAMHOOFD.domains,
|
|
712
|
+
defaultTransactionalEmail: {
|
|
713
|
+
'': 'my-platform.com',
|
|
714
|
+
},
|
|
715
|
+
defaultBroadcastEmail: {
|
|
716
|
+
'': 'broadcast.my-platform.com',
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const brightYellow = '#FFD700';
|
|
721
|
+
const expectedContrastColor = '#000'; // black
|
|
722
|
+
|
|
723
|
+
const organization = await new OrganizationFactory({
|
|
724
|
+
meta: OrganizationMetaData.create({
|
|
725
|
+
color: brightYellow,
|
|
726
|
+
}),
|
|
727
|
+
}).create();
|
|
728
|
+
|
|
729
|
+
const model = await buildEmail({
|
|
730
|
+
organizationId: organization.id,
|
|
731
|
+
recipients: [
|
|
732
|
+
EmailRecipientStruct.create({
|
|
733
|
+
email: 'example@domain.be',
|
|
734
|
+
}),
|
|
735
|
+
],
|
|
736
|
+
fromAddress: 'custom@customdomain.com',
|
|
737
|
+
html: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
738
|
+
text: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
739
|
+
subject: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
740
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
await EmailSendService.queueForSending(model, true);
|
|
744
|
+
await model.refresh();
|
|
745
|
+
|
|
746
|
+
// Check if it was sent correctly
|
|
747
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
748
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
749
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
750
|
+
|
|
751
|
+
// Both have succeeded
|
|
752
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
753
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
754
|
+
|
|
755
|
+
// Check to header
|
|
756
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
757
|
+
subject: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
758
|
+
html: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
759
|
+
text: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
760
|
+
});
|
|
761
|
+
}, 15_000);
|
|
762
|
+
|
|
763
|
+
it('Organization replacements default to platform replacements if not set', async () => {
|
|
764
|
+
TestUtils.setEnvironment('domains', {
|
|
765
|
+
...STAMHOOFD.domains,
|
|
766
|
+
defaultTransactionalEmail: {
|
|
767
|
+
'': 'my-platform.com',
|
|
768
|
+
},
|
|
769
|
+
defaultBroadcastEmail: {
|
|
770
|
+
'': 'broadcast.my-platform.com',
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
const brightBlue = '#0000FF';
|
|
775
|
+
const expectedContrastColor = '#fff'; // white
|
|
776
|
+
|
|
777
|
+
const platform = await Platform.getForEditing();
|
|
778
|
+
platform.config.color = brightBlue;
|
|
779
|
+
await platform.save();
|
|
780
|
+
|
|
781
|
+
const organization = await new OrganizationFactory({
|
|
782
|
+
meta: OrganizationMetaData.create({
|
|
783
|
+
color: null,
|
|
784
|
+
}),
|
|
785
|
+
}).create();
|
|
786
|
+
|
|
787
|
+
const model = await buildEmail({
|
|
788
|
+
organizationId: organization.id,
|
|
789
|
+
recipients: [
|
|
790
|
+
EmailRecipientStruct.create({
|
|
791
|
+
email: 'example@domain.be',
|
|
792
|
+
}),
|
|
793
|
+
],
|
|
794
|
+
fromAddress: 'custom@customdomain.com',
|
|
795
|
+
fromName: 'Custom Name',
|
|
796
|
+
html: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
797
|
+
text: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
798
|
+
subject: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
799
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
await EmailSendService.queueForSending(model, true);
|
|
803
|
+
await model.refresh();
|
|
804
|
+
|
|
805
|
+
// Check if it was sent correctly
|
|
806
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
807
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
808
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
809
|
+
|
|
810
|
+
// Both have succeeded
|
|
811
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
812
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
813
|
+
|
|
814
|
+
// Check to header
|
|
815
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
816
|
+
subject: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
817
|
+
html: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
818
|
+
text: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
819
|
+
});
|
|
820
|
+
}, 15_000);
|
|
821
|
+
|
|
822
|
+
it('Platform replacements are used for platform level emails', async () => {
|
|
823
|
+
TestUtils.setEnvironment('domains', {
|
|
824
|
+
...STAMHOOFD.domains,
|
|
825
|
+
defaultTransactionalEmail: {
|
|
826
|
+
'': 'my-platform.com',
|
|
827
|
+
},
|
|
828
|
+
defaultBroadcastEmail: {
|
|
829
|
+
'': 'broadcast.my-platform.com',
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const darkRed = '#8B0000';
|
|
834
|
+
const expectedContrastColor = '#fff'; // white
|
|
835
|
+
|
|
836
|
+
const platform = await Platform.getForEditing();
|
|
837
|
+
platform.config.color = darkRed;
|
|
838
|
+
await platform.save();
|
|
839
|
+
|
|
840
|
+
const model = await buildEmail({
|
|
841
|
+
recipients: [
|
|
842
|
+
EmailRecipientStruct.create({
|
|
843
|
+
email: 'example@domain.be',
|
|
844
|
+
}),
|
|
845
|
+
],
|
|
846
|
+
fromAddress: 'custom@customdomain.com',
|
|
847
|
+
html: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
848
|
+
text: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
849
|
+
subject: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
850
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
await EmailSendService.queueForSending(model, true);
|
|
854
|
+
await model.refresh();
|
|
855
|
+
|
|
856
|
+
// Check if it was sent correctly
|
|
857
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
858
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
859
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
860
|
+
|
|
861
|
+
// Both have succeeded
|
|
862
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
863
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
864
|
+
|
|
865
|
+
// Check to header
|
|
866
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
867
|
+
subject: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
|
868
|
+
html: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
|
869
|
+
text: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
|
870
|
+
});
|
|
871
|
+
}, 15_000);
|
|
872
|
+
|
|
873
|
+
describe('User based replacements', () => {
|
|
874
|
+
it('The balance is added for existing users', async () => {
|
|
875
|
+
TestUtils.setEnvironment('domains', {
|
|
876
|
+
...STAMHOOFD.domains,
|
|
877
|
+
defaultTransactionalEmail: {
|
|
878
|
+
'': 'my-platform.com',
|
|
879
|
+
},
|
|
880
|
+
defaultBroadcastEmail: {
|
|
881
|
+
'': 'broadcast.my-platform.com',
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const organization = await new OrganizationFactory({}).create();
|
|
886
|
+
const existingUser = await new UserFactory({
|
|
887
|
+
organization,
|
|
888
|
+
}).create();
|
|
889
|
+
|
|
890
|
+
// Add outstanding balance items for this user
|
|
891
|
+
const balanceItem = await new BalanceItemFactory({
|
|
892
|
+
userId: existingUser.id,
|
|
893
|
+
organizationId: organization.id,
|
|
894
|
+
type: BalanceItemType.Other,
|
|
895
|
+
amount: 2,
|
|
896
|
+
unitPrice: 12_99_00,
|
|
897
|
+
description: 'Test balance item',
|
|
898
|
+
}).create();
|
|
899
|
+
await CachedBalance.updateForUsers(organization.id, [existingUser.id]);
|
|
900
|
+
|
|
901
|
+
const model = await buildEmail({
|
|
902
|
+
organizationId: organization.id,
|
|
903
|
+
recipients: [
|
|
904
|
+
EmailRecipientStruct.create({
|
|
905
|
+
email: existingUser.email,
|
|
906
|
+
userId: existingUser.id,
|
|
907
|
+
}),
|
|
908
|
+
],
|
|
909
|
+
fromAddress: 'custom@customdomain.com',
|
|
910
|
+
html: '<p>{{outstandingBalance}}</p>{{balanceTable}}',
|
|
911
|
+
subject: '{{outstandingBalance}}',
|
|
912
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
await EmailSendService.queueForSending(model, true);
|
|
916
|
+
await model.refresh();
|
|
917
|
+
|
|
918
|
+
// Check if it was sent correctly
|
|
919
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
920
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
921
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
922
|
+
|
|
923
|
+
// Both have succeeded
|
|
924
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
925
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
926
|
+
|
|
927
|
+
const expectedAmount = Formatter.price(balanceItem.unitPrice * balanceItem.amount);
|
|
928
|
+
|
|
929
|
+
// Check to header
|
|
930
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
931
|
+
subject: `${expectedAmount}`,
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch('<p>' + expectedAmount + '</p>');
|
|
935
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch(expectedAmount);
|
|
936
|
+
|
|
937
|
+
// Check if the table is correct
|
|
938
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch('<table');
|
|
939
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch('2 x '); // amount
|
|
940
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch(Formatter.price(12_99_00)); // unit price
|
|
941
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch('<td>' + expectedAmount); // total price in table
|
|
942
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch('Test balance item'); // description
|
|
943
|
+
|
|
944
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch('2 x '); // amount
|
|
945
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch(Formatter.price(12_99_00)); // unit price
|
|
946
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch(expectedAmount); // total price in table
|
|
947
|
+
expect(EmailMocker.getSucceededEmail(0).text?.toLowerCase()).toMatch('test balance item'); // description
|
|
948
|
+
}, 15_000);
|
|
949
|
+
|
|
950
|
+
it('The balance is zero for unknown users', async () => {
|
|
951
|
+
TestUtils.setEnvironment('domains', {
|
|
952
|
+
...STAMHOOFD.domains,
|
|
953
|
+
defaultTransactionalEmail: {
|
|
954
|
+
'': 'my-platform.com',
|
|
955
|
+
},
|
|
956
|
+
defaultBroadcastEmail: {
|
|
957
|
+
'': 'broadcast.my-platform.com',
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
const organization = await new OrganizationFactory({}).create();
|
|
962
|
+
|
|
963
|
+
const model = await buildEmail({
|
|
964
|
+
organizationId: organization.id,
|
|
965
|
+
recipients: [
|
|
966
|
+
EmailRecipientStruct.create({
|
|
967
|
+
email: 'unknown-user@example.com',
|
|
968
|
+
}),
|
|
969
|
+
],
|
|
970
|
+
fromAddress: 'custom@customdomain.com',
|
|
971
|
+
html: '<p>{{outstandingBalance}}</p>{{balanceTable}}',
|
|
972
|
+
subject: '{{outstandingBalance}}',
|
|
973
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
await EmailSendService.queueForSending(model, true);
|
|
977
|
+
await model.refresh();
|
|
978
|
+
|
|
979
|
+
// Check if it was sent correctly
|
|
980
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
981
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
982
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
983
|
+
|
|
984
|
+
// Both have succeeded
|
|
985
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
986
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
987
|
+
|
|
988
|
+
const expectedAmount = Formatter.price(0);
|
|
989
|
+
|
|
990
|
+
// Check to header
|
|
991
|
+
expect(EmailMocker.getSucceededEmail(0)).toMatchObject({
|
|
992
|
+
subject: `${expectedAmount}`,
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch('<p>' + expectedAmount + '</p>');
|
|
996
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch(expectedAmount);
|
|
997
|
+
|
|
998
|
+
// Check if the table is correct
|
|
999
|
+
expect(EmailMocker.getSucceededEmail(0).html).not.toMatch('<table');
|
|
1000
|
+
expect(EmailMocker.getSucceededEmail(0).html).not.toMatch(' x '); // amount
|
|
1001
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch('<p class="description">' + $t('%hX') + '</p>');
|
|
1002
|
+
|
|
1003
|
+
expect(EmailMocker.getSucceededEmail(0).text).not.toMatch(' x '); // amount
|
|
1004
|
+
expect(EmailMocker.getSucceededEmail(0).text?.toLowerCase()).toMatch($t('%hX').toLowerCase());
|
|
1005
|
+
}, 15_000);
|
|
1006
|
+
|
|
1007
|
+
it('loginDetails are added for existing users without password', async () => {
|
|
1008
|
+
TestUtils.setEnvironment('domains', {
|
|
1009
|
+
...STAMHOOFD.domains,
|
|
1010
|
+
defaultTransactionalEmail: {
|
|
1011
|
+
'': 'my-platform.com',
|
|
1012
|
+
},
|
|
1013
|
+
defaultBroadcastEmail: {
|
|
1014
|
+
'': 'broadcast.my-platform.com',
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
const organization = await new OrganizationFactory({}).create();
|
|
1019
|
+
const existingUser = await new UserFactory({
|
|
1020
|
+
organization,
|
|
1021
|
+
password: null,
|
|
1022
|
+
}).create();
|
|
1023
|
+
|
|
1024
|
+
const model = await buildEmail({
|
|
1025
|
+
organizationId: organization.id,
|
|
1026
|
+
recipients: [
|
|
1027
|
+
EmailRecipientStruct.create({
|
|
1028
|
+
email: existingUser.email,
|
|
1029
|
+
userId: existingUser.id,
|
|
1030
|
+
}),
|
|
1031
|
+
],
|
|
1032
|
+
fromAddress: 'custom@customdomain.com',
|
|
1033
|
+
html: '{{loginDetails}}',
|
|
1034
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
await EmailSendService.queueForSending(model, true);
|
|
1038
|
+
await model.refresh();
|
|
1039
|
+
|
|
1040
|
+
// Check if it was sent correctly
|
|
1041
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
1042
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
1043
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
1044
|
+
|
|
1045
|
+
// Both have succeeded
|
|
1046
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
1047
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
1048
|
+
|
|
1049
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch(
|
|
1050
|
+
$t('%1EB', { email: `<strong>${Formatter.escapeHtml(existingUser.email)}</strong>` }),
|
|
1051
|
+
);
|
|
1052
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch(
|
|
1053
|
+
$t('%1EB', { email: existingUser.email }),
|
|
1054
|
+
);
|
|
1055
|
+
}, 15_000);
|
|
1056
|
+
|
|
1057
|
+
it('loginDetails are added for inexisting users', async () => {
|
|
1058
|
+
TestUtils.setEnvironment('domains', {
|
|
1059
|
+
...STAMHOOFD.domains,
|
|
1060
|
+
defaultTransactionalEmail: {
|
|
1061
|
+
'': 'my-platform.com',
|
|
1062
|
+
},
|
|
1063
|
+
defaultBroadcastEmail: {
|
|
1064
|
+
'': 'broadcast.my-platform.com',
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
const organization = await new OrganizationFactory({}).create();
|
|
1069
|
+
|
|
1070
|
+
const model = await buildEmail({
|
|
1071
|
+
organizationId: organization.id,
|
|
1072
|
+
recipients: [
|
|
1073
|
+
EmailRecipientStruct.create({
|
|
1074
|
+
email: 'unknown@example.com',
|
|
1075
|
+
}),
|
|
1076
|
+
],
|
|
1077
|
+
fromAddress: 'custom@customdomain.com',
|
|
1078
|
+
html: '{{loginDetails}}',
|
|
1079
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
await EmailSendService.queueForSending(model, true);
|
|
1083
|
+
await model.refresh();
|
|
1084
|
+
|
|
1085
|
+
// Check if it was sent correctly
|
|
1086
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
1087
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
1088
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
1089
|
+
|
|
1090
|
+
// Both have succeeded
|
|
1091
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
1092
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
1093
|
+
|
|
1094
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch(
|
|
1095
|
+
$t('%1EB', { email: '<strong>unknown@example.com</strong>' }),
|
|
1096
|
+
);
|
|
1097
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch(
|
|
1098
|
+
$t('%1EB', { email: 'unknown@example.com' }),
|
|
1099
|
+
);
|
|
1100
|
+
}, 15_000);
|
|
1101
|
+
|
|
1102
|
+
it('loginDetails are added for existing users with password', async () => {
|
|
1103
|
+
TestUtils.setEnvironment('domains', {
|
|
1104
|
+
...STAMHOOFD.domains,
|
|
1105
|
+
defaultTransactionalEmail: {
|
|
1106
|
+
'': 'my-platform.com',
|
|
1107
|
+
},
|
|
1108
|
+
defaultBroadcastEmail: {
|
|
1109
|
+
'': 'broadcast.my-platform.com',
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const organization = await new OrganizationFactory({}).create();
|
|
1114
|
+
const existingUser = await new UserFactory({
|
|
1115
|
+
organization,
|
|
1116
|
+
password: 'existing',
|
|
1117
|
+
}).create();
|
|
1118
|
+
|
|
1119
|
+
const model = await buildEmail({
|
|
1120
|
+
organizationId: organization.id,
|
|
1121
|
+
recipients: [
|
|
1122
|
+
EmailRecipientStruct.create({
|
|
1123
|
+
email: existingUser.email,
|
|
1124
|
+
userId: existingUser.id,
|
|
1125
|
+
}),
|
|
1126
|
+
],
|
|
1127
|
+
fromAddress: 'custom@customdomain.com',
|
|
1128
|
+
html: '{{loginDetails}}',
|
|
1129
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
await EmailSendService.queueForSending(model, true);
|
|
1133
|
+
await model.refresh();
|
|
1134
|
+
|
|
1135
|
+
// Check if it was sent correctly
|
|
1136
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
1137
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
1138
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
1139
|
+
|
|
1140
|
+
// Both have succeeded
|
|
1141
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
1142
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
1143
|
+
|
|
1144
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch(
|
|
1145
|
+
$t('%1EA', { email: `<strong>${Formatter.escapeHtml(existingUser.email)}</strong>` }),
|
|
1146
|
+
);
|
|
1147
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch(
|
|
1148
|
+
$t('%1EA', { email: existingUser.email }),
|
|
1149
|
+
);
|
|
1150
|
+
}, 15_000);
|
|
1151
|
+
|
|
1152
|
+
it('loginDetails include member security keys for existing users without password', async () => {
|
|
1153
|
+
TestUtils.setEnvironment('userMode', 'platform');
|
|
1154
|
+
TestUtils.setEnvironment('domains', {
|
|
1155
|
+
...STAMHOOFD.domains,
|
|
1156
|
+
defaultTransactionalEmail: {
|
|
1157
|
+
'': 'my-platform.com',
|
|
1158
|
+
},
|
|
1159
|
+
defaultBroadcastEmail: {
|
|
1160
|
+
'': 'broadcast.my-platform.com',
|
|
1161
|
+
},
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
const organization = await new OrganizationFactory({}).create();
|
|
1165
|
+
const existingUser = await new UserFactory({
|
|
1166
|
+
organization,
|
|
1167
|
+
password: null,
|
|
1168
|
+
}).create();
|
|
1169
|
+
const member = await new MemberFactory({
|
|
1170
|
+
organization,
|
|
1171
|
+
user: existingUser,
|
|
1172
|
+
}).create();
|
|
1173
|
+
member.details.securityCode = '123456790123456'; // 16 chars
|
|
1174
|
+
await member.save();
|
|
1175
|
+
|
|
1176
|
+
// Add a linked member
|
|
1177
|
+
|
|
1178
|
+
const model = await buildEmail({
|
|
1179
|
+
organizationId: organization.id,
|
|
1180
|
+
recipients: [
|
|
1181
|
+
EmailRecipientStruct.create({
|
|
1182
|
+
email: existingUser.email,
|
|
1183
|
+
userId: existingUser.id,
|
|
1184
|
+
}),
|
|
1185
|
+
],
|
|
1186
|
+
fromAddress: 'custom@customdomain.com',
|
|
1187
|
+
html: '{{loginDetails}}',
|
|
1188
|
+
emailType: 'system-test', // Makes sure we don't need to include unsubscribeUrl
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
await EmailSendService.queueForSending(model, true);
|
|
1192
|
+
await model.refresh();
|
|
1193
|
+
|
|
1194
|
+
// Check if it was sent correctly
|
|
1195
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
1196
|
+
expect(model.emailRecipientsCount).toBe(1);
|
|
1197
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
1198
|
+
|
|
1199
|
+
// Both have succeeded
|
|
1200
|
+
expect(await EmailMocker.getSucceededCount()).toBe(1);
|
|
1201
|
+
expect(await EmailMocker.getFailedCount()).toBe(0);
|
|
1202
|
+
|
|
1203
|
+
expect(EmailMocker.getSucceededEmail(0).html).toMatch(
|
|
1204
|
+
$t('%1EC', {
|
|
1205
|
+
firstName: Formatter.escapeHtml(member.firstName),
|
|
1206
|
+
securityCode: `<span class="style-inline-code">${Formatter.escapeHtml(Formatter.spaceString(member.details.securityCode ?? '', 4, '-'))}</span>`,
|
|
1207
|
+
}),
|
|
1208
|
+
);
|
|
1209
|
+
expect(EmailMocker.getSucceededEmail(0).text).toMatch(
|
|
1210
|
+
$t('%1EC', {
|
|
1211
|
+
firstName: member.firstName,
|
|
1212
|
+
securityCode: Formatter.spaceString(member.details.securityCode ?? '', 4, '-'),
|
|
1213
|
+
}),
|
|
1214
|
+
);
|
|
1215
|
+
}, 15_000);
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
});
|