@stamhoofd/backend 2.96.3 → 2.97.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.
@@ -0,0 +1,623 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { Email, EmailRecipient, MemberFactory, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
3
+ import { EmailStatus, LimitedFilteredRequest, Replacement } from '@stamhoofd/structures';
4
+ import { TestUtils } from '@stamhoofd/test-utils';
5
+ import { testServer } from '../../../../tests/helpers/TestServer';
6
+ import { GetUserEmailsEndpoint } from './GetUserEmailsEndpoint';
7
+ import { Formatter } from '@stamhoofd/utility';
8
+
9
+ const baseUrl = `/user/email`;
10
+
11
+ describe('Endpoint.GetUserEmails', () => {
12
+ const endpoint = new GetUserEmailsEndpoint();
13
+ let period: RegistrationPeriod;
14
+ let organization: Organization;
15
+ let userToken: Token;
16
+ let user: User;
17
+ let member: any; // MemberWithRegistrations type
18
+
19
+ beforeAll(async () => {
20
+ TestUtils.setPermanentEnvironment('userMode', 'platform');
21
+
22
+ period = await new RegistrationPeriodFactory({
23
+ startDate: new Date(2023, 0, 1),
24
+ endDate: new Date(2023, 11, 31),
25
+ }).create();
26
+
27
+ organization = await new OrganizationFactory({ period })
28
+ .create();
29
+
30
+ user = await new UserFactory({
31
+ organization,
32
+ }).create();
33
+
34
+ // Create a member associated with the user
35
+ member = await new MemberFactory({
36
+ organization,
37
+ user,
38
+ }).create();
39
+
40
+ userToken = await Token.createToken(user);
41
+ });
42
+
43
+ const getUserEmails = async (query: LimitedFilteredRequest = new LimitedFilteredRequest({ limit: 10 }), token: Token = userToken, testOrganization: Organization = organization) => {
44
+ const request = Request.get({
45
+ path: baseUrl,
46
+ host: testOrganization.getApiHost(),
47
+ query,
48
+ headers: {
49
+ authorization: 'Bearer ' + token.accessToken,
50
+ },
51
+ });
52
+ return await testServer.test(endpoint, request);
53
+ };
54
+
55
+ test('Should return empty list when no emails are sent to user', async () => {
56
+ const response = await getUserEmails();
57
+ expect(response.body.results).toHaveLength(0);
58
+ });
59
+
60
+ test('Should return sent email when it is sent to user', async () => {
61
+ // Create an email
62
+ const email = new Email();
63
+ email.subject = 'Test Email Subject';
64
+ email.status = EmailStatus.Sent;
65
+ email.text = 'This is a test email content';
66
+ email.html = '<p>This is a test email content</p>';
67
+ email.json = {};
68
+ email.organizationId = organization.id;
69
+ email.showInMemberPortal = true;
70
+ email.sentAt = new Date();
71
+ await email.save();
72
+
73
+ // Create an email recipient linked to the member
74
+ const emailRecipient = new EmailRecipient();
75
+ emailRecipient.emailId = email.id;
76
+ emailRecipient.memberId = member.id;
77
+ emailRecipient.userId = user.id;
78
+ emailRecipient.email = user.email;
79
+ emailRecipient.firstName = member.details.firstName;
80
+ emailRecipient.lastName = member.details.lastName;
81
+ emailRecipient.sentAt = new Date();
82
+ await emailRecipient.save();
83
+
84
+ const response = await getUserEmails();
85
+
86
+ expect(response.body.results).toHaveLength(1);
87
+ expect(response.body.results[0].subject).toBe('Test Email Subject');
88
+ expect(response.body.results[0].id).toBe(email.id);
89
+ expect(response.body.results[0].status).toBe(EmailStatus.Sent);
90
+ });
91
+
92
+ test('Should not return draft emails', async () => {
93
+ // Create a draft email
94
+ const email = new Email();
95
+ email.subject = 'Draft Email';
96
+ email.status = EmailStatus.Draft;
97
+ email.text = 'This is a draft email';
98
+ email.html = '<p>This is a draft email</p>';
99
+ email.json = {};
100
+ email.organizationId = organization.id;
101
+ email.showInMemberPortal = true;
102
+ await email.save();
103
+
104
+ // Create an email recipient linked to the member
105
+ const emailRecipient = new EmailRecipient();
106
+ emailRecipient.emailId = email.id;
107
+ emailRecipient.memberId = member.id;
108
+ emailRecipient.userId = user.id;
109
+ emailRecipient.email = user.email;
110
+ emailRecipient.firstName = member.details.firstName;
111
+ emailRecipient.lastName = member.details.lastName;
112
+ await emailRecipient.save();
113
+
114
+ const response = await getUserEmails();
115
+
116
+ // Should not include the draft email
117
+ expect(response.body.results.find(e => e.id === email.id)).toBeUndefined();
118
+ });
119
+
120
+ test('Should not return emails with showInMemberPortal = false', async () => {
121
+ // Create an email with showInMemberPortal = false
122
+ const email = new Email();
123
+ email.subject = 'Hidden Email';
124
+ email.status = EmailStatus.Sent;
125
+ email.text = 'This email should be hidden';
126
+ email.html = '<p>This email should be hidden</p>';
127
+ email.json = {};
128
+ email.organizationId = organization.id;
129
+ email.showInMemberPortal = false; // This should hide the email
130
+ email.sentAt = new Date();
131
+ await email.save();
132
+
133
+ // Create an email recipient linked to the member
134
+ const emailRecipient = new EmailRecipient();
135
+ emailRecipient.emailId = email.id;
136
+ emailRecipient.memberId = member.id;
137
+ emailRecipient.userId = user.id;
138
+ emailRecipient.email = user.email;
139
+ emailRecipient.firstName = member.details.firstName;
140
+ emailRecipient.lastName = member.details.lastName;
141
+ emailRecipient.sentAt = new Date();
142
+ await emailRecipient.save();
143
+
144
+ const response = await getUserEmails();
145
+
146
+ // Should not include the hidden email
147
+ expect(response.body.results.find(e => e.id === email.id)).toBeUndefined();
148
+ });
149
+
150
+ test('Should not return emails sent to other users', async () => {
151
+ // Create another user and member
152
+ const otherUser = await new UserFactory({
153
+ organization,
154
+ }).create();
155
+
156
+ const otherMember = await new MemberFactory({
157
+ organization,
158
+ user: otherUser,
159
+ }).create();
160
+
161
+ // Create an email
162
+ const email = new Email();
163
+ email.subject = 'Email for Other User';
164
+ email.status = EmailStatus.Sent;
165
+ email.text = 'This email is for another user';
166
+ email.html = '<p>This email is for another user</p>';
167
+ email.json = {};
168
+ email.organizationId = organization.id;
169
+ email.showInMemberPortal = true;
170
+ email.sentAt = new Date();
171
+ await email.save();
172
+
173
+ // Create an email recipient linked to the OTHER member
174
+ const emailRecipient = new EmailRecipient();
175
+ emailRecipient.emailId = email.id;
176
+ emailRecipient.memberId = otherMember.id;
177
+ emailRecipient.userId = otherUser.id;
178
+ emailRecipient.email = otherUser.email;
179
+ emailRecipient.firstName = otherMember.details.firstName;
180
+ emailRecipient.lastName = otherMember.details.lastName;
181
+ emailRecipient.sentAt = new Date();
182
+ await emailRecipient.save();
183
+
184
+ const response = await getUserEmails();
185
+
186
+ // Should not include emails sent to other users
187
+ expect(response.body.results.find(e => e.id === email.id)).toBeUndefined();
188
+ });
189
+
190
+ test('Should not return deleted emails', async () => {
191
+ // Create a deleted email
192
+ const email = new Email();
193
+ email.subject = 'Deleted Email';
194
+ email.status = EmailStatus.Sent;
195
+ email.text = 'This email has been deleted';
196
+ email.html = '<p>This email has been deleted</p>';
197
+ email.json = {};
198
+ email.organizationId = organization.id;
199
+ email.showInMemberPortal = true;
200
+ email.sentAt = new Date();
201
+ email.deletedAt = new Date(); // Mark as deleted
202
+ await email.save();
203
+
204
+ // Create an email recipient linked to the member
205
+ const emailRecipient = new EmailRecipient();
206
+ emailRecipient.emailId = email.id;
207
+ emailRecipient.memberId = member.id;
208
+ emailRecipient.userId = user.id;
209
+ emailRecipient.email = user.email;
210
+ emailRecipient.firstName = member.details.firstName;
211
+ emailRecipient.lastName = member.details.lastName;
212
+ emailRecipient.sentAt = new Date();
213
+ await emailRecipient.save();
214
+
215
+ const response = await getUserEmails();
216
+
217
+ // Should not include the deleted email
218
+ expect(response.body.results.find(e => e.id === email.id)).toBeUndefined();
219
+ });
220
+
221
+ test('Should filter emails by subject when search is provided', async () => {
222
+ // Create first email
223
+ const email1 = new Email();
224
+ email1.subject = 'Important Meeting Reminder';
225
+ email1.status = EmailStatus.Sent;
226
+ email1.text = 'Meeting content';
227
+ email1.html = '<p>Meeting content</p>';
228
+ email1.json = {};
229
+ email1.organizationId = organization.id;
230
+ email1.showInMemberPortal = true;
231
+ email1.sentAt = new Date();
232
+ await email1.save();
233
+
234
+ // Create second email
235
+ const email2 = new Email();
236
+ email2.subject = 'Newsletter Update';
237
+ email2.status = EmailStatus.Sent;
238
+ email2.text = 'Newsletter content';
239
+ email2.html = '<p>Newsletter content</p>';
240
+ email2.json = {};
241
+ email2.organizationId = organization.id;
242
+ email2.showInMemberPortal = true;
243
+ email2.sentAt = new Date();
244
+ await email2.save();
245
+
246
+ // Create recipients for both emails
247
+ const recipient1 = new EmailRecipient();
248
+ recipient1.emailId = email1.id;
249
+ recipient1.memberId = member.id;
250
+ recipient1.userId = user.id;
251
+ recipient1.email = user.email;
252
+ recipient1.firstName = member.details.firstName;
253
+ recipient1.lastName = member.details.lastName;
254
+ recipient1.sentAt = new Date();
255
+ await recipient1.save();
256
+
257
+ const recipient2 = new EmailRecipient();
258
+ recipient2.emailId = email2.id;
259
+ recipient2.memberId = member.id;
260
+ recipient2.userId = user.id;
261
+ recipient2.email = user.email;
262
+ recipient2.firstName = member.details.firstName;
263
+ recipient2.lastName = member.details.lastName;
264
+ recipient2.sentAt = new Date();
265
+ await recipient2.save();
266
+
267
+ // Search for "Meeting"
268
+ const searchQuery = new LimitedFilteredRequest({
269
+ limit: 10,
270
+ search: 'Meeting',
271
+ });
272
+
273
+ const response = await getUserEmails(searchQuery);
274
+
275
+ expect(response.body.results).toHaveLength(1);
276
+ expect(response.body.results[0].subject).toBe('Important Meeting Reminder');
277
+ });
278
+
279
+ test('Should respect pagination limit', async () => {
280
+ // Create multiple emails
281
+ const emails: Email[] = [];
282
+ for (let i = 0; i < 5; i++) {
283
+ const email = new Email();
284
+ email.subject = `Test Email ${i}`;
285
+ email.status = EmailStatus.Sent;
286
+ email.text = `Content ${i}`;
287
+ email.html = `<p>Content ${i}</p>`;
288
+ email.json = {};
289
+ email.organizationId = organization.id;
290
+ email.showInMemberPortal = true;
291
+ email.sentAt = new Date();
292
+ await email.save();
293
+ emails.push(email);
294
+ }
295
+
296
+ // Create recipients for all emails
297
+ const recipients: EmailRecipient[] = [];
298
+ for (const email of emails) {
299
+ const recipient = new EmailRecipient();
300
+ recipient.emailId = email.id;
301
+ recipient.memberId = member.id;
302
+ recipient.userId = user.id;
303
+ recipient.email = user.email;
304
+ recipient.firstName = member.details.firstName;
305
+ recipient.lastName = member.details.lastName;
306
+ recipient.sentAt = new Date();
307
+ await recipient.save();
308
+ recipients.push(recipient);
309
+ }
310
+
311
+ // Test with limit of 3
312
+ const limitedQuery = new LimitedFilteredRequest({
313
+ limit: 3,
314
+ });
315
+
316
+ const response = await getUserEmails(limitedQuery);
317
+
318
+ expect(response.body.results).toHaveLength(3);
319
+ expect(response.body.next).toBeDefined(); // Should have next page
320
+ });
321
+
322
+ test('Should return correct recipient when email has multiple recipients matching same user', async () => {
323
+ // Create an email
324
+ const email = new Email();
325
+ email.subject = 'Multiple Recipients Email';
326
+ email.status = EmailStatus.Sent;
327
+ email.text = 'Email with multiple recipients';
328
+ email.html = '<p>Email with multiple recipients</p>';
329
+ email.json = {};
330
+ email.organizationId = organization.id;
331
+ email.showInMemberPortal = true;
332
+ email.sentAt = new Date();
333
+ await email.save();
334
+
335
+ // Create multiple recipients for the same user/member combination
336
+ // First recipient: exact match (userId and email match)
337
+ const exactMatchRecipient = new EmailRecipient();
338
+ exactMatchRecipient.emailId = email.id;
339
+ exactMatchRecipient.memberId = member.id;
340
+ exactMatchRecipient.userId = user.id;
341
+ exactMatchRecipient.email = user.email;
342
+ exactMatchRecipient.firstName = 'Exact';
343
+ exactMatchRecipient.lastName = 'Match';
344
+ exactMatchRecipient.sentAt = new Date();
345
+ await exactMatchRecipient.save();
346
+
347
+ // Second recipient: member match only (no userId or email)
348
+ const memberOnlyRecipient = new EmailRecipient();
349
+ memberOnlyRecipient.emailId = email.id;
350
+ memberOnlyRecipient.memberId = member.id;
351
+ memberOnlyRecipient.userId = null;
352
+ memberOnlyRecipient.email = null;
353
+ memberOnlyRecipient.firstName = 'Member';
354
+ memberOnlyRecipient.lastName = 'Only';
355
+ memberOnlyRecipient.sentAt = new Date();
356
+ await memberOnlyRecipient.save();
357
+
358
+ // Third recipient: any data but same member
359
+ const anyDataRecipient = new EmailRecipient();
360
+ anyDataRecipient.emailId = email.id;
361
+ anyDataRecipient.memberId = member.id;
362
+ anyDataRecipient.userId = null; // No specific user
363
+ anyDataRecipient.email = 'other@example.com'; // Different email
364
+ anyDataRecipient.firstName = 'Any';
365
+ anyDataRecipient.lastName = 'Data';
366
+ anyDataRecipient.sentAt = new Date();
367
+ await anyDataRecipient.save();
368
+
369
+ // Search specifically for this email to avoid interference from other tests
370
+ const searchQuery = new LimitedFilteredRequest({
371
+ limit: 10,
372
+ search: 'Multiple Recipients Email',
373
+ });
374
+
375
+ const response = await getUserEmails(searchQuery);
376
+
377
+ expect(response.body.results).toHaveLength(1);
378
+ const emailResult = response.body.results[0];
379
+
380
+ // Should prefer the exact match recipient
381
+ expect(emailResult.recipients).toHaveLength(1);
382
+ expect(emailResult.recipients[0].firstName).toBe('Exact');
383
+ expect(emailResult.recipients[0].lastName).toBe('Match');
384
+ });
385
+
386
+ test('Should return generic data when recipient has no matching user id or email', async () => {
387
+ // Create an email
388
+ const email = new Email();
389
+ email.subject = 'Generic Recipient Email';
390
+ email.status = EmailStatus.Sent;
391
+ email.text = 'Email with generic recipient';
392
+ email.html = '<p>Email with generic recipient</p>';
393
+ email.json = {};
394
+ email.organizationId = organization.id;
395
+ email.showInMemberPortal = true;
396
+ email.sentAt = new Date();
397
+ await email.save();
398
+
399
+ // Create a recipient with no userId or email (generic member data)
400
+ const genericRecipient = new EmailRecipient();
401
+ genericRecipient.emailId = email.id;
402
+ genericRecipient.memberId = member.id;
403
+ genericRecipient.userId = null; // No specific user
404
+ genericRecipient.email = null; // No specific email
405
+ genericRecipient.firstName = 'Generic';
406
+ genericRecipient.lastName = 'Member';
407
+ genericRecipient.sentAt = new Date();
408
+ await genericRecipient.save();
409
+
410
+ // Search specifically for this email to avoid interference from other tests
411
+ const searchQuery = new LimitedFilteredRequest({
412
+ limit: 10,
413
+ search: 'Generic Recipient Email',
414
+ });
415
+
416
+ const response = await getUserEmails(searchQuery);
417
+
418
+ expect(response.body.results).toHaveLength(1);
419
+ const emailResult = response.body.results[0];
420
+
421
+ // Should return the generic recipient data
422
+ expect(emailResult.recipients).toHaveLength(1);
423
+ expect(emailResult.recipients[0].firstName).toBe('Generic');
424
+ expect(emailResult.recipients[0].lastName).toBe('Member');
425
+ // The original recipient struct keeps its original userId and email
426
+ expect(emailResult.recipients[0].userId).toBe(null); // Original was null
427
+ expect(emailResult.recipients[0].email).toBe(null); // Original was null
428
+ // But replacements should be generated for the current user (tested below)
429
+ expect(emailResult.recipients[0].replacements).toBeDefined();
430
+ });
431
+
432
+ test('Should strip sensitive information from email replacements for non-matching recipients', async () => {
433
+ // Create another user that will have sensitive data
434
+ const sensitiveUser = await new UserFactory({
435
+ organization,
436
+ }).create();
437
+
438
+ // Create an email
439
+ const email = new Email();
440
+ email.subject = 'Sensitive Data Email';
441
+ email.status = EmailStatus.Sent;
442
+ email.text = 'Email with sensitive replacements {{outstandingBalance}} {{loginDetails}} {{unsubscribeUrl}}';
443
+ email.html = '<p>Email with sensitive replacements {{outstandingBalance}} {{loginDetails}} {{unsubscribeUrl}}</p>';
444
+ email.json = {};
445
+ email.organizationId = organization.id;
446
+ email.showInMemberPortal = true;
447
+ email.sentAt = new Date();
448
+ await email.save();
449
+
450
+ // Create a recipient for the sensitive user (with different user data than our test user)
451
+ const sensitiveRecipient = new EmailRecipient();
452
+ sensitiveRecipient.emailId = email.id;
453
+ sensitiveRecipient.memberId = member.id; // Same member as our test user
454
+ sensitiveRecipient.userId = sensitiveUser.id; // Different user ID
455
+ sensitiveRecipient.email = sensitiveUser.email; // Different email
456
+ sensitiveRecipient.firstName = member.details.firstName;
457
+ sensitiveRecipient.lastName = member.details.lastName;
458
+ sensitiveRecipient.sentAt = new Date();
459
+
460
+ // Add sensitive replacements that should be stripped by the API
461
+ sensitiveRecipient.replacements = [
462
+ Replacement.create({
463
+ token: 'loginDetails',
464
+ value: '',
465
+ html: `<p class="description"><em>Login with <strong>${sensitiveUser.email}</strong> Alice Security Code: <span class="style-inline-code">ABCD-EFGH-IJKL-MNOP</span></em></p>`,
466
+ }),
467
+ Replacement.create({
468
+ token: 'unsubscribeUrl',
469
+ value: 'https://example.com/unsubscribe?token=secret-token-12345',
470
+ }),
471
+ Replacement.create({
472
+ token: 'signInUrl',
473
+ value: 'https://example.com/login?token=private-signin-token-67890',
474
+ }),
475
+ Replacement.create({
476
+ token: 'outstandingBalance',
477
+ value: '€ 150.00',
478
+ }),
479
+ Replacement.create({
480
+ token: 'balanceTable',
481
+ html: '<table><tr><td>Private balance information</td><td>€ 150.00</td></tr></table>',
482
+ }),
483
+ ];
484
+
485
+ await sensitiveRecipient.save();
486
+
487
+ // Search specifically for this email to avoid interference from other tests
488
+ const searchQuery = new LimitedFilteredRequest({
489
+ limit: 10,
490
+ search: 'Sensitive Data Email',
491
+ });
492
+
493
+ const response = await getUserEmails(searchQuery);
494
+
495
+ expect(response.body.results).toHaveLength(1);
496
+ const emailResult = response.body.results[0];
497
+
498
+ expect(emailResult.recipients).toHaveLength(1);
499
+ const recipient = emailResult.recipients[0];
500
+
501
+ // The original recipient struct keeps its original userId and email from the sensitive user
502
+ expect(recipient.userId).toBe(sensitiveUser.id); // Original userId
503
+ expect(recipient.email).toBe(sensitiveUser.email); // Original email
504
+
505
+ // Verify that sensitive data has been properly processed
506
+ expect(recipient.replacements).toBeDefined();
507
+ expect(Array.isArray(recipient.replacements)).toBe(true);
508
+
509
+ // The system should:
510
+ // 1. Strip sensitive replacements from the original recipient (done with willFill: true)
511
+ // 2. Create new appropriate replacements for the current viewing user (done with fillRecipientReplacements)
512
+ // 3. Apply web display filtering (done with stripRecipientReplacementsForWebDisplay)
513
+
514
+ // The original sensitive data (ABCD-EFGH-IJKL-MNOP, secret-token-12345, private-signin-token-67890)
515
+ // should be completely gone because:
516
+ // - stripSensitiveRecipientReplacements removes the original replacements
517
+ // - fillRecipientReplacements creates new ones for the current user
518
+ // - stripRecipientReplacementsForWebDisplay makes them safe for web display
519
+
520
+ const allReplacementsString = JSON.stringify(recipient.replacements);
521
+ expect(allReplacementsString).not.toContain('ABCD-EFGH-IJKL-MNOP'); // Original security code should be gone
522
+ expect(allReplacementsString).not.toContain('secret-token-12345'); // Original sensitive unsubscribe token should be gone
523
+ expect(allReplacementsString).not.toContain('private-signin-token-67890'); // Original sensitive signin token should be gone
524
+
525
+ // Verify that safe, current-user-appropriate replacements are created
526
+ const loginDetailsReplacement = recipient.replacements.find(r => r.token === 'loginDetails');
527
+ const unsubscribeUrlReplacement = recipient.replacements.find(r => r.token === 'unsubscribeUrl');
528
+
529
+ // loginDetails should exist and be empty/generic for web display
530
+ expect(loginDetailsReplacement).toBeDefined();
531
+ expect(loginDetailsReplacement!.html).toBe(undefined); // Should be empty for web display
532
+ expect(loginDetailsReplacement!.value).toBe('');
533
+
534
+ // unsubscribeUrl should exist and be safe for web display
535
+ expect(unsubscribeUrlReplacement).toBeDefined();
536
+ expect(unsubscribeUrlReplacement!.value).toMatch(/^https:\/\//); // Should still be a valid URL
537
+ expect(unsubscribeUrlReplacement!.value).not.toContain('secret-token-12345'); // Original sensitive token should be gone
538
+
539
+ // This tests that Email.getStructureForUser properly handles sensitive data by:
540
+ // 1. Removing original sensitive replacements from other users' data
541
+ // 2. Creating fresh, appropriate replacements for the current viewer
542
+ // 3. Ensuring web safety of all replacement values
543
+
544
+ // Check outstandingBalance replacement
545
+ const balanceReplacement = recipient.replacements.find(r => r.token === 'outstandingBalance');
546
+ expect(balanceReplacement).toBeDefined();
547
+ expect(balanceReplacement!.value).toBe(Formatter.price(0)); // Should be corrected to the new user
548
+
549
+ // Check balanceTable replacement
550
+ const balanceTableReplacement = recipient.replacements.find(r => r.token === 'balanceTable');
551
+ expect(balanceTableReplacement).toBeDefined();
552
+ expect(balanceTableReplacement!.html).toBe('<p class="description">' + $t('4c4f6571-f7b5-469d-a16f-b1547b43a610') + '</p>');
553
+ });
554
+
555
+ test('Should not return emails from other members the user does not have access to', async () => {
556
+ // Create another user and member
557
+ const otherUser = await new UserFactory({
558
+ organization,
559
+ }).create();
560
+
561
+ const otherMember = await new MemberFactory({
562
+ organization,
563
+ user: otherUser,
564
+ }).create();
565
+
566
+ // Create an email
567
+ const email = new Email();
568
+ email.subject = 'Email for Other Member';
569
+ email.status = EmailStatus.Sent;
570
+ email.text = 'This email is for another member';
571
+ email.html = '<p>This email is for another member</p>';
572
+ email.json = {};
573
+ email.organizationId = organization.id;
574
+ email.showInMemberPortal = true;
575
+ email.sentAt = new Date();
576
+ await email.save();
577
+
578
+ // Create an email recipient linked to the OTHER member
579
+ const emailRecipient = new EmailRecipient();
580
+ emailRecipient.emailId = email.id;
581
+ emailRecipient.memberId = otherMember.id; // Different member
582
+ emailRecipient.userId = otherUser.id;
583
+ emailRecipient.email = otherUser.email;
584
+ emailRecipient.firstName = otherMember.details.firstName;
585
+ emailRecipient.lastName = otherMember.details.lastName;
586
+ emailRecipient.sentAt = new Date();
587
+ await emailRecipient.save();
588
+
589
+ const response = await getUserEmails();
590
+
591
+ // Should not include emails sent to other members
592
+ expect(response.body.results.find(e => e.id === email.id)).toBeUndefined();
593
+
594
+ // Test that is IS included if we request the same data via the 'otherUser' token
595
+ const otherUserToken = await Token.createToken(otherUser);
596
+ const responseForOtherUser = await getUserEmails(new LimitedFilteredRequest({ limit: 10 }), otherUserToken);
597
+ expect(responseForOtherUser.body.results.find(e => e.id === email.id)).toBeDefined();
598
+ });
599
+
600
+ test('Should throw error when not authenticated', async () => {
601
+ const request = Request.get({
602
+ path: baseUrl,
603
+ host: organization.getApiHost(),
604
+ query: new LimitedFilteredRequest({ limit: 10 }),
605
+ });
606
+ // No authorization header
607
+
608
+ await expect(testServer.test(endpoint, request)).rejects.toThrow();
609
+ });
610
+
611
+ test('Should throw error with invalid token', async () => {
612
+ const request = Request.get({
613
+ path: baseUrl,
614
+ host: organization.getApiHost(),
615
+ query: new LimitedFilteredRequest({ limit: 10 }),
616
+ headers: {
617
+ authorization: 'Bearer invalid_token',
618
+ },
619
+ });
620
+
621
+ await expect(testServer.test(endpoint, request)).rejects.toThrow();
622
+ });
623
+ });
@@ -60,7 +60,8 @@ export class GetUserEmailsEndpoint extends Endpoint<Params, Query, Body, Respons
60
60
 
61
61
  const query = Email.select()
62
62
  .where('deletedAt', null)
63
- .where('status', EmailStatus.Sent);
63
+ .where('status', EmailStatus.Sent)
64
+ .where('showInMemberPortal', true);
64
65
 
65
66
  if (scopeFilter) {
66
67
  query.where(await compileToSQLFilter(scopeFilter, filterCompilers));