payload-plugin-newsletter 0.3.0 → 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.
- package/CHANGELOG.md +43 -0
- package/CLAUDE.md +110 -0
- package/README.md +25 -3
- package/TEST_SUMMARY.md +152 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/collections/NewsletterSettings.d.ts.map +1 -1
- package/dist/collections/Subscribers.d.ts.map +1 -1
- package/dist/src/__tests__/fixtures/newsletter-settings.js +41 -0
- package/dist/src/__tests__/fixtures/newsletter-settings.js.map +1 -0
- package/dist/src/__tests__/fixtures/subscribers.js +70 -0
- package/dist/src/__tests__/fixtures/subscribers.js.map +1 -0
- package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js +356 -0
- package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js.map +1 -0
- package/dist/src/__tests__/integration/endpoints/preferences.test.js +266 -0
- package/dist/src/__tests__/integration/endpoints/preferences.test.js.map +1 -0
- package/dist/src/__tests__/integration/endpoints/subscribe.test.js +280 -0
- package/dist/src/__tests__/integration/endpoints/subscribe.test.js.map +1 -0
- package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js +187 -0
- package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js.map +1 -0
- package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js +188 -0
- package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js.map +1 -0
- package/dist/src/__tests__/mocks/email-providers.js +153 -0
- package/dist/src/__tests__/mocks/email-providers.js.map +1 -0
- package/dist/src/__tests__/mocks/payload.js +244 -0
- package/dist/src/__tests__/mocks/payload.js.map +1 -0
- package/dist/src/__tests__/security/csrf-protection.test.js +309 -0
- package/dist/src/__tests__/security/csrf-protection.test.js.map +1 -0
- package/dist/src/__tests__/security/settings-access.test.js +204 -0
- package/dist/src/__tests__/security/settings-access.test.js.map +1 -0
- package/dist/src/__tests__/security/subscriber-access.test.js +210 -0
- package/dist/src/__tests__/security/subscriber-access.test.js.map +1 -0
- package/dist/src/__tests__/security/xss-prevention.test.js +305 -0
- package/dist/src/__tests__/security/xss-prevention.test.js.map +1 -0
- package/dist/src/__tests__/setup/integration.setup.js +38 -0
- package/dist/src/__tests__/setup/integration.setup.js.map +1 -0
- package/dist/src/__tests__/setup/unit.setup.js +41 -0
- package/dist/src/__tests__/setup/unit.setup.js.map +1 -0
- package/dist/src/__tests__/unit/utils/access.test.js +116 -0
- package/dist/src/__tests__/unit/utils/access.test.js.map +1 -0
- package/dist/src/__tests__/unit/utils/jwt.test.js +238 -0
- package/dist/src/__tests__/unit/utils/jwt.test.js.map +1 -0
- package/dist/src/collections/NewsletterSettings.js +4 -3
- package/dist/src/collections/NewsletterSettings.js.map +1 -1
- package/dist/src/collections/Subscribers.js +4 -39
- package/dist/src/collections/Subscribers.js.map +1 -1
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/utils/access.js +80 -0
- package/dist/src/utils/access.js.map +1 -0
- package/dist/src/utils/validation.js +9 -1
- package/dist/src/utils/validation.js.map +1 -1
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/access.d.ts +15 -0
- package/dist/utils/access.d.ts.map +1 -0
- package/dist/utils/validation.d.ts.map +1 -1
- 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
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('<script>');
|
|
128
|
+
expect(rendered).toContain('<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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\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('<script>')\n expect(rendered).toContain('<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"}
|