payload-plugin-newsletter 0.3.1 → 0.3.2

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 (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CLAUDE.md +110 -0
  3. package/README.md +3 -3
  4. package/TEST_SUMMARY.md +152 -0
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/src/__tests__/fixtures/newsletter-settings.js +41 -0
  7. package/dist/src/__tests__/fixtures/newsletter-settings.js.map +1 -0
  8. package/dist/src/__tests__/fixtures/subscribers.js +70 -0
  9. package/dist/src/__tests__/fixtures/subscribers.js.map +1 -0
  10. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js +356 -0
  11. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js.map +1 -0
  12. package/dist/src/__tests__/integration/endpoints/preferences.test.js +266 -0
  13. package/dist/src/__tests__/integration/endpoints/preferences.test.js.map +1 -0
  14. package/dist/src/__tests__/integration/endpoints/subscribe.test.js +280 -0
  15. package/dist/src/__tests__/integration/endpoints/subscribe.test.js.map +1 -0
  16. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js +187 -0
  17. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js.map +1 -0
  18. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js +188 -0
  19. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js.map +1 -0
  20. package/dist/src/__tests__/mocks/email-providers.js +153 -0
  21. package/dist/src/__tests__/mocks/email-providers.js.map +1 -0
  22. package/dist/src/__tests__/mocks/payload.js +244 -0
  23. package/dist/src/__tests__/mocks/payload.js.map +1 -0
  24. package/dist/src/__tests__/security/csrf-protection.test.js +309 -0
  25. package/dist/src/__tests__/security/csrf-protection.test.js.map +1 -0
  26. package/dist/src/__tests__/security/settings-access.test.js +204 -0
  27. package/dist/src/__tests__/security/settings-access.test.js.map +1 -0
  28. package/dist/src/__tests__/security/subscriber-access.test.js +210 -0
  29. package/dist/src/__tests__/security/subscriber-access.test.js.map +1 -0
  30. package/dist/src/__tests__/security/xss-prevention.test.js +305 -0
  31. package/dist/src/__tests__/security/xss-prevention.test.js.map +1 -0
  32. package/dist/src/__tests__/setup/integration.setup.js +38 -0
  33. package/dist/src/__tests__/setup/integration.setup.js.map +1 -0
  34. package/dist/src/__tests__/setup/unit.setup.js +41 -0
  35. package/dist/src/__tests__/setup/unit.setup.js.map +1 -0
  36. package/dist/src/__tests__/unit/utils/access.test.js +116 -0
  37. package/dist/src/__tests__/unit/utils/access.test.js.map +1 -0
  38. package/dist/src/__tests__/unit/utils/jwt.test.js +238 -0
  39. package/dist/src/__tests__/unit/utils/jwt.test.js.map +1 -0
  40. package/dist/src/utils/access.js +28 -4
  41. package/dist/src/utils/access.js.map +1 -1
  42. package/dist/src/utils/validation.js +9 -1
  43. package/dist/src/utils/validation.js.map +1 -1
  44. package/dist/utils/access.d.ts.map +1 -1
  45. package/dist/utils/validation.d.ts.map +1 -1
  46. package/package.json +22 -4
@@ -0,0 +1,305 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { createPayloadRequestMock, seedCollection, clearCollections } from '../mocks/payload';
3
+ import { mockNewsletterSettings } from '../fixtures/newsletter-settings';
4
+ describe('XSS Prevention', ()=>{
5
+ let mockReq;
6
+ const config = {};
7
+ beforeEach(()=>{
8
+ clearCollections();
9
+ seedCollection('newsletter-settings', [
10
+ mockNewsletterSettings
11
+ ]);
12
+ const payloadMock = createPayloadRequestMock();
13
+ mockReq = {
14
+ payload: payloadMock.payload,
15
+ body: {}
16
+ };
17
+ vi.clearAllMocks();
18
+ });
19
+ describe('Input Sanitization', ()=>{
20
+ it('should sanitize subscriber name field', async ()=>{
21
+ const maliciousNames = [
22
+ '<script>alert("xss")</script>John',
23
+ 'John<img src=x onerror=alert("xss")>',
24
+ 'John<iframe src="javascript:alert(\'xss\')"></iframe>',
25
+ '<svg onload=alert("xss")>John</svg>',
26
+ 'John<body onload=alert("xss")>'
27
+ ];
28
+ for (const maliciousName of maliciousNames){
29
+ const result = await mockReq.payload.create({
30
+ collection: 'subscribers',
31
+ data: {
32
+ email: `test${Date.now()}@example.com`,
33
+ name: maliciousName
34
+ }
35
+ });
36
+ // Name should be sanitized (implementation dependent)
37
+ expect(result.name).not.toContain('<script>');
38
+ expect(result.name).not.toContain('alert(');
39
+ expect(result.name).not.toContain('onerror=');
40
+ expect(result.name).not.toContain('javascript:');
41
+ }
42
+ });
43
+ it('should not allow HTML in email addresses', async ()=>{
44
+ const maliciousEmails = [
45
+ 'user<script>alert("xss")</script>@example.com',
46
+ 'user@example.com<img src=x onerror=alert("xss")>',
47
+ '<user@example.com>'
48
+ ];
49
+ for (const maliciousEmail of maliciousEmails){
50
+ try {
51
+ await mockReq.payload.create({
52
+ collection: 'subscribers',
53
+ data: {
54
+ email: maliciousEmail,
55
+ name: 'Test User'
56
+ }
57
+ });
58
+ } catch (error) {
59
+ // Should fail validation
60
+ expect(error.message).toContain('Invalid email');
61
+ }
62
+ }
63
+ });
64
+ it('should sanitize custom fields', async ()=>{
65
+ const result = await mockReq.payload.create({
66
+ collection: 'subscribers',
67
+ data: {
68
+ email: 'test@example.com',
69
+ name: 'Test User',
70
+ customField: '<script>alert("xss")</script>Custom Value'
71
+ }
72
+ });
73
+ if (result.customField) {
74
+ expect(result.customField).not.toContain('<script>');
75
+ expect(result.customField).not.toContain('alert(');
76
+ }
77
+ });
78
+ });
79
+ describe('Template Injection Prevention', ()=>{
80
+ it('should prevent template injection in email subjects', async ()=>{
81
+ const maliciousSubjects = [
82
+ '{{process.env.JWT_SECRET}}',
83
+ '${process.env.JWT_SECRET}',
84
+ '<%= process.env.JWT_SECRET %>',
85
+ '#{process.env.JWT_SECRET}'
86
+ ];
87
+ for (const subject of maliciousSubjects){
88
+ const settings = await mockReq.payload.update({
89
+ collection: 'newsletter-settings',
90
+ id: 'settings-1',
91
+ data: {
92
+ emailTemplates: {
93
+ welcome: {
94
+ subject: subject
95
+ }
96
+ }
97
+ }
98
+ });
99
+ // Subject should be treated as literal string, not evaluated
100
+ expect(settings.emailTemplates.welcome.subject).toBe(subject);
101
+ // When used, should not expose secrets
102
+ }
103
+ });
104
+ it('should escape user data in email templates', ()=>{
105
+ // Template rendering function (example)
106
+ const renderTemplate = (template, data)=>{
107
+ // Should escape HTML entities
108
+ const escaped = {};
109
+ for (const [key, value] of Object.entries(data)){
110
+ if (typeof value === 'string') {
111
+ escaped[key] = value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
112
+ } else {
113
+ escaped[key] = value;
114
+ }
115
+ }
116
+ // Simple template replacement
117
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key)=>escaped[key] || '');
118
+ };
119
+ const template = '<p>Hello {{name}}, welcome to {{newsletter}}!</p>';
120
+ const maliciousData = {
121
+ name: '<script>alert("xss")</script>',
122
+ newsletter: 'Test Newsletter<img src=x onerror=alert("xss")>'
123
+ };
124
+ const rendered = renderTemplate(template, maliciousData);
125
+ expect(rendered).not.toContain('<script>');
126
+ expect(rendered).not.toContain('<img src=x onerror=');
127
+ expect(rendered).toContain('&lt;script&gt;');
128
+ expect(rendered).toContain('&lt;img');
129
+ });
130
+ });
131
+ describe('Content Security Policy', ()=>{
132
+ it('should set appropriate CSP headers for admin UI', ()=>{
133
+ // Mock response headers
134
+ const headers = {};
135
+ // CSP middleware (example)
136
+ const setCSPHeaders = (res)=>{
137
+ res.setHeader('Content-Security-Policy', [
138
+ "default-src 'self'",
139
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
140
+ "style-src 'self' 'unsafe-inline'",
141
+ "img-src 'self' data: https:",
142
+ "font-src 'self'",
143
+ "connect-src 'self'",
144
+ "frame-ancestors 'none'",
145
+ "base-uri 'self'",
146
+ "form-action 'self'"
147
+ ].join('; '));
148
+ };
149
+ const mockRes = {
150
+ setHeader: (name, value)=>{
151
+ headers[name] = value;
152
+ }
153
+ };
154
+ setCSPHeaders(mockRes);
155
+ expect(headers['Content-Security-Policy']).toContain("default-src 'self'");
156
+ expect(headers['Content-Security-Policy']).toContain("frame-ancestors 'none'");
157
+ });
158
+ });
159
+ describe('JSON Injection Prevention', ()=>{
160
+ it('should prevent JSON injection in API responses', async ()=>{
161
+ const maliciousData = {
162
+ email: 'test@example.com',
163
+ name: 'Test", "isAdmin": true, "name": "Hacked'
164
+ };
165
+ const result = await mockReq.payload.create({
166
+ collection: 'subscribers',
167
+ data: maliciousData
168
+ });
169
+ // The name should be stored as a string, not parsed as JSON
170
+ expect(result.name).toBe(maliciousData.name);
171
+ expect(result.isAdmin).toBeUndefined();
172
+ });
173
+ it('should properly escape JSON in responses', ()=>{
174
+ const escapeJSON = (data)=>{
175
+ return JSON.stringify(data).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
176
+ };
177
+ const data = {
178
+ name: 'Test\u2028User\u2029',
179
+ email: 'test@example.com'
180
+ };
181
+ const escaped = escapeJSON(data);
182
+ expect(escaped).not.toContain('\u2028');
183
+ expect(escaped).not.toContain('\u2029');
184
+ });
185
+ });
186
+ describe('URL Injection Prevention', ()=>{
187
+ it('should validate redirect URLs', ()=>{
188
+ const validateRedirectUrl = (url, allowedHosts)=>{
189
+ try {
190
+ const parsed = new URL(url);
191
+ return allowedHosts.includes(parsed.host);
192
+ } catch {
193
+ return false;
194
+ }
195
+ };
196
+ const allowedHosts = [
197
+ 'example.com',
198
+ 'app.example.com'
199
+ ];
200
+ // Valid URLs
201
+ expect(validateRedirectUrl('https://example.com/preferences', allowedHosts)).toBe(true);
202
+ expect(validateRedirectUrl('https://app.example.com/unsubscribe', allowedHosts)).toBe(true);
203
+ // Invalid URLs
204
+ expect(validateRedirectUrl('https://evil.com/phishing', allowedHosts)).toBe(false);
205
+ expect(validateRedirectUrl('javascript:alert("xss")', allowedHosts)).toBe(false);
206
+ expect(validateRedirectUrl('data:text/html,<script>alert("xss")</script>', allowedHosts)).toBe(false);
207
+ });
208
+ it('should sanitize magic link URLs', ()=>{
209
+ const generateMagicLink = (baseUrl, token)=>{
210
+ // Validate base URL
211
+ try {
212
+ const url = new URL(baseUrl);
213
+ if (![
214
+ 'http:',
215
+ 'https:'
216
+ ].includes(url.protocol)) {
217
+ throw new Error('Invalid protocol');
218
+ }
219
+ // Encode token to prevent injection
220
+ url.searchParams.set('token', encodeURIComponent(token));
221
+ return url.toString();
222
+ } catch {
223
+ throw new Error('Invalid base URL');
224
+ }
225
+ };
226
+ // Valid usage
227
+ const link = generateMagicLink('https://example.com/verify', 'abc123');
228
+ expect(link).toBe('https://example.com/verify?token=abc123');
229
+ // Token with special characters
230
+ const maliciousToken = '"><script>alert("xss")</script>';
231
+ const safeLink = generateMagicLink('https://example.com/verify', maliciousToken);
232
+ expect(safeLink).not.toContain('<script>');
233
+ // Verify the token is properly encoded (double-encoding is actually safer)
234
+ expect(safeLink).toContain('%253Cscript%253E');
235
+ expect(safeLink).toContain('%2522');
236
+ // Invalid base URLs
237
+ expect(()=>generateMagicLink('javascript:alert("xss")', 'token')).toThrow();
238
+ expect(()=>generateMagicLink('data:text/html,test', 'token')).toThrow();
239
+ });
240
+ });
241
+ describe('MongoDB Injection Prevention', ()=>{
242
+ it('should prevent NoSQL injection in queries', async ()=>{
243
+ // Malicious input attempting to bypass authentication
244
+ const maliciousInputs = [
245
+ {
246
+ email: {
247
+ $ne: null
248
+ }
249
+ },
250
+ {
251
+ email: {
252
+ $regex: '.*'
253
+ }
254
+ },
255
+ {
256
+ email: {
257
+ $where: 'this.isAdmin == true'
258
+ }
259
+ }
260
+ ];
261
+ for (const input of maliciousInputs){
262
+ try {
263
+ await mockReq.payload.find({
264
+ collection: 'subscribers',
265
+ where: input
266
+ });
267
+ } catch (error) {
268
+ // Should either sanitize or reject
269
+ expect(error.message).toContain('Invalid');
270
+ }
271
+ }
272
+ });
273
+ it('should sanitize field names', async ()=>{
274
+ const maliciousFields = [
275
+ {
276
+ '$where': 'malicious code'
277
+ },
278
+ {
279
+ '__proto__': {
280
+ isAdmin: true
281
+ }
282
+ },
283
+ {
284
+ 'constructor.prototype.isAdmin': true
285
+ }
286
+ ];
287
+ for (const fields of maliciousFields){
288
+ try {
289
+ await mockReq.payload.create({
290
+ collection: 'subscribers',
291
+ data: {
292
+ email: 'test@example.com',
293
+ ...fields
294
+ }
295
+ });
296
+ } catch (error) {
297
+ // Should reject dangerous field names
298
+ expect(error).toBeDefined();
299
+ }
300
+ }
301
+ });
302
+ });
303
+ });
304
+
305
+ //# sourceMappingURL=xss-prevention.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/__tests__/security/xss-prevention.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport { createPayloadRequestMock, seedCollection, clearCollections } from '../mocks/payload'\nimport { mockNewsletterSettings } from '../fixtures/newsletter-settings'\nimport type { NewsletterPluginConfig } from '../../types'\n\ndescribe('XSS Prevention', () => {\n let mockReq: any\n const config: NewsletterPluginConfig = {}\n\n beforeEach(() => {\n clearCollections()\n seedCollection('newsletter-settings', [mockNewsletterSettings])\n \n const payloadMock = createPayloadRequestMock()\n mockReq = {\n payload: payloadMock.payload,\n body: {},\n }\n \n vi.clearAllMocks()\n })\n\n describe('Input Sanitization', () => {\n it('should sanitize subscriber name field', async () => {\n const maliciousNames = [\n '<script>alert(\"xss\")</script>John',\n 'John<img src=x onerror=alert(\"xss\")>',\n 'John<iframe src=\"javascript:alert(\\'xss\\')\"></iframe>',\n '<svg onload=alert(\"xss\")>John</svg>',\n 'John<body onload=alert(\"xss\")>',\n ]\n\n for (const maliciousName of maliciousNames) {\n const result = await mockReq.payload.create({\n collection: 'subscribers',\n data: {\n email: `test${Date.now()}@example.com`,\n name: maliciousName,\n },\n })\n\n // Name should be sanitized (implementation dependent)\n expect(result.name).not.toContain('<script>')\n expect(result.name).not.toContain('alert(')\n expect(result.name).not.toContain('onerror=')\n expect(result.name).not.toContain('javascript:')\n }\n })\n\n it('should not allow HTML in email addresses', async () => {\n const maliciousEmails = [\n 'user<script>alert(\"xss\")</script>@example.com',\n 'user@example.com<img src=x onerror=alert(\"xss\")>',\n '<user@example.com>',\n ]\n\n for (const maliciousEmail of maliciousEmails) {\n try {\n await mockReq.payload.create({\n collection: 'subscribers',\n data: {\n email: maliciousEmail,\n name: 'Test User',\n },\n })\n } catch (error: any) {\n // Should fail validation\n expect(error.message).toContain('Invalid email')\n }\n }\n })\n\n it('should sanitize custom fields', async () => {\n const result = await mockReq.payload.create({\n collection: 'subscribers',\n data: {\n email: 'test@example.com',\n name: 'Test User',\n customField: '<script>alert(\"xss\")</script>Custom Value',\n },\n })\n\n if (result.customField) {\n expect(result.customField).not.toContain('<script>')\n expect(result.customField).not.toContain('alert(')\n }\n })\n })\n\n describe('Template Injection Prevention', () => {\n it('should prevent template injection in email subjects', async () => {\n const maliciousSubjects = [\n '{{process.env.JWT_SECRET}}',\n '${process.env.JWT_SECRET}',\n '<%= process.env.JWT_SECRET %>',\n '#{process.env.JWT_SECRET}',\n ]\n\n for (const subject of maliciousSubjects) {\n const settings = await mockReq.payload.update({\n collection: 'newsletter-settings',\n id: 'settings-1',\n data: {\n emailTemplates: {\n welcome: {\n subject: subject,\n },\n },\n },\n })\n\n // Subject should be treated as literal string, not evaluated\n expect(settings.emailTemplates.welcome.subject).toBe(subject)\n // When used, should not expose secrets\n }\n })\n\n it('should escape user data in email templates', () => {\n // Template rendering function (example)\n const renderTemplate = (template: string, data: any) => {\n // Should escape HTML entities\n const escaped: any = {}\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === 'string') {\n escaped[key] = value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#x27;')\n } else {\n escaped[key] = value\n }\n }\n \n // Simple template replacement\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key) => escaped[key] || '')\n }\n\n const template = '<p>Hello {{name}}, welcome to {{newsletter}}!</p>'\n const maliciousData = {\n name: '<script>alert(\"xss\")</script>',\n newsletter: 'Test Newsletter<img src=x onerror=alert(\"xss\")>',\n }\n\n const rendered = renderTemplate(template, maliciousData)\n \n expect(rendered).not.toContain('<script>')\n expect(rendered).not.toContain('<img src=x onerror=')\n expect(rendered).toContain('&lt;script&gt;')\n expect(rendered).toContain('&lt;img')\n })\n })\n\n describe('Content Security Policy', () => {\n it('should set appropriate CSP headers for admin UI', () => {\n // Mock response headers\n const headers: Record<string, string> = {}\n \n // CSP middleware (example)\n const setCSPHeaders = (res: any) => {\n res.setHeader('Content-Security-Policy', [\n \"default-src 'self'\",\n \"script-src 'self' 'unsafe-inline' 'unsafe-eval'\", // Payload admin needs these\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: https:\",\n \"font-src 'self'\",\n \"connect-src 'self'\",\n \"frame-ancestors 'none'\",\n \"base-uri 'self'\",\n \"form-action 'self'\",\n ].join('; '))\n }\n\n const mockRes = {\n setHeader: (name: string, value: string) => {\n headers[name] = value\n },\n }\n\n setCSPHeaders(mockRes)\n \n expect(headers['Content-Security-Policy']).toContain(\"default-src 'self'\")\n expect(headers['Content-Security-Policy']).toContain(\"frame-ancestors 'none'\")\n })\n })\n\n describe('JSON Injection Prevention', () => {\n it('should prevent JSON injection in API responses', async () => {\n const maliciousData = {\n email: 'test@example.com',\n name: 'Test\", \"isAdmin\": true, \"name\": \"Hacked',\n }\n\n const result = await mockReq.payload.create({\n collection: 'subscribers',\n data: maliciousData,\n })\n\n // The name should be stored as a string, not parsed as JSON\n expect(result.name).toBe(maliciousData.name)\n expect(result.isAdmin).toBeUndefined()\n })\n\n it('should properly escape JSON in responses', () => {\n const escapeJSON = (data: any): string => {\n return JSON.stringify(data)\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n }\n\n const data = {\n name: 'Test\\u2028User\\u2029',\n email: 'test@example.com',\n }\n\n const escaped = escapeJSON(data)\n expect(escaped).not.toContain('\\u2028')\n expect(escaped).not.toContain('\\u2029')\n })\n })\n\n describe('URL Injection Prevention', () => {\n it('should validate redirect URLs', () => {\n const validateRedirectUrl = (url: string, allowedHosts: string[]): boolean => {\n try {\n const parsed = new URL(url)\n return allowedHosts.includes(parsed.host)\n } catch {\n return false\n }\n }\n\n const allowedHosts = ['example.com', 'app.example.com']\n \n // Valid URLs\n expect(validateRedirectUrl('https://example.com/preferences', allowedHosts)).toBe(true)\n expect(validateRedirectUrl('https://app.example.com/unsubscribe', allowedHosts)).toBe(true)\n \n // Invalid URLs\n expect(validateRedirectUrl('https://evil.com/phishing', allowedHosts)).toBe(false)\n expect(validateRedirectUrl('javascript:alert(\"xss\")', allowedHosts)).toBe(false)\n expect(validateRedirectUrl('data:text/html,<script>alert(\"xss\")</script>', allowedHosts)).toBe(false)\n })\n\n it('should sanitize magic link URLs', () => {\n const generateMagicLink = (baseUrl: string, token: string): string => {\n // Validate base URL\n try {\n const url = new URL(baseUrl)\n if (!['http:', 'https:'].includes(url.protocol)) {\n throw new Error('Invalid protocol')\n }\n \n // Encode token to prevent injection\n url.searchParams.set('token', encodeURIComponent(token))\n return url.toString()\n } catch {\n throw new Error('Invalid base URL')\n }\n }\n\n // Valid usage\n const link = generateMagicLink('https://example.com/verify', 'abc123')\n expect(link).toBe('https://example.com/verify?token=abc123')\n \n // Token with special characters\n const maliciousToken = '\"><script>alert(\"xss\")</script>'\n const safeLink = generateMagicLink('https://example.com/verify', maliciousToken)\n expect(safeLink).not.toContain('<script>')\n // Verify the token is properly encoded (double-encoding is actually safer)\n expect(safeLink).toContain('%253Cscript%253E')\n expect(safeLink).toContain('%2522')\n \n // Invalid base URLs\n expect(() => generateMagicLink('javascript:alert(\"xss\")', 'token')).toThrow()\n expect(() => generateMagicLink('data:text/html,test', 'token')).toThrow()\n })\n })\n\n describe('MongoDB Injection Prevention', () => {\n it('should prevent NoSQL injection in queries', async () => {\n // Malicious input attempting to bypass authentication\n const maliciousInputs = [\n { email: { $ne: null } }, // Trying to get all records\n { email: { $regex: '.*' } }, // Regex injection\n { email: { $where: 'this.isAdmin == true' } }, // JavaScript injection\n ]\n\n for (const input of maliciousInputs) {\n try {\n await mockReq.payload.find({\n collection: 'subscribers',\n where: input as any,\n })\n } catch (error: any) {\n // Should either sanitize or reject\n expect(error.message).toContain('Invalid')\n }\n }\n })\n\n it('should sanitize field names', async () => {\n const maliciousFields = [\n { '$where': 'malicious code' },\n { '__proto__': { isAdmin: true } },\n { 'constructor.prototype.isAdmin': true },\n ]\n\n for (const fields of maliciousFields) {\n try {\n await mockReq.payload.create({\n collection: 'subscribers',\n data: {\n email: 'test@example.com',\n ...fields,\n },\n })\n } catch (error: any) {\n // Should reject dangerous field names\n expect(error).toBeDefined()\n }\n }\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","createPayloadRequestMock","seedCollection","clearCollections","mockNewsletterSettings","mockReq","config","payloadMock","payload","body","clearAllMocks","maliciousNames","maliciousName","result","create","collection","data","email","Date","now","name","not","toContain","maliciousEmails","maliciousEmail","error","message","customField","maliciousSubjects","subject","settings","update","id","emailTemplates","welcome","toBe","renderTemplate","template","escaped","key","value","Object","entries","replace","match","maliciousData","newsletter","rendered","headers","setCSPHeaders","res","setHeader","join","mockRes","isAdmin","toBeUndefined","escapeJSON","JSON","stringify","validateRedirectUrl","url","allowedHosts","parsed","URL","includes","host","generateMagicLink","baseUrl","token","protocol","Error","searchParams","set","encodeURIComponent","toString","link","maliciousToken","safeLink","toThrow","maliciousInputs","$ne","$regex","$where","input","find","where","maliciousFields","fields","toBeDefined"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { vi } from 'vitest';
2
+ import { MongoMemoryServer } from 'mongodb-memory-server';
3
+ // MongoDB Memory Server instance
4
+ let mongoServer;
5
+ // Mock environment variables
6
+ process.env.JWT_SECRET = 'test-jwt-secret';
7
+ process.env.PAYLOAD_SECRET = 'test-payload-secret';
8
+ process.env.NODE_ENV = 'test';
9
+ // Setup MongoDB Memory Server
10
+ beforeAll(async ()=>{
11
+ if (process.env.TEST_USE_MONGODB_MEMORY_SERVER === 'true') {
12
+ mongoServer = await MongoMemoryServer.create();
13
+ const mongoUri = mongoServer.getUri();
14
+ process.env.DATABASE_URI = mongoUri;
15
+ }
16
+ });
17
+ // Cleanup MongoDB Memory Server
18
+ afterAll(async ()=>{
19
+ if (mongoServer) {
20
+ await mongoServer.stop();
21
+ }
22
+ });
23
+ // Reset mocks before each test
24
+ beforeEach(()=>{
25
+ vi.clearAllMocks();
26
+ });
27
+ // Suppress console logs in tests unless debugging
28
+ if (!process.env.DEBUG_TESTS) {
29
+ global.console = {
30
+ ...console,
31
+ log: vi.fn(),
32
+ error: vi.fn(),
33
+ warn: vi.fn(),
34
+ info: vi.fn()
35
+ };
36
+ }
37
+
38
+ //# sourceMappingURL=integration.setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/__tests__/setup/integration.setup.ts"],"sourcesContent":["import { vi } from 'vitest'\nimport { MongoMemoryServer } from 'mongodb-memory-server'\n\n// MongoDB Memory Server instance\nlet mongoServer: MongoMemoryServer | undefined\n\n// Mock environment variables\nprocess.env.JWT_SECRET = 'test-jwt-secret'\nprocess.env.PAYLOAD_SECRET = 'test-payload-secret'\nprocess.env.NODE_ENV = 'test'\n\n// Setup MongoDB Memory Server\nbeforeAll(async () => {\n if (process.env.TEST_USE_MONGODB_MEMORY_SERVER === 'true') {\n mongoServer = await MongoMemoryServer.create()\n const mongoUri = mongoServer.getUri()\n process.env.DATABASE_URI = mongoUri\n }\n})\n\n// Cleanup MongoDB Memory Server\nafterAll(async () => {\n if (mongoServer) {\n await mongoServer.stop()\n }\n})\n\n// Reset mocks before each test\nbeforeEach(() => {\n vi.clearAllMocks()\n})\n\n// Suppress console logs in tests unless debugging\nif (!process.env.DEBUG_TESTS) {\n global.console = {\n ...console,\n log: vi.fn(),\n error: vi.fn(),\n warn: vi.fn(),\n info: vi.fn(),\n }\n}"],"names":["vi","MongoMemoryServer","mongoServer","process","env","JWT_SECRET","PAYLOAD_SECRET","NODE_ENV","beforeAll","TEST_USE_MONGODB_MEMORY_SERVER","create","mongoUri","getUri","DATABASE_URI","afterAll","stop","beforeEach","clearAllMocks","DEBUG_TESTS","global","console","log","fn","error","warn","info"],"mappings":"AAAA,SAASA,EAAE,QAAQ,SAAQ;AAC3B,SAASC,iBAAiB,QAAQ,wBAAuB;AAEzD,iCAAiC;AACjC,IAAIC;AAEJ,6BAA6B;AAC7BC,QAAQC,GAAG,CAACC,UAAU,GAAG;AACzBF,QAAQC,GAAG,CAACE,cAAc,GAAG;AAC7BH,QAAQC,GAAG,CAACG,QAAQ,GAAG;AAEvB,8BAA8B;AAC9BC,UAAU;IACR,IAAIL,QAAQC,GAAG,CAACK,8BAA8B,KAAK,QAAQ;QACzDP,cAAc,MAAMD,kBAAkBS,MAAM;QAC5C,MAAMC,WAAWT,YAAYU,MAAM;QACnCT,QAAQC,GAAG,CAACS,YAAY,GAAGF;IAC7B;AACF;AAEA,gCAAgC;AAChCG,SAAS;IACP,IAAIZ,aAAa;QACf,MAAMA,YAAYa,IAAI;IACxB;AACF;AAEA,+BAA+B;AAC/BC,WAAW;IACThB,GAAGiB,aAAa;AAClB;AAEA,kDAAkD;AAClD,IAAI,CAACd,QAAQC,GAAG,CAACc,WAAW,EAAE;IAC5BC,OAAOC,OAAO,GAAG;QACf,GAAGA,OAAO;QACVC,KAAKrB,GAAGsB,EAAE;QACVC,OAAOvB,GAAGsB,EAAE;QACZE,MAAMxB,GAAGsB,EAAE;QACXG,MAAMzB,GAAGsB,EAAE;IACb;AACF"}
@@ -0,0 +1,41 @@
1
+ import '@testing-library/jest-dom';
2
+ import { vi } from 'vitest';
3
+ // Mock environment variables
4
+ process.env.JWT_SECRET = 'test-jwt-secret';
5
+ process.env.PAYLOAD_SECRET = 'test-payload-secret';
6
+ // Mock console methods to reduce noise in tests
7
+ global.console = {
8
+ ...console,
9
+ error: vi.fn(),
10
+ warn: vi.fn()
11
+ };
12
+ // Mock window.location for browser-like tests
13
+ Object.defineProperty(window, 'location', {
14
+ value: {
15
+ href: 'http://localhost:3000',
16
+ origin: 'http://localhost:3000',
17
+ search: ''
18
+ },
19
+ writable: true
20
+ });
21
+ // Mock localStorage
22
+ const localStorageMock = {
23
+ getItem: vi.fn(),
24
+ setItem: vi.fn(),
25
+ removeItem: vi.fn(),
26
+ clear: vi.fn()
27
+ };
28
+ Object.defineProperty(window, 'localStorage', {
29
+ value: localStorageMock,
30
+ writable: true
31
+ });
32
+ // Reset mocks before each test
33
+ beforeEach(()=>{
34
+ vi.clearAllMocks();
35
+ localStorageMock.getItem.mockReset();
36
+ localStorageMock.setItem.mockReset();
37
+ localStorageMock.removeItem.mockReset();
38
+ localStorageMock.clear.mockReset();
39
+ });
40
+
41
+ //# sourceMappingURL=unit.setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/__tests__/setup/unit.setup.ts"],"sourcesContent":["import '@testing-library/jest-dom'\nimport { vi } from 'vitest'\n\n// Mock environment variables\nprocess.env.JWT_SECRET = 'test-jwt-secret'\nprocess.env.PAYLOAD_SECRET = 'test-payload-secret'\n\n// Mock console methods to reduce noise in tests\nglobal.console = {\n ...console,\n error: vi.fn(),\n warn: vi.fn(),\n}\n\n// Mock window.location for browser-like tests\nObject.defineProperty(window, 'location', {\n value: {\n href: 'http://localhost:3000',\n origin: 'http://localhost:3000',\n search: '',\n },\n writable: true,\n})\n\n// Mock localStorage\nconst localStorageMock = {\n getItem: vi.fn(),\n setItem: vi.fn(),\n removeItem: vi.fn(),\n clear: vi.fn(),\n}\nObject.defineProperty(window, 'localStorage', {\n value: localStorageMock,\n writable: true,\n})\n\n// Reset mocks before each test\nbeforeEach(() => {\n vi.clearAllMocks()\n localStorageMock.getItem.mockReset()\n localStorageMock.setItem.mockReset()\n localStorageMock.removeItem.mockReset()\n localStorageMock.clear.mockReset()\n})"],"names":["vi","process","env","JWT_SECRET","PAYLOAD_SECRET","global","console","error","fn","warn","Object","defineProperty","window","value","href","origin","search","writable","localStorageMock","getItem","setItem","removeItem","clear","beforeEach","clearAllMocks","mockReset"],"mappings":"AAAA,OAAO,4BAA2B;AAClC,SAASA,EAAE,QAAQ,SAAQ;AAE3B,6BAA6B;AAC7BC,QAAQC,GAAG,CAACC,UAAU,GAAG;AACzBF,QAAQC,GAAG,CAACE,cAAc,GAAG;AAE7B,gDAAgD;AAChDC,OAAOC,OAAO,GAAG;IACf,GAAGA,OAAO;IACVC,OAAOP,GAAGQ,EAAE;IACZC,MAAMT,GAAGQ,EAAE;AACb;AAEA,8CAA8C;AAC9CE,OAAOC,cAAc,CAACC,QAAQ,YAAY;IACxCC,OAAO;QACLC,MAAM;QACNC,QAAQ;QACRC,QAAQ;IACV;IACAC,UAAU;AACZ;AAEA,oBAAoB;AACpB,MAAMC,mBAAmB;IACvBC,SAASnB,GAAGQ,EAAE;IACdY,SAASpB,GAAGQ,EAAE;IACda,YAAYrB,GAAGQ,EAAE;IACjBc,OAAOtB,GAAGQ,EAAE;AACd;AACAE,OAAOC,cAAc,CAACC,QAAQ,gBAAgB;IAC5CC,OAAOK;IACPD,UAAU;AACZ;AAEA,+BAA+B;AAC/BM,WAAW;IACTvB,GAAGwB,aAAa;IAChBN,iBAAiBC,OAAO,CAACM,SAAS;IAClCP,iBAAiBE,OAAO,CAACK,SAAS;IAClCP,iBAAiBG,UAAU,CAACI,SAAS;IACrCP,iBAAiBI,KAAK,CAACG,SAAS;AAClC"}
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { isAdmin } from '../../../utils/access';
3
+ describe('Access Control Utilities', ()=>{
4
+ describe('isAdmin', ()=>{
5
+ it('should return false for non-user collections', ()=>{
6
+ const subscriber = {
7
+ id: 'sub-123',
8
+ email: 'test@example.com',
9
+ collection: 'subscribers'
10
+ };
11
+ expect(isAdmin(subscriber)).toBe(false);
12
+ });
13
+ it('should return false for null or undefined user', ()=>{
14
+ expect(isAdmin(null)).toBe(false);
15
+ expect(isAdmin(undefined)).toBe(false);
16
+ });
17
+ it('should detect admin via roles array', ()=>{
18
+ const user = {
19
+ id: 'user-123',
20
+ email: 'admin@example.com',
21
+ collection: 'users',
22
+ roles: [
23
+ 'admin',
24
+ 'editor'
25
+ ]
26
+ };
27
+ expect(isAdmin(user)).toBe(true);
28
+ });
29
+ it('should detect admin via isAdmin boolean', ()=>{
30
+ const user = {
31
+ id: 'user-123',
32
+ email: 'admin@example.com',
33
+ collection: 'users',
34
+ isAdmin: true
35
+ };
36
+ expect(isAdmin(user)).toBe(true);
37
+ });
38
+ it('should detect admin via role string', ()=>{
39
+ const user = {
40
+ id: 'user-123',
41
+ email: 'admin@example.com',
42
+ collection: 'users',
43
+ role: 'admin'
44
+ };
45
+ expect(isAdmin(user)).toBe(true);
46
+ });
47
+ it('should detect admin via admin boolean', ()=>{
48
+ const user = {
49
+ id: 'user-123',
50
+ email: 'admin@example.com',
51
+ collection: 'users',
52
+ admin: true
53
+ };
54
+ expect(isAdmin(user)).toBe(true);
55
+ });
56
+ it('should return false for non-admin users', ()=>{
57
+ const user = {
58
+ id: 'user-123',
59
+ email: 'user@example.com',
60
+ collection: 'users',
61
+ roles: [
62
+ 'editor',
63
+ 'viewer'
64
+ ],
65
+ isAdmin: false,
66
+ role: 'editor',
67
+ admin: false
68
+ };
69
+ expect(isAdmin(user)).toBe(false);
70
+ });
71
+ it('should use custom isAdmin function when provided', ()=>{
72
+ const config = {
73
+ access: {
74
+ isAdmin: (user)=>user?.customRole === 'super-admin'
75
+ }
76
+ };
77
+ const regularAdmin = {
78
+ id: 'user-123',
79
+ email: 'admin@example.com',
80
+ collection: 'users',
81
+ roles: [
82
+ 'admin'
83
+ ]
84
+ };
85
+ expect(isAdmin(regularAdmin, config)).toBe(false);
86
+ const customAdmin = {
87
+ id: 'user-456',
88
+ email: 'super@example.com',
89
+ collection: 'users',
90
+ customRole: 'super-admin'
91
+ };
92
+ expect(isAdmin(customAdmin, config)).toBe(true);
93
+ });
94
+ it('should handle edge cases gracefully', ()=>{
95
+ // Empty user object
96
+ const emptyUser = {
97
+ collection: 'users'
98
+ };
99
+ expect(isAdmin(emptyUser)).toBe(false);
100
+ // User with empty roles array
101
+ const userWithEmptyRoles = {
102
+ collection: 'users',
103
+ roles: []
104
+ };
105
+ expect(isAdmin(userWithEmptyRoles)).toBe(false);
106
+ // User with non-admin role string
107
+ const userWithNonAdminRole = {
108
+ collection: 'users',
109
+ role: 'viewer'
110
+ };
111
+ expect(isAdmin(userWithNonAdminRole)).toBe(false);
112
+ });
113
+ });
114
+ });
115
+
116
+ //# sourceMappingURL=access.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../src/__tests__/unit/utils/access.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\nimport { isAdmin } from '../../../utils/access'\nimport type { NewsletterPluginConfig } from '../../../types'\n\ndescribe('Access Control Utilities', () => {\n describe('isAdmin', () => {\n it('should return false for non-user collections', () => {\n const subscriber = {\n id: 'sub-123',\n email: 'test@example.com',\n collection: 'subscribers',\n }\n expect(isAdmin(subscriber)).toBe(false)\n })\n\n it('should return false for null or undefined user', () => {\n expect(isAdmin(null)).toBe(false)\n expect(isAdmin(undefined)).toBe(false)\n })\n\n it('should detect admin via roles array', () => {\n const user = {\n id: 'user-123',\n email: 'admin@example.com',\n collection: 'users',\n roles: ['admin', 'editor'],\n }\n expect(isAdmin(user)).toBe(true)\n })\n\n it('should detect admin via isAdmin boolean', () => {\n const user = {\n id: 'user-123',\n email: 'admin@example.com',\n collection: 'users',\n isAdmin: true,\n }\n expect(isAdmin(user)).toBe(true)\n })\n\n it('should detect admin via role string', () => {\n const user = {\n id: 'user-123',\n email: 'admin@example.com',\n collection: 'users',\n role: 'admin',\n }\n expect(isAdmin(user)).toBe(true)\n })\n\n it('should detect admin via admin boolean', () => {\n const user = {\n id: 'user-123',\n email: 'admin@example.com',\n collection: 'users',\n admin: true,\n }\n expect(isAdmin(user)).toBe(true)\n })\n\n it('should return false for non-admin users', () => {\n const user = {\n id: 'user-123',\n email: 'user@example.com',\n collection: 'users',\n roles: ['editor', 'viewer'],\n isAdmin: false,\n role: 'editor',\n admin: false,\n }\n expect(isAdmin(user)).toBe(false)\n })\n\n it('should use custom isAdmin function when provided', () => {\n const config: NewsletterPluginConfig = {\n access: {\n isAdmin: (user) => user?.customRole === 'super-admin',\n },\n }\n\n const regularAdmin = {\n id: 'user-123',\n email: 'admin@example.com',\n collection: 'users',\n roles: ['admin'],\n }\n expect(isAdmin(regularAdmin, config)).toBe(false)\n\n const customAdmin = {\n id: 'user-456',\n email: 'super@example.com',\n collection: 'users',\n customRole: 'super-admin',\n }\n expect(isAdmin(customAdmin, config)).toBe(true)\n })\n\n it('should handle edge cases gracefully', () => {\n // Empty user object\n const emptyUser = { collection: 'users' }\n expect(isAdmin(emptyUser)).toBe(false)\n\n // User with empty roles array\n const userWithEmptyRoles = {\n collection: 'users',\n roles: [],\n }\n expect(isAdmin(userWithEmptyRoles)).toBe(false)\n\n // User with non-admin role string\n const userWithNonAdminRole = {\n collection: 'users',\n role: 'viewer',\n }\n expect(isAdmin(userWithNonAdminRole)).toBe(false)\n })\n })\n})"],"names":["describe","it","expect","isAdmin","subscriber","id","email","collection","toBe","undefined","user","roles","role","admin","config","access","customRole","regularAdmin","customAdmin","emptyUser","userWithEmptyRoles","userWithNonAdminRole"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,EAAE,EAAEC,MAAM,QAAQ,SAAQ;AAC7C,SAASC,OAAO,QAAQ,wBAAuB;AAG/CH,SAAS,4BAA4B;IACnCA,SAAS,WAAW;QAClBC,GAAG,gDAAgD;YACjD,MAAMG,aAAa;gBACjBC,IAAI;gBACJC,OAAO;gBACPC,YAAY;YACd;YACAL,OAAOC,QAAQC,aAAaI,IAAI,CAAC;QACnC;QAEAP,GAAG,kDAAkD;YACnDC,OAAOC,QAAQ,OAAOK,IAAI,CAAC;YAC3BN,OAAOC,QAAQM,YAAYD,IAAI,CAAC;QAClC;QAEAP,GAAG,uCAAuC;YACxC,MAAMS,OAAO;gBACXL,IAAI;gBACJC,OAAO;gBACPC,YAAY;gBACZI,OAAO;oBAAC;oBAAS;iBAAS;YAC5B;YACAT,OAAOC,QAAQO,OAAOF,IAAI,CAAC;QAC7B;QAEAP,GAAG,2CAA2C;YAC5C,MAAMS,OAAO;gBACXL,IAAI;gBACJC,OAAO;gBACPC,YAAY;gBACZJ,SAAS;YACX;YACAD,OAAOC,QAAQO,OAAOF,IAAI,CAAC;QAC7B;QAEAP,GAAG,uCAAuC;YACxC,MAAMS,OAAO;gBACXL,IAAI;gBACJC,OAAO;gBACPC,YAAY;gBACZK,MAAM;YACR;YACAV,OAAOC,QAAQO,OAAOF,IAAI,CAAC;QAC7B;QAEAP,GAAG,yCAAyC;YAC1C,MAAMS,OAAO;gBACXL,IAAI;gBACJC,OAAO;gBACPC,YAAY;gBACZM,OAAO;YACT;YACAX,OAAOC,QAAQO,OAAOF,IAAI,CAAC;QAC7B;QAEAP,GAAG,2CAA2C;YAC5C,MAAMS,OAAO;gBACXL,IAAI;gBACJC,OAAO;gBACPC,YAAY;gBACZI,OAAO;oBAAC;oBAAU;iBAAS;gBAC3BR,SAAS;gBACTS,MAAM;gBACNC,OAAO;YACT;YACAX,OAAOC,QAAQO,OAAOF,IAAI,CAAC;QAC7B;QAEAP,GAAG,oDAAoD;YACrD,MAAMa,SAAiC;gBACrCC,QAAQ;oBACNZ,SAAS,CAACO,OAASA,MAAMM,eAAe;gBAC1C;YACF;YAEA,MAAMC,eAAe;gBACnBZ,IAAI;gBACJC,OAAO;gBACPC,YAAY;gBACZI,OAAO;oBAAC;iBAAQ;YAClB;YACAT,OAAOC,QAAQc,cAAcH,SAASN,IAAI,CAAC;YAE3C,MAAMU,cAAc;gBAClBb,IAAI;gBACJC,OAAO;gBACPC,YAAY;gBACZS,YAAY;YACd;YACAd,OAAOC,QAAQe,aAAaJ,SAASN,IAAI,CAAC;QAC5C;QAEAP,GAAG,uCAAuC;YACxC,oBAAoB;YACpB,MAAMkB,YAAY;gBAAEZ,YAAY;YAAQ;YACxCL,OAAOC,QAAQgB,YAAYX,IAAI,CAAC;YAEhC,8BAA8B;YAC9B,MAAMY,qBAAqB;gBACzBb,YAAY;gBACZI,OAAO,EAAE;YACX;YACAT,OAAOC,QAAQiB,qBAAqBZ,IAAI,CAAC;YAEzC,kCAAkC;YAClC,MAAMa,uBAAuB;gBAC3Bd,YAAY;gBACZK,MAAM;YACR;YACAV,OAAOC,QAAQkB,uBAAuBb,IAAI,CAAC;QAC7C;IACF;AACF"}