payload-plugin-newsletter 0.3.2 → 0.4.5

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.
Files changed (180) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/CLAUDE.md +31 -19
  3. package/dist/client.cjs +899 -0
  4. package/dist/client.cjs.map +1 -0
  5. package/dist/client.d.cts +52 -0
  6. package/dist/client.d.ts +52 -0
  7. package/dist/client.js +867 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/components.cjs +899 -0
  10. package/dist/components.cjs.map +1 -0
  11. package/dist/components.d.cts +4 -0
  12. package/dist/components.d.ts +4 -0
  13. package/dist/components.js +867 -0
  14. package/dist/components.js.map +1 -0
  15. package/dist/index.cjs +2004 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +11 -0
  18. package/dist/index.d.ts +6 -5
  19. package/dist/index.js +1967 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/types.cjs +19 -0
  22. package/dist/types.cjs.map +1 -0
  23. package/dist/{types/index.d.ts → types.d.cts} +19 -17
  24. package/dist/types.d.ts +350 -0
  25. package/dist/types.js +1 -0
  26. package/dist/types.js.map +1 -0
  27. package/package.json +48 -25
  28. package/dist/.tsbuildinfo +0 -1
  29. package/dist/collections/NewsletterSettings.d.ts +0 -4
  30. package/dist/collections/NewsletterSettings.d.ts.map +0 -1
  31. package/dist/collections/Subscribers.d.ts +0 -4
  32. package/dist/collections/Subscribers.d.ts.map +0 -1
  33. package/dist/components/MagicLinkVerify.d.ts +0 -27
  34. package/dist/components/MagicLinkVerify.d.ts.map +0 -1
  35. package/dist/components/NewsletterForm.d.ts +0 -5
  36. package/dist/components/NewsletterForm.d.ts.map +0 -1
  37. package/dist/components/PreferencesForm.d.ts +0 -5
  38. package/dist/components/PreferencesForm.d.ts.map +0 -1
  39. package/dist/components/index.d.ts +0 -5
  40. package/dist/components/index.d.ts.map +0 -1
  41. package/dist/endpoints/index.d.ts +0 -4
  42. package/dist/endpoints/index.d.ts.map +0 -1
  43. package/dist/endpoints/preferences.d.ts +0 -5
  44. package/dist/endpoints/preferences.d.ts.map +0 -1
  45. package/dist/endpoints/subscribe.d.ts +0 -4
  46. package/dist/endpoints/subscribe.d.ts.map +0 -1
  47. package/dist/endpoints/unsubscribe.d.ts +0 -4
  48. package/dist/endpoints/unsubscribe.d.ts.map +0 -1
  49. package/dist/endpoints/verify-magic-link.d.ts +0 -4
  50. package/dist/endpoints/verify-magic-link.d.ts.map +0 -1
  51. package/dist/exports/client.d.ts +0 -6
  52. package/dist/exports/client.d.ts.map +0 -1
  53. package/dist/exports/components.d.ts +0 -2
  54. package/dist/exports/components.d.ts.map +0 -1
  55. package/dist/exports/types.d.ts +0 -2
  56. package/dist/exports/types.d.ts.map +0 -1
  57. package/dist/fields/newsletterScheduling.d.ts +0 -4
  58. package/dist/fields/newsletterScheduling.d.ts.map +0 -1
  59. package/dist/hooks/useNewsletterAuth.d.ts +0 -16
  60. package/dist/hooks/useNewsletterAuth.d.ts.map +0 -1
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/providers/broadcast.d.ts +0 -19
  63. package/dist/providers/broadcast.d.ts.map +0 -1
  64. package/dist/providers/index.d.ts +0 -23
  65. package/dist/providers/index.d.ts.map +0 -1
  66. package/dist/providers/resend.d.ts +0 -20
  67. package/dist/providers/resend.d.ts.map +0 -1
  68. package/dist/providers/types.d.ts +0 -46
  69. package/dist/providers/types.d.ts.map +0 -1
  70. package/dist/src/__tests__/fixtures/newsletter-settings.js +0 -41
  71. package/dist/src/__tests__/fixtures/newsletter-settings.js.map +0 -1
  72. package/dist/src/__tests__/fixtures/subscribers.js +0 -70
  73. package/dist/src/__tests__/fixtures/subscribers.js.map +0 -1
  74. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js +0 -356
  75. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js.map +0 -1
  76. package/dist/src/__tests__/integration/endpoints/preferences.test.js +0 -266
  77. package/dist/src/__tests__/integration/endpoints/preferences.test.js.map +0 -1
  78. package/dist/src/__tests__/integration/endpoints/subscribe.test.js +0 -280
  79. package/dist/src/__tests__/integration/endpoints/subscribe.test.js.map +0 -1
  80. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js +0 -187
  81. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js.map +0 -1
  82. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js +0 -188
  83. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js.map +0 -1
  84. package/dist/src/__tests__/mocks/email-providers.js +0 -153
  85. package/dist/src/__tests__/mocks/email-providers.js.map +0 -1
  86. package/dist/src/__tests__/mocks/payload.js +0 -244
  87. package/dist/src/__tests__/mocks/payload.js.map +0 -1
  88. package/dist/src/__tests__/security/csrf-protection.test.js +0 -309
  89. package/dist/src/__tests__/security/csrf-protection.test.js.map +0 -1
  90. package/dist/src/__tests__/security/settings-access.test.js +0 -204
  91. package/dist/src/__tests__/security/settings-access.test.js.map +0 -1
  92. package/dist/src/__tests__/security/subscriber-access.test.js +0 -210
  93. package/dist/src/__tests__/security/subscriber-access.test.js.map +0 -1
  94. package/dist/src/__tests__/security/xss-prevention.test.js +0 -305
  95. package/dist/src/__tests__/security/xss-prevention.test.js.map +0 -1
  96. package/dist/src/__tests__/setup/integration.setup.js +0 -38
  97. package/dist/src/__tests__/setup/integration.setup.js.map +0 -1
  98. package/dist/src/__tests__/setup/unit.setup.js +0 -41
  99. package/dist/src/__tests__/setup/unit.setup.js.map +0 -1
  100. package/dist/src/__tests__/unit/utils/access.test.js +0 -116
  101. package/dist/src/__tests__/unit/utils/access.test.js.map +0 -1
  102. package/dist/src/__tests__/unit/utils/jwt.test.js +0 -238
  103. package/dist/src/__tests__/unit/utils/jwt.test.js.map +0 -1
  104. package/dist/src/collections/NewsletterSettings.js +0 -390
  105. package/dist/src/collections/NewsletterSettings.js.map +0 -1
  106. package/dist/src/collections/Subscribers.js +0 -309
  107. package/dist/src/collections/Subscribers.js.map +0 -1
  108. package/dist/src/components/MagicLinkVerify.js +0 -180
  109. package/dist/src/components/MagicLinkVerify.js.map +0 -1
  110. package/dist/src/components/NewsletterForm.js +0 -326
  111. package/dist/src/components/NewsletterForm.js.map +0 -1
  112. package/dist/src/components/PreferencesForm.js +0 -524
  113. package/dist/src/components/PreferencesForm.js.map +0 -1
  114. package/dist/src/components/index.js +0 -5
  115. package/dist/src/components/index.js.map +0 -1
  116. package/dist/src/endpoints/index.js +0 -17
  117. package/dist/src/endpoints/index.js.map +0 -1
  118. package/dist/src/endpoints/preferences.js +0 -136
  119. package/dist/src/endpoints/preferences.js.map +0 -1
  120. package/dist/src/endpoints/subscribe.js +0 -151
  121. package/dist/src/endpoints/subscribe.js.map +0 -1
  122. package/dist/src/endpoints/unsubscribe.js +0 -105
  123. package/dist/src/endpoints/unsubscribe.js.map +0 -1
  124. package/dist/src/endpoints/verify-magic-link.js +0 -103
  125. package/dist/src/endpoints/verify-magic-link.js.map +0 -1
  126. package/dist/src/exports/client.js +0 -7
  127. package/dist/src/exports/client.js.map +0 -1
  128. package/dist/src/exports/components.js +0 -6
  129. package/dist/src/exports/components.js.map +0 -1
  130. package/dist/src/exports/types.js +0 -3
  131. package/dist/src/exports/types.js.map +0 -1
  132. package/dist/src/fields/newsletterScheduling.js +0 -195
  133. package/dist/src/fields/newsletterScheduling.js.map +0 -1
  134. package/dist/src/hooks/useNewsletterAuth.js +0 -112
  135. package/dist/src/hooks/useNewsletterAuth.js.map +0 -1
  136. package/dist/src/index.js +0 -130
  137. package/dist/src/index.js.map +0 -1
  138. package/dist/src/providers/broadcast.js +0 -158
  139. package/dist/src/providers/broadcast.js.map +0 -1
  140. package/dist/src/providers/index.js +0 -63
  141. package/dist/src/providers/index.js.map +0 -1
  142. package/dist/src/providers/resend.js +0 -122
  143. package/dist/src/providers/resend.js.map +0 -1
  144. package/dist/src/providers/types.js +0 -12
  145. package/dist/src/providers/types.js.map +0 -1
  146. package/dist/src/templates/BaseTemplate.js +0 -105
  147. package/dist/src/templates/BaseTemplate.js.map +0 -1
  148. package/dist/src/templates/MagicLinkTemplate.js +0 -178
  149. package/dist/src/templates/MagicLinkTemplate.js.map +0 -1
  150. package/dist/src/templates/NewsletterTemplate.js +0 -150
  151. package/dist/src/templates/NewsletterTemplate.js.map +0 -1
  152. package/dist/src/templates/WelcomeTemplate.js +0 -192
  153. package/dist/src/templates/WelcomeTemplate.js.map +0 -1
  154. package/dist/src/templates/index.js +0 -6
  155. package/dist/src/templates/index.js.map +0 -1
  156. package/dist/src/types/index.js +0 -3
  157. package/dist/src/types/index.js.map +0 -1
  158. package/dist/src/utils/access.js +0 -80
  159. package/dist/src/utils/access.js.map +0 -1
  160. package/dist/src/utils/jwt.js +0 -91
  161. package/dist/src/utils/jwt.js.map +0 -1
  162. package/dist/src/utils/validation.js +0 -74
  163. package/dist/src/utils/validation.js.map +0 -1
  164. package/dist/templates/BaseTemplate.d.ts +0 -45
  165. package/dist/templates/BaseTemplate.d.ts.map +0 -1
  166. package/dist/templates/MagicLinkTemplate.d.ts +0 -67
  167. package/dist/templates/MagicLinkTemplate.d.ts.map +0 -1
  168. package/dist/templates/NewsletterTemplate.d.ts +0 -112
  169. package/dist/templates/NewsletterTemplate.d.ts.map +0 -1
  170. package/dist/templates/WelcomeTemplate.d.ts +0 -55
  171. package/dist/templates/WelcomeTemplate.d.ts.map +0 -1
  172. package/dist/templates/index.d.ts +0 -7
  173. package/dist/templates/index.d.ts.map +0 -1
  174. package/dist/types/index.d.ts.map +0 -1
  175. package/dist/utils/access.d.ts +0 -15
  176. package/dist/utils/access.d.ts.map +0 -1
  177. package/dist/utils/jwt.d.ts +0 -32
  178. package/dist/utils/jwt.d.ts.map +0 -1
  179. package/dist/utils/validation.d.ts +0 -25
  180. package/dist/utils/validation.d.ts.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../src/__tests__/integration/endpoints/preferences.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport { createPreferencesEndpoint } from '../../../endpoints/preferences'\nimport { createPayloadRequestMock, seedCollection, clearCollections, createMockUser, createMockAdminUser } from '../../mocks/payload'\nimport { mockSubscribers } from '../../fixtures/subscribers'\nimport type { NewsletterPluginConfig } from '../../../types'\n\ndescribe('Preferences Endpoint Security', () => {\n let endpoint: any\n let mockReq: any\n let mockRes: any\n const config: NewsletterPluginConfig = {\n subscribersSlug: 'subscribers',\n }\n\n beforeEach(() => {\n clearCollections()\n seedCollection('subscribers', mockSubscribers)\n \n endpoint = createPreferencesEndpoint(config)\n const payloadMock = createPayloadRequestMock()\n \n mockReq = {\n payload: payloadMock.payload,\n body: {},\n user: null,\n method: 'GET',\n }\n \n mockRes = {\n status: vi.fn().mockReturnThis(),\n json: vi.fn(),\n }\n \n vi.clearAllMocks()\n })\n\n describe('GET - Read Preferences', () => {\n it('should require authentication', async () => {\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(401)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Authentication required',\n })\n })\n\n it('should allow subscribers to read their own preferences', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(200)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: true,\n preferences: expect.objectContaining({\n email: 'active@example.com',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n },\n }),\n })\n })\n\n it('should prevent reading other subscribers preferences', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.query = { subscriberId: 'sub-2' }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(403)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'You can only access your own preferences',\n })\n })\n\n it('should allow admins to read any preferences', async () => {\n mockReq.user = createMockAdminUser()\n mockReq.query = { subscriberId: 'sub-2' }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(200)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: true,\n preferences: expect.objectContaining({\n email: 'pending@example.com',\n }),\n })\n })\n })\n\n describe('POST - Update Preferences', () => {\n beforeEach(() => {\n mockReq.method = 'POST'\n })\n\n it('should require authentication', async () => {\n mockReq.body = {\n emailPreferences: {\n newsletter: false,\n },\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(401)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Authentication required',\n })\n })\n\n it('should allow subscribers to update their own preferences', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.body = {\n emailPreferences: {\n newsletter: false,\n announcements: true,\n },\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockReq.payload.update).toHaveBeenCalledWith({\n collection: 'subscribers',\n id: 'sub-1',\n data: {\n emailPreferences: {\n newsletter: false,\n announcements: true,\n },\n },\n overrideAccess: false,\n user: mockReq.user,\n })\n \n expect(mockRes.status).toHaveBeenCalledWith(200)\n })\n\n it('should prevent updating other subscribers preferences', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.body = {\n subscriberId: 'sub-2',\n emailPreferences: {\n newsletter: false,\n },\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(403)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'You can only update your own preferences',\n })\n })\n\n it('should validate preference structure', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.body = {\n emailPreferences: {\n newsletter: 'yes', // Should be boolean\n unknownField: true, // Should be filtered out\n },\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(400)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Invalid preference values',\n })\n })\n\n it('should prevent updating protected fields', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.body = {\n email: 'newemail@example.com', // Should not be allowed\n subscriptionStatus: 'active', // Should not be allowed\n emailPreferences: {\n newsletter: false,\n },\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n // Should only update emailPreferences\n expect(mockReq.payload.update).toHaveBeenCalledWith(\n expect.objectContaining({\n data: {\n emailPreferences: {\n newsletter: false,\n announcements: true, // Preserves existing value\n },\n },\n })\n )\n \n // Should not include protected fields\n const updateData = mockReq.payload.update.mock.calls[0][0].data\n expect(updateData).not.toHaveProperty('email')\n expect(updateData).not.toHaveProperty('subscriptionStatus')\n })\n })\n\n describe('Error Handling', () => {\n it('should handle non-existent subscribers', async () => {\n mockReq.user = {\n id: 'sub-999',\n email: 'ghost@example.com',\n collection: 'subscribers',\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(404)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Subscriber not found',\n })\n })\n\n it('should handle database errors gracefully', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n \n mockReq.payload.findByID.mockRejectedValueOnce(new Error('Database error'))\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(500)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Failed to retrieve preferences',\n })\n })\n })\n\n describe('Unsubscribed Users', () => {\n it('should allow unsubscribed users to view preferences', async () => {\n mockReq.user = {\n id: 'sub-3',\n email: 'unsubscribed@example.com',\n collection: 'subscribers',\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(200)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: true,\n preferences: expect.objectContaining({\n subscriptionStatus: 'unsubscribed',\n }),\n })\n })\n\n it('should prevent unsubscribed users from updating preferences', async () => {\n mockReq.method = 'POST'\n mockReq.user = {\n id: 'sub-3',\n email: 'unsubscribed@example.com',\n collection: 'subscribers',\n }\n mockReq.body = {\n emailPreferences: {\n newsletter: true,\n },\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(403)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Cannot update preferences for unsubscribed users',\n })\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","createPreferencesEndpoint","createPayloadRequestMock","seedCollection","clearCollections","createMockAdminUser","mockSubscribers","endpoint","mockReq","mockRes","config","subscribersSlug","payloadMock","payload","body","user","method","status","fn","mockReturnThis","json","clearAllMocks","handler","toHaveBeenCalledWith","success","error","id","email","collection","preferences","objectContaining","emailPreferences","newsletter","announcements","query","subscriberId","update","data","overrideAccess","unknownField","subscriptionStatus","updateData","mock","calls","not","toHaveProperty","findByID","mockRejectedValueOnce","Error"],"mappings":"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"}
@@ -1,280 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { createSubscribeEndpoint } from '../../../endpoints/subscribe';
3
- import { createPayloadRequestMock, seedCollection, clearCollections } from '../../mocks/payload';
4
- import { mockNewsletterSettings } from '../../fixtures/newsletter-settings';
5
- import { createResendMock } from '../../mocks/email-providers';
6
- // Mock email service
7
- vi.mock('../../../services/email', ()=>({
8
- getEmailService: vi.fn()
9
- }));
10
- import { getEmailService } from '../../../services/email';
11
- describe('Subscribe Endpoint Security', ()=>{
12
- let endpoint;
13
- let mockReq;
14
- let mockRes;
15
- let mockEmailService;
16
- const config = {
17
- subscribersSlug: 'subscribers',
18
- settingsSlug: 'newsletter-settings'
19
- };
20
- beforeEach(()=>{
21
- clearCollections();
22
- seedCollection('newsletter-settings', [
23
- mockNewsletterSettings
24
- ]);
25
- endpoint = createSubscribeEndpoint(config);
26
- const payloadMock = createPayloadRequestMock();
27
- mockReq = {
28
- payload: payloadMock.payload,
29
- body: {},
30
- ip: '127.0.0.1'
31
- };
32
- mockRes = {
33
- status: vi.fn().mockReturnThis(),
34
- json: vi.fn()
35
- };
36
- // Setup email service mock
37
- mockEmailService = createResendMock();
38
- getEmailService.mockResolvedValue(mockEmailService);
39
- vi.clearAllMocks();
40
- });
41
- describe('Input Validation', ()=>{
42
- it('should reject requests without email', async ()=>{
43
- await endpoint.handler(mockReq, mockRes);
44
- expect(mockRes.status).toHaveBeenCalledWith(400);
45
- expect(mockRes.json).toHaveBeenCalledWith({
46
- success: false,
47
- error: 'Email is required'
48
- });
49
- });
50
- it('should reject invalid email formats', async ()=>{
51
- const invalidEmails = [
52
- 'notanemail',
53
- '@example.com',
54
- 'user@',
55
- 'user @example.com',
56
- 'user@example',
57
- '<script>alert("xss")</script>@example.com'
58
- ];
59
- for (const email of invalidEmails){
60
- mockReq.body = {
61
- email
62
- };
63
- await endpoint.handler(mockReq, mockRes);
64
- expect(mockRes.status).toHaveBeenCalledWith(400);
65
- expect(mockRes.json).toHaveBeenCalledWith({
66
- success: false,
67
- error: 'Invalid email format'
68
- });
69
- }
70
- });
71
- it('should sanitize email input', async ()=>{
72
- mockReq.body = {
73
- email: ' User@EXAMPLE.com ',
74
- name: '<script>alert("xss")</script>Test User'
75
- };
76
- await endpoint.handler(mockReq, mockRes);
77
- // Check that create was called with sanitized data
78
- expect(mockReq.payload.create).toHaveBeenCalledWith(expect.objectContaining({
79
- data: expect.objectContaining({
80
- email: 'user@example.com',
81
- name: 'Test User'
82
- })
83
- }));
84
- });
85
- });
86
- describe('Rate Limiting', ()=>{
87
- it('should enforce max subscribers per IP', async ()=>{
88
- // Create max subscribers from same IP
89
- const maxSubscribers = mockNewsletterSettings.subscriptionSettings.maxSubscribersPerIP;
90
- for(let i = 0; i < maxSubscribers; i++){
91
- seedCollection('subscribers', [
92
- {
93
- id: `sub-ip-${i}`,
94
- email: `user${i}@example.com`,
95
- ip: '127.0.0.1',
96
- subscriptionStatus: 'active'
97
- }
98
- ]);
99
- }
100
- mockReq.body = {
101
- email: 'newuser@example.com'
102
- };
103
- await endpoint.handler(mockReq, mockRes);
104
- expect(mockRes.status).toHaveBeenCalledWith(429);
105
- expect(mockRes.json).toHaveBeenCalledWith({
106
- success: false,
107
- error: 'Too many subscription attempts from this IP address'
108
- });
109
- });
110
- it('should not count unsubscribed users in rate limit', async ()=>{
111
- // Create some unsubscribed users
112
- for(let i = 0; i < 5; i++){
113
- seedCollection('subscribers', [
114
- {
115
- id: `sub-unsub-${i}`,
116
- email: `unsub${i}@example.com`,
117
- ip: '127.0.0.1',
118
- subscriptionStatus: 'unsubscribed'
119
- }
120
- ]);
121
- }
122
- mockReq.body = {
123
- email: 'newuser@example.com'
124
- };
125
- await endpoint.handler(mockReq, mockRes);
126
- expect(mockRes.status).toHaveBeenCalledWith(200);
127
- });
128
- });
129
- describe('Domain Restrictions', ()=>{
130
- it('should enforce allowed domains when configured', async ()=>{
131
- // Update settings to restrict domains
132
- const restrictedSettings = {
133
- ...mockNewsletterSettings,
134
- subscriptionSettings: {
135
- ...mockNewsletterSettings.subscriptionSettings,
136
- allowedDomains: [
137
- {
138
- domain: 'allowed.com'
139
- },
140
- {
141
- domain: 'company.com'
142
- }
143
- ]
144
- }
145
- };
146
- clearCollections();
147
- seedCollection('newsletter-settings', [
148
- restrictedSettings
149
- ]);
150
- // Test blocked domain
151
- mockReq.body = {
152
- email: 'user@blocked.com'
153
- };
154
- await endpoint.handler(mockReq, mockRes);
155
- expect(mockRes.status).toHaveBeenCalledWith(403);
156
- expect(mockRes.json).toHaveBeenCalledWith({
157
- success: false,
158
- error: 'Email domain not allowed'
159
- });
160
- // Test allowed domain
161
- mockReq.body = {
162
- email: 'user@allowed.com'
163
- };
164
- await endpoint.handler(mockReq, mockRes);
165
- expect(mockRes.status).toHaveBeenCalledWith(200);
166
- });
167
- });
168
- describe('Duplicate Prevention', ()=>{
169
- it('should handle existing active subscribers', async ()=>{
170
- seedCollection('subscribers', [
171
- {
172
- id: 'existing-sub',
173
- email: 'existing@example.com',
174
- subscriptionStatus: 'active'
175
- }
176
- ]);
177
- mockReq.body = {
178
- email: 'existing@example.com'
179
- };
180
- await endpoint.handler(mockReq, mockRes);
181
- expect(mockRes.status).toHaveBeenCalledWith(409);
182
- expect(mockRes.json).toHaveBeenCalledWith({
183
- success: false,
184
- error: 'This email is already subscribed'
185
- });
186
- });
187
- it('should allow resubscription of unsubscribed users', async ()=>{
188
- seedCollection('subscribers', [
189
- {
190
- id: 'unsub-user',
191
- email: 'comeback@example.com',
192
- subscriptionStatus: 'unsubscribed'
193
- }
194
- ]);
195
- mockReq.body = {
196
- email: 'comeback@example.com'
197
- };
198
- await endpoint.handler(mockReq, mockRes);
199
- expect(mockReq.payload.update).toHaveBeenCalledWith(expect.objectContaining({
200
- id: 'unsub-user',
201
- data: expect.objectContaining({
202
- subscriptionStatus: 'pending',
203
- unsubscribedAt: null
204
- })
205
- }));
206
- expect(mockRes.status).toHaveBeenCalledWith(200);
207
- });
208
- });
209
- describe('Double Opt-In', ()=>{
210
- it('should send confirmation email when double opt-in is enabled', async ()=>{
211
- mockReq.body = {
212
- email: 'newuser@example.com'
213
- };
214
- await endpoint.handler(mockReq, mockRes);
215
- expect(mockEmailService.emails.send).toHaveBeenCalledWith(expect.objectContaining({
216
- to: [
217
- 'newuser@example.com'
218
- ],
219
- subject: expect.stringContaining('Welcome')
220
- }));
221
- expect(mockRes.json).toHaveBeenCalledWith({
222
- success: true,
223
- message: 'Please check your email to confirm your subscription',
224
- requiresConfirmation: true
225
- });
226
- });
227
- it('should activate immediately when double opt-in is disabled', async ()=>{
228
- // Disable double opt-in
229
- const noDoubleOptIn = {
230
- ...mockNewsletterSettings,
231
- subscriptionSettings: {
232
- ...mockNewsletterSettings.subscriptionSettings,
233
- requireDoubleOptIn: false
234
- }
235
- };
236
- clearCollections();
237
- seedCollection('newsletter-settings', [
238
- noDoubleOptIn
239
- ]);
240
- mockReq.body = {
241
- email: 'instant@example.com'
242
- };
243
- await endpoint.handler(mockReq, mockRes);
244
- expect(mockReq.payload.create).toHaveBeenCalledWith(expect.objectContaining({
245
- data: expect.objectContaining({
246
- subscriptionStatus: 'active'
247
- })
248
- }));
249
- });
250
- });
251
- describe('Error Handling', ()=>{
252
- it('should handle email service failures gracefully', async ()=>{
253
- mockEmailService.emails.send.mockRejectedValueOnce(new Error('Email service down'));
254
- mockReq.body = {
255
- email: 'test@example.com'
256
- };
257
- await endpoint.handler(mockReq, mockRes);
258
- expect(mockRes.status).toHaveBeenCalledWith(500);
259
- expect(mockRes.json).toHaveBeenCalledWith({
260
- success: false,
261
- error: 'Failed to process subscription. Please try again later.'
262
- });
263
- });
264
- it('should not leak internal errors to users', async ()=>{
265
- mockReq.payload.create.mockRejectedValueOnce(new Error('Database connection failed'));
266
- mockReq.body = {
267
- email: 'test@example.com'
268
- };
269
- await endpoint.handler(mockReq, mockRes);
270
- expect(mockRes.status).toHaveBeenCalledWith(500);
271
- expect(mockRes.json).toHaveBeenCalledWith({
272
- success: false,
273
- error: 'Failed to process subscription. Please try again later.'
274
- });
275
- // Should not expose database error details
276
- });
277
- });
278
- });
279
-
280
- //# sourceMappingURL=subscribe.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../src/__tests__/integration/endpoints/subscribe.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport { createSubscribeEndpoint } from '../../../endpoints/subscribe'\nimport { createPayloadRequestMock, seedCollection, clearCollections } from '../../mocks/payload'\nimport { mockNewsletterSettings } from '../../fixtures/newsletter-settings'\nimport { createResendMock } from '../../mocks/email-providers'\nimport type { NewsletterPluginConfig } from '../../../types'\n\n// Mock email service\nvi.mock('../../../services/email', () => ({\n getEmailService: vi.fn(),\n}))\n\nimport { getEmailService } from '../../../services/email'\n\ndescribe('Subscribe Endpoint Security', () => {\n let endpoint: any\n let mockReq: any\n let mockRes: any\n let mockEmailService: any\n const config: NewsletterPluginConfig = {\n subscribersSlug: 'subscribers',\n settingsSlug: 'newsletter-settings',\n }\n\n beforeEach(() => {\n clearCollections()\n seedCollection('newsletter-settings', [mockNewsletterSettings])\n \n endpoint = createSubscribeEndpoint(config)\n const payloadMock = createPayloadRequestMock()\n \n mockReq = {\n payload: payloadMock.payload,\n body: {},\n ip: '127.0.0.1',\n }\n \n mockRes = {\n status: vi.fn().mockReturnThis(),\n json: vi.fn(),\n }\n \n // Setup email service mock\n mockEmailService = createResendMock()\n ;(getEmailService as any).mockResolvedValue(mockEmailService)\n \n vi.clearAllMocks()\n })\n\n describe('Input Validation', () => {\n it('should reject requests without email', async () => {\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(400)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Email is required',\n })\n })\n\n it('should reject invalid email formats', async () => {\n const invalidEmails = [\n 'notanemail',\n '@example.com',\n 'user@',\n 'user @example.com',\n 'user@example',\n '<script>alert(\"xss\")</script>@example.com',\n ]\n\n for (const email of invalidEmails) {\n mockReq.body = { email }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(400)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Invalid email format',\n })\n }\n })\n\n it('should sanitize email input', async () => {\n mockReq.body = { \n email: ' User@EXAMPLE.com ',\n name: '<script>alert(\"xss\")</script>Test User',\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n // Check that create was called with sanitized data\n expect(mockReq.payload.create).toHaveBeenCalledWith(\n expect.objectContaining({\n data: expect.objectContaining({\n email: 'user@example.com', // Trimmed and lowercased\n name: 'Test User', // XSS stripped\n }),\n })\n )\n })\n })\n\n describe('Rate Limiting', () => {\n it('should enforce max subscribers per IP', async () => {\n // Create max subscribers from same IP\n const maxSubscribers = mockNewsletterSettings.subscriptionSettings.maxSubscribersPerIP\n \n for (let i = 0; i < maxSubscribers; i++) {\n seedCollection('subscribers', [{\n id: `sub-ip-${i}`,\n email: `user${i}@example.com`,\n ip: '127.0.0.1',\n subscriptionStatus: 'active',\n }])\n }\n\n mockReq.body = { email: 'newuser@example.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(429)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Too many subscription attempts from this IP address',\n })\n })\n\n it('should not count unsubscribed users in rate limit', async () => {\n // Create some unsubscribed users\n for (let i = 0; i < 5; i++) {\n seedCollection('subscribers', [{\n id: `sub-unsub-${i}`,\n email: `unsub${i}@example.com`,\n ip: '127.0.0.1',\n subscriptionStatus: 'unsubscribed',\n }])\n }\n\n mockReq.body = { email: 'newuser@example.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(200)\n })\n })\n\n describe('Domain Restrictions', () => {\n it('should enforce allowed domains when configured', async () => {\n // Update settings to restrict domains\n const restrictedSettings = {\n ...mockNewsletterSettings,\n subscriptionSettings: {\n ...mockNewsletterSettings.subscriptionSettings,\n allowedDomains: [\n { domain: 'allowed.com' },\n { domain: 'company.com' },\n ],\n },\n }\n clearCollections()\n seedCollection('newsletter-settings', [restrictedSettings])\n\n // Test blocked domain\n mockReq.body = { email: 'user@blocked.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(403)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Email domain not allowed',\n })\n\n // Test allowed domain\n mockReq.body = { email: 'user@allowed.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(200)\n })\n })\n\n describe('Duplicate Prevention', () => {\n it('should handle existing active subscribers', async () => {\n seedCollection('subscribers', [{\n id: 'existing-sub',\n email: 'existing@example.com',\n subscriptionStatus: 'active',\n }])\n\n mockReq.body = { email: 'existing@example.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(409)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'This email is already subscribed',\n })\n })\n\n it('should allow resubscription of unsubscribed users', async () => {\n seedCollection('subscribers', [{\n id: 'unsub-user',\n email: 'comeback@example.com',\n subscriptionStatus: 'unsubscribed',\n }])\n\n mockReq.body = { email: 'comeback@example.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockReq.payload.update).toHaveBeenCalledWith(\n expect.objectContaining({\n id: 'unsub-user',\n data: expect.objectContaining({\n subscriptionStatus: 'pending',\n unsubscribedAt: null,\n }),\n })\n )\n expect(mockRes.status).toHaveBeenCalledWith(200)\n })\n })\n\n describe('Double Opt-In', () => {\n it('should send confirmation email when double opt-in is enabled', async () => {\n mockReq.body = { email: 'newuser@example.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockEmailService.emails.send).toHaveBeenCalledWith(\n expect.objectContaining({\n to: ['newuser@example.com'],\n subject: expect.stringContaining('Welcome'),\n })\n )\n \n expect(mockRes.json).toHaveBeenCalledWith({\n success: true,\n message: 'Please check your email to confirm your subscription',\n requiresConfirmation: true,\n })\n })\n\n it('should activate immediately when double opt-in is disabled', async () => {\n // Disable double opt-in\n const noDoubleOptIn = {\n ...mockNewsletterSettings,\n subscriptionSettings: {\n ...mockNewsletterSettings.subscriptionSettings,\n requireDoubleOptIn: false,\n },\n }\n clearCollections()\n seedCollection('newsletter-settings', [noDoubleOptIn])\n\n mockReq.body = { email: 'instant@example.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockReq.payload.create).toHaveBeenCalledWith(\n expect.objectContaining({\n data: expect.objectContaining({\n subscriptionStatus: 'active',\n }),\n })\n )\n })\n })\n\n describe('Error Handling', () => {\n it('should handle email service failures gracefully', async () => {\n mockEmailService.emails.send.mockRejectedValueOnce(new Error('Email service down'))\n\n mockReq.body = { email: 'test@example.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(500)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Failed to process subscription. Please try again later.',\n })\n })\n\n it('should not leak internal errors to users', async () => {\n mockReq.payload.create.mockRejectedValueOnce(new Error('Database connection failed'))\n\n mockReq.body = { email: 'test@example.com' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(500)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Failed to process subscription. Please try again later.',\n })\n // Should not expose database error details\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","createSubscribeEndpoint","createPayloadRequestMock","seedCollection","clearCollections","mockNewsletterSettings","createResendMock","mock","getEmailService","fn","endpoint","mockReq","mockRes","mockEmailService","config","subscribersSlug","settingsSlug","payloadMock","payload","body","ip","status","mockReturnThis","json","mockResolvedValue","clearAllMocks","handler","toHaveBeenCalledWith","success","error","invalidEmails","email","name","create","objectContaining","data","maxSubscribers","subscriptionSettings","maxSubscribersPerIP","i","id","subscriptionStatus","restrictedSettings","allowedDomains","domain","update","unsubscribedAt","emails","send","to","subject","stringContaining","message","requiresConfirmation","noDoubleOptIn","requireDoubleOptIn","mockRejectedValueOnce","Error"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,EAAE,EAAEC,MAAM,EAAEC,UAAU,EAAEC,EAAE,QAAQ,SAAQ;AAC7D,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,wBAAwB,EAAEC,cAAc,EAAEC,gBAAgB,QAAQ,sBAAqB;AAChG,SAASC,sBAAsB,QAAQ,qCAAoC;AAC3E,SAASC,gBAAgB,QAAQ,8BAA6B;AAG9D,qBAAqB;AACrBN,GAAGO,IAAI,CAAC,2BAA2B,IAAO,CAAA;QACxCC,iBAAiBR,GAAGS,EAAE;IACxB,CAAA;AAEA,SAASD,eAAe,QAAQ,0BAAyB;AAEzDZ,SAAS,+BAA+B;IACtC,IAAIc;IACJ,IAAIC;IACJ,IAAIC;IACJ,IAAIC;IACJ,MAAMC,SAAiC;QACrCC,iBAAiB;QACjBC,cAAc;IAChB;IAEAjB,WAAW;QACTK;QACAD,eAAe,uBAAuB;YAACE;SAAuB;QAE9DK,WAAWT,wBAAwBa;QACnC,MAAMG,cAAcf;QAEpBS,UAAU;YACRO,SAASD,YAAYC,OAAO;YAC5BC,MAAM,CAAC;YACPC,IAAI;QACN;QAEAR,UAAU;YACRS,QAAQrB,GAAGS,EAAE,GAAGa,cAAc;YAC9BC,MAAMvB,GAAGS,EAAE;QACb;QAEA,2BAA2B;QAC3BI,mBAAmBP;QACjBE,gBAAwBgB,iBAAiB,CAACX;QAE5Cb,GAAGyB,aAAa;IAClB;IAEA7B,SAAS,oBAAoB;QAC3BC,GAAG,wCAAwC;YACzC,MAAMa,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;YAC5C7B,OAAOc,QAAQW,IAAI,EAAEI,oBAAoB,CAAC;gBACxCC,SAAS;gBACTC,OAAO;YACT;QACF;QAEAhC,GAAG,uCAAuC;YACxC,MAAMiC,gBAAgB;gBACpB;gBACA;gBACA;gBACA;gBACA;gBACA;aACD;YAED,KAAK,MAAMC,SAASD,cAAe;gBACjCnB,QAAQQ,IAAI,GAAG;oBAAEY;gBAAM;gBACvB,MAAMrB,SAASgB,OAAO,CAACf,SAASC;gBAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;gBAC5C7B,OAAOc,QAAQW,IAAI,EAAEI,oBAAoB,CAAC;oBACxCC,SAAS;oBACTC,OAAO;gBACT;YACF;QACF;QAEAhC,GAAG,+BAA+B;YAChCc,QAAQQ,IAAI,GAAG;gBACbY,OAAO;gBACPC,MAAM;YACR;YAEA,MAAMtB,SAASgB,OAAO,CAACf,SAASC;YAEhC,mDAAmD;YACnDd,OAAOa,QAAQO,OAAO,CAACe,MAAM,EAAEN,oBAAoB,CACjD7B,OAAOoC,gBAAgB,CAAC;gBACtBC,MAAMrC,OAAOoC,gBAAgB,CAAC;oBAC5BH,OAAO;oBACPC,MAAM;gBACR;YACF;QAEJ;IACF;IAEApC,SAAS,iBAAiB;QACxBC,GAAG,yCAAyC;YAC1C,sCAAsC;YACtC,MAAMuC,iBAAiB/B,uBAAuBgC,oBAAoB,CAACC,mBAAmB;YAEtF,IAAK,IAAIC,IAAI,GAAGA,IAAIH,gBAAgBG,IAAK;gBACvCpC,eAAe,eAAe;oBAAC;wBAC7BqC,IAAI,CAAC,OAAO,EAAED,GAAG;wBACjBR,OAAO,CAAC,IAAI,EAAEQ,EAAE,YAAY,CAAC;wBAC7BnB,IAAI;wBACJqB,oBAAoB;oBACtB;iBAAE;YACJ;YAEA9B,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAsB;YAC9C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;YAC5C7B,OAAOc,QAAQW,IAAI,EAAEI,oBAAoB,CAAC;gBACxCC,SAAS;gBACTC,OAAO;YACT;QACF;QAEAhC,GAAG,qDAAqD;YACtD,iCAAiC;YACjC,IAAK,IAAI0C,IAAI,GAAGA,IAAI,GAAGA,IAAK;gBAC1BpC,eAAe,eAAe;oBAAC;wBAC7BqC,IAAI,CAAC,UAAU,EAAED,GAAG;wBACpBR,OAAO,CAAC,KAAK,EAAEQ,EAAE,YAAY,CAAC;wBAC9BnB,IAAI;wBACJqB,oBAAoB;oBACtB;iBAAE;YACJ;YAEA9B,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAsB;YAC9C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;QAC9C;IACF;IAEA/B,SAAS,uBAAuB;QAC9BC,GAAG,kDAAkD;YACnD,sCAAsC;YACtC,MAAM6C,qBAAqB;gBACzB,GAAGrC,sBAAsB;gBACzBgC,sBAAsB;oBACpB,GAAGhC,uBAAuBgC,oBAAoB;oBAC9CM,gBAAgB;wBACd;4BAAEC,QAAQ;wBAAc;wBACxB;4BAAEA,QAAQ;wBAAc;qBACzB;gBACH;YACF;YACAxC;YACAD,eAAe,uBAAuB;gBAACuC;aAAmB;YAE1D,sBAAsB;YACtB/B,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAmB;YAC3C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;YAC5C7B,OAAOc,QAAQW,IAAI,EAAEI,oBAAoB,CAAC;gBACxCC,SAAS;gBACTC,OAAO;YACT;YAEA,sBAAsB;YACtBlB,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAmB;YAC3C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;QAC9C;IACF;IAEA/B,SAAS,wBAAwB;QAC/BC,GAAG,6CAA6C;YAC9CM,eAAe,eAAe;gBAAC;oBAC7BqC,IAAI;oBACJT,OAAO;oBACPU,oBAAoB;gBACtB;aAAE;YAEF9B,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAuB;YAC/C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;YAC5C7B,OAAOc,QAAQW,IAAI,EAAEI,oBAAoB,CAAC;gBACxCC,SAAS;gBACTC,OAAO;YACT;QACF;QAEAhC,GAAG,qDAAqD;YACtDM,eAAe,eAAe;gBAAC;oBAC7BqC,IAAI;oBACJT,OAAO;oBACPU,oBAAoB;gBACtB;aAAE;YAEF9B,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAuB;YAC/C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOa,QAAQO,OAAO,CAAC2B,MAAM,EAAElB,oBAAoB,CACjD7B,OAAOoC,gBAAgB,CAAC;gBACtBM,IAAI;gBACJL,MAAMrC,OAAOoC,gBAAgB,CAAC;oBAC5BO,oBAAoB;oBACpBK,gBAAgB;gBAClB;YACF;YAEFhD,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;QAC9C;IACF;IAEA/B,SAAS,iBAAiB;QACxBC,GAAG,gEAAgE;YACjEc,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAsB;YAC9C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOe,iBAAiBkC,MAAM,CAACC,IAAI,EAAErB,oBAAoB,CACvD7B,OAAOoC,gBAAgB,CAAC;gBACtBe,IAAI;oBAAC;iBAAsB;gBAC3BC,SAASpD,OAAOqD,gBAAgB,CAAC;YACnC;YAGFrD,OAAOc,QAAQW,IAAI,EAAEI,oBAAoB,CAAC;gBACxCC,SAAS;gBACTwB,SAAS;gBACTC,sBAAsB;YACxB;QACF;QAEAxD,GAAG,8DAA8D;YAC/D,wBAAwB;YACxB,MAAMyD,gBAAgB;gBACpB,GAAGjD,sBAAsB;gBACzBgC,sBAAsB;oBACpB,GAAGhC,uBAAuBgC,oBAAoB;oBAC9CkB,oBAAoB;gBACtB;YACF;YACAnD;YACAD,eAAe,uBAAuB;gBAACmD;aAAc;YAErD3C,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAsB;YAC9C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOa,QAAQO,OAAO,CAACe,MAAM,EAAEN,oBAAoB,CACjD7B,OAAOoC,gBAAgB,CAAC;gBACtBC,MAAMrC,OAAOoC,gBAAgB,CAAC;oBAC5BO,oBAAoB;gBACtB;YACF;QAEJ;IACF;IAEA7C,SAAS,kBAAkB;QACzBC,GAAG,mDAAmD;YACpDgB,iBAAiBkC,MAAM,CAACC,IAAI,CAACQ,qBAAqB,CAAC,IAAIC,MAAM;YAE7D9C,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAmB;YAC3C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;YAC5C7B,OAAOc,QAAQW,IAAI,EAAEI,oBAAoB,CAAC;gBACxCC,SAAS;gBACTC,OAAO;YACT;QACF;QAEAhC,GAAG,4CAA4C;YAC7Cc,QAAQO,OAAO,CAACe,MAAM,CAACuB,qBAAqB,CAAC,IAAIC,MAAM;YAEvD9C,QAAQQ,IAAI,GAAG;gBAAEY,OAAO;YAAmB;YAC3C,MAAMrB,SAASgB,OAAO,CAACf,SAASC;YAEhCd,OAAOc,QAAQS,MAAM,EAAEM,oBAAoB,CAAC;YAC5C7B,OAAOc,QAAQW,IAAI,EAAEI,oBAAoB,CAAC;gBACxCC,SAAS;gBACTC,OAAO;YACT;QACA,2CAA2C;QAC7C;IACF;AACF"}
@@ -1,187 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { createUnsubscribeEndpoint } from '../../../endpoints/unsubscribe';
3
- import { createPayloadRequestMock, seedCollection, clearCollections, createMockUser } from '../../mocks/payload';
4
- import { mockSubscribers } from '../../fixtures/subscribers';
5
- describe('Unsubscribe Endpoint Security', ()=>{
6
- let endpoint;
7
- let mockReq;
8
- let mockRes;
9
- const config = {
10
- subscribersSlug: 'subscribers'
11
- };
12
- beforeEach(()=>{
13
- clearCollections();
14
- seedCollection('subscribers', mockSubscribers);
15
- endpoint = createUnsubscribeEndpoint(config);
16
- const payloadMock = createPayloadRequestMock();
17
- mockReq = {
18
- payload: payloadMock.payload,
19
- body: {},
20
- user: null
21
- };
22
- mockRes = {
23
- status: vi.fn().mockReturnThis(),
24
- json: vi.fn()
25
- };
26
- vi.clearAllMocks();
27
- });
28
- describe('Authentication Required', ()=>{
29
- it('should reject unauthenticated requests', async ()=>{
30
- mockReq.body = {
31
- subscriberId: 'sub-1'
32
- };
33
- await endpoint.handler(mockReq, mockRes);
34
- expect(mockRes.status).toHaveBeenCalledWith(401);
35
- expect(mockRes.json).toHaveBeenCalledWith({
36
- success: false,
37
- error: 'Authentication required'
38
- });
39
- });
40
- it('should reject requests without subscriber ID', async ()=>{
41
- mockReq.user = {
42
- id: 'sub-1',
43
- email: 'active@example.com',
44
- collection: 'subscribers'
45
- };
46
- await endpoint.handler(mockReq, mockRes);
47
- expect(mockRes.status).toHaveBeenCalledWith(400);
48
- expect(mockRes.json).toHaveBeenCalledWith({
49
- success: false,
50
- error: 'Subscriber ID is required'
51
- });
52
- });
53
- });
54
- describe('Access Control', ()=>{
55
- it('should allow subscribers to unsubscribe themselves', async ()=>{
56
- mockReq.user = {
57
- id: 'sub-1',
58
- email: 'active@example.com',
59
- collection: 'subscribers'
60
- };
61
- mockReq.body = {
62
- subscriberId: 'sub-1'
63
- };
64
- await endpoint.handler(mockReq, mockRes);
65
- expect(mockReq.payload.update).toHaveBeenCalledWith({
66
- collection: 'subscribers',
67
- id: 'sub-1',
68
- data: {
69
- subscriptionStatus: 'unsubscribed',
70
- unsubscribedAt: expect.any(Date)
71
- },
72
- overrideAccess: false,
73
- user: mockReq.user
74
- });
75
- expect(mockRes.status).toHaveBeenCalledWith(200);
76
- });
77
- it('should prevent subscribers from unsubscribing others', async ()=>{
78
- mockReq.user = {
79
- id: 'sub-1',
80
- email: 'active@example.com',
81
- collection: 'subscribers'
82
- };
83
- mockReq.body = {
84
- subscriberId: 'sub-2'
85
- }; // Trying to unsubscribe someone else
86
- await endpoint.handler(mockReq, mockRes);
87
- expect(mockRes.status).toHaveBeenCalledWith(403);
88
- expect(mockRes.json).toHaveBeenCalledWith({
89
- success: false,
90
- error: 'You can only unsubscribe yourself'
91
- });
92
- });
93
- it('should prevent regular users from unsubscribing subscribers', async ()=>{
94
- mockReq.user = createMockUser({
95
- id: 'user-123'
96
- });
97
- mockReq.body = {
98
- subscriberId: 'sub-1'
99
- };
100
- await endpoint.handler(mockReq, mockRes);
101
- expect(mockRes.status).toHaveBeenCalledWith(403);
102
- expect(mockRes.json).toHaveBeenCalledWith({
103
- success: false,
104
- error: 'Invalid user type for this operation'
105
- });
106
- });
107
- });
108
- describe('Already Unsubscribed', ()=>{
109
- it('should handle already unsubscribed users gracefully', async ()=>{
110
- mockReq.user = {
111
- id: 'sub-3',
112
- email: 'unsubscribed@example.com',
113
- collection: 'subscribers'
114
- };
115
- mockReq.body = {
116
- subscriberId: 'sub-3'
117
- };
118
- await endpoint.handler(mockReq, mockRes);
119
- expect(mockRes.status).toHaveBeenCalledWith(200);
120
- expect(mockRes.json).toHaveBeenCalledWith({
121
- success: true,
122
- message: 'Already unsubscribed'
123
- });
124
- // Should not call update
125
- expect(mockReq.payload.update).not.toHaveBeenCalled();
126
- });
127
- });
128
- describe('Non-existent Subscribers', ()=>{
129
- it('should handle non-existent subscriber IDs', async ()=>{
130
- mockReq.user = {
131
- id: 'sub-999',
132
- email: 'ghost@example.com',
133
- collection: 'subscribers'
134
- };
135
- mockReq.body = {
136
- subscriberId: 'sub-999'
137
- };
138
- await endpoint.handler(mockReq, mockRes);
139
- expect(mockRes.status).toHaveBeenCalledWith(404);
140
- expect(mockRes.json).toHaveBeenCalledWith({
141
- success: false,
142
- error: 'Subscriber not found'
143
- });
144
- });
145
- });
146
- describe('Data Integrity', ()=>{
147
- it('should set unsubscribedAt timestamp', async ()=>{
148
- const beforeTime = new Date();
149
- mockReq.user = {
150
- id: 'sub-1',
151
- email: 'active@example.com',
152
- collection: 'subscribers'
153
- };
154
- mockReq.body = {
155
- subscriberId: 'sub-1'
156
- };
157
- await endpoint.handler(mockReq, mockRes);
158
- const updateCall = mockReq.payload.update.mock.calls[0][0];
159
- const unsubscribedAt = new Date(updateCall.data.unsubscribedAt);
160
- const afterTime = new Date();
161
- // Verify timestamp is between test start and end
162
- expect(unsubscribedAt.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime());
163
- expect(unsubscribedAt.getTime()).toBeLessThanOrEqual(afterTime.getTime());
164
- });
165
- it('should clear email preferences on unsubscribe', async ()=>{
166
- mockReq.user = {
167
- id: 'sub-1',
168
- email: 'active@example.com',
169
- collection: 'subscribers'
170
- };
171
- mockReq.body = {
172
- subscriberId: 'sub-1'
173
- };
174
- await endpoint.handler(mockReq, mockRes);
175
- expect(mockReq.payload.update).toHaveBeenCalledWith(expect.objectContaining({
176
- data: expect.objectContaining({
177
- emailPreferences: {
178
- newsletter: false,
179
- announcements: false
180
- }
181
- })
182
- }));
183
- });
184
- });
185
- });
186
-
187
- //# sourceMappingURL=unsubscribe.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../src/__tests__/integration/endpoints/unsubscribe.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport { createUnsubscribeEndpoint } from '../../../endpoints/unsubscribe'\nimport { createPayloadRequestMock, seedCollection, clearCollections, createMockUser } from '../../mocks/payload'\nimport { mockSubscribers } from '../../fixtures/subscribers'\nimport type { NewsletterPluginConfig } from '../../../types'\n\ndescribe('Unsubscribe Endpoint Security', () => {\n let endpoint: any\n let mockReq: any\n let mockRes: any\n const config: NewsletterPluginConfig = {\n subscribersSlug: 'subscribers',\n }\n\n beforeEach(() => {\n clearCollections()\n seedCollection('subscribers', mockSubscribers)\n \n endpoint = createUnsubscribeEndpoint(config)\n const payloadMock = createPayloadRequestMock()\n \n mockReq = {\n payload: payloadMock.payload,\n body: {},\n user: null,\n }\n \n mockRes = {\n status: vi.fn().mockReturnThis(),\n json: vi.fn(),\n }\n \n vi.clearAllMocks()\n })\n\n describe('Authentication Required', () => {\n it('should reject unauthenticated requests', async () => {\n mockReq.body = { subscriberId: 'sub-1' }\n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(401)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Authentication required',\n })\n })\n\n it('should reject requests without subscriber ID', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(400)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Subscriber ID is required',\n })\n })\n })\n\n describe('Access Control', () => {\n it('should allow subscribers to unsubscribe themselves', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.body = { subscriberId: 'sub-1' }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockReq.payload.update).toHaveBeenCalledWith({\n collection: 'subscribers',\n id: 'sub-1',\n data: {\n subscriptionStatus: 'unsubscribed',\n unsubscribedAt: expect.any(Date),\n },\n overrideAccess: false,\n user: mockReq.user,\n })\n \n expect(mockRes.status).toHaveBeenCalledWith(200)\n })\n\n it('should prevent subscribers from unsubscribing others', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.body = { subscriberId: 'sub-2' } // Trying to unsubscribe someone else\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(403)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'You can only unsubscribe yourself',\n })\n })\n\n it('should prevent regular users from unsubscribing subscribers', async () => {\n mockReq.user = createMockUser({ id: 'user-123' })\n mockReq.body = { subscriberId: 'sub-1' }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(403)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Invalid user type for this operation',\n })\n })\n })\n\n describe('Already Unsubscribed', () => {\n it('should handle already unsubscribed users gracefully', async () => {\n mockReq.user = {\n id: 'sub-3', // Already unsubscribed\n email: 'unsubscribed@example.com',\n collection: 'subscribers',\n }\n mockReq.body = { subscriberId: 'sub-3' }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(200)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: true,\n message: 'Already unsubscribed',\n })\n \n // Should not call update\n expect(mockReq.payload.update).not.toHaveBeenCalled()\n })\n })\n\n describe('Non-existent Subscribers', () => {\n it('should handle non-existent subscriber IDs', async () => {\n mockReq.user = {\n id: 'sub-999',\n email: 'ghost@example.com',\n collection: 'subscribers',\n }\n mockReq.body = { subscriberId: 'sub-999' }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockRes.status).toHaveBeenCalledWith(404)\n expect(mockRes.json).toHaveBeenCalledWith({\n success: false,\n error: 'Subscriber not found',\n })\n })\n })\n\n describe('Data Integrity', () => {\n it('should set unsubscribedAt timestamp', async () => {\n const beforeTime = new Date()\n \n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.body = { subscriberId: 'sub-1' }\n \n await endpoint.handler(mockReq, mockRes)\n \n const updateCall = mockReq.payload.update.mock.calls[0][0]\n const unsubscribedAt = new Date(updateCall.data.unsubscribedAt)\n const afterTime = new Date()\n \n // Verify timestamp is between test start and end\n expect(unsubscribedAt.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime())\n expect(unsubscribedAt.getTime()).toBeLessThanOrEqual(afterTime.getTime())\n })\n\n it('should clear email preferences on unsubscribe', async () => {\n mockReq.user = {\n id: 'sub-1',\n email: 'active@example.com',\n collection: 'subscribers',\n }\n mockReq.body = { subscriberId: 'sub-1' }\n \n await endpoint.handler(mockReq, mockRes)\n \n expect(mockReq.payload.update).toHaveBeenCalledWith(\n expect.objectContaining({\n data: expect.objectContaining({\n emailPreferences: {\n newsletter: false,\n announcements: false,\n },\n }),\n })\n )\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","createUnsubscribeEndpoint","createPayloadRequestMock","seedCollection","clearCollections","createMockUser","mockSubscribers","endpoint","mockReq","mockRes","config","subscribersSlug","payloadMock","payload","body","user","status","fn","mockReturnThis","json","clearAllMocks","subscriberId","handler","toHaveBeenCalledWith","success","error","id","email","collection","update","data","subscriptionStatus","unsubscribedAt","any","Date","overrideAccess","message","not","toHaveBeenCalled","beforeTime","updateCall","mock","calls","afterTime","getTime","toBeGreaterThanOrEqual","toBeLessThanOrEqual","objectContaining","emailPreferences","newsletter","announcements"],"mappings":"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"}