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,210 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { adminOnly, adminOrSelf } from '../../utils/access';
3
- import { createMockUser, createMockAdminUser } from '../mocks/payload';
4
- describe('Subscriber Access Control Security', ()=>{
5
- let mockReq;
6
- const mockConfig = {};
7
- beforeEach(()=>{
8
- mockReq = {
9
- user: null
10
- };
11
- });
12
- describe('adminOnly', ()=>{
13
- it('should deny access to unauthenticated users', ()=>{
14
- const access = adminOnly(mockConfig);
15
- expect(access({
16
- req: mockReq
17
- })).toBe(false);
18
- });
19
- it('should deny access to regular users', ()=>{
20
- mockReq.user = createMockUser();
21
- const access = adminOnly(mockConfig);
22
- expect(access({
23
- req: mockReq
24
- })).toBe(false);
25
- });
26
- it('should allow access to admin users', ()=>{
27
- mockReq.user = createMockAdminUser();
28
- const access = adminOnly(mockConfig);
29
- expect(access({
30
- req: mockReq
31
- })).toBe(true);
32
- });
33
- it('should respect custom admin function', ()=>{
34
- const customConfig = {
35
- access: {
36
- isAdmin: (user)=>user?.customRole === 'manager'
37
- }
38
- };
39
- // Regular admin should be denied
40
- mockReq.user = createMockAdminUser();
41
- const access = adminOnly(customConfig);
42
- expect(access({
43
- req: mockReq
44
- })).toBe(false);
45
- // Custom role should be allowed
46
- mockReq.user = createMockUser({
47
- customRole: 'manager'
48
- });
49
- expect(access({
50
- req: mockReq
51
- })).toBe(true);
52
- });
53
- });
54
- describe('adminOrSelf', ()=>{
55
- it('should deny access to unauthenticated users', ()=>{
56
- const access = adminOrSelf(mockConfig);
57
- expect(access({
58
- req: mockReq,
59
- id: 'sub-123'
60
- })).toBe(false);
61
- });
62
- it('should allow admin users to access any subscriber', ()=>{
63
- mockReq.user = createMockAdminUser();
64
- const access = adminOrSelf(mockConfig);
65
- // Admin can access any subscriber
66
- expect(access({
67
- req: mockReq,
68
- id: 'sub-123'
69
- })).toBe(true);
70
- expect(access({
71
- req: mockReq,
72
- id: 'sub-456'
73
- })).toBe(true);
74
- });
75
- it('should allow synthetic users to access their own data', ()=>{
76
- // Synthetic user (from magic link)
77
- mockReq.user = {
78
- id: 'sub-123',
79
- email: 'test@example.com',
80
- collection: 'subscribers'
81
- };
82
- const access = adminOrSelf(mockConfig);
83
- // Can access own data
84
- expect(access({
85
- req: mockReq,
86
- id: 'sub-123'
87
- })).toBe(true);
88
- // Cannot access other subscribers
89
- expect(access({
90
- req: mockReq,
91
- id: 'sub-456'
92
- })).toBe(false);
93
- });
94
- it('should deny regular users access to subscriber data', ()=>{
95
- mockReq.user = createMockUser({
96
- id: 'user-123'
97
- });
98
- const access = adminOrSelf(mockConfig);
99
- // Regular users cannot access any subscriber data
100
- expect(access({
101
- req: mockReq,
102
- id: 'sub-123'
103
- })).toBe(false);
104
- });
105
- it('should handle where queries for list operations', ()=>{
106
- const access = adminOrSelf(mockConfig);
107
- // Unauthenticated - no access
108
- expect(access({
109
- req: mockReq
110
- })).toEqual({
111
- id: {
112
- equals: 'unauthorized-no-access'
113
- }
114
- });
115
- // Admin - full access
116
- mockReq.user = createMockAdminUser();
117
- expect(access({
118
- req: mockReq
119
- })).toBe(true);
120
- // Synthetic user - scoped to self
121
- mockReq.user = {
122
- id: 'sub-123',
123
- email: 'test@example.com',
124
- collection: 'subscribers'
125
- };
126
- expect(access({
127
- req: mockReq
128
- })).toEqual({
129
- id: {
130
- equals: 'sub-123'
131
- }
132
- });
133
- // Regular user - no access
134
- mockReq.user = createMockUser();
135
- expect(access({
136
- req: mockReq
137
- })).toEqual({
138
- id: {
139
- equals: 'unauthorized-no-access'
140
- }
141
- });
142
- });
143
- it('should prevent data leakage through query manipulation', ()=>{
144
- // Synthetic user trying to access other data
145
- mockReq.user = {
146
- id: 'sub-123',
147
- email: 'attacker@example.com',
148
- collection: 'subscribers'
149
- };
150
- const access = adminOrSelf(mockConfig);
151
- // Direct ID access is properly restricted
152
- expect(access({
153
- req: mockReq,
154
- id: 'sub-456'
155
- })).toBe(false);
156
- // List queries are properly scoped
157
- const whereClause = access({
158
- req: mockReq
159
- });
160
- expect(whereClause).toEqual({
161
- id: {
162
- equals: 'sub-123'
163
- }
164
- });
165
- });
166
- });
167
- describe('Cross-subscriber data isolation', ()=>{
168
- it('should prevent subscribers from accessing each other\'s data', ()=>{
169
- const access = adminOrSelf(mockConfig);
170
- // Subscriber A
171
- const subscriberA = {
172
- id: 'sub-a',
173
- email: 'a@example.com',
174
- collection: 'subscribers'
175
- };
176
- // Subscriber B trying to access Subscriber A's data
177
- mockReq.user = {
178
- id: 'sub-b',
179
- email: 'b@example.com',
180
- collection: 'subscribers'
181
- };
182
- expect(access({
183
- req: mockReq,
184
- id: 'sub-a'
185
- })).toBe(false);
186
- });
187
- it('should prevent forged synthetic users', ()=>{
188
- const access = adminOrSelf(mockConfig);
189
- // Attacker trying to forge a synthetic user
190
- mockReq.user = {
191
- id: 'sub-target',
192
- email: 'attacker@evil.com',
193
- collection: 'subscribers'
194
- };
195
- // Even if they claim the right ID, the magic link system
196
- // should have validated this is the correct user
197
- expect(access({
198
- req: mockReq,
199
- id: 'sub-target'
200
- })).toBe(true); // Access control trusts the auth layer
201
- // But they still can't access other subscribers
202
- expect(access({
203
- req: mockReq,
204
- id: 'sub-other'
205
- })).toBe(false);
206
- });
207
- });
208
- });
209
-
210
- //# sourceMappingURL=subscriber-access.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/__tests__/security/subscriber-access.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport { adminOnly, adminOrSelf } from '../../utils/access'\nimport type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig } from '../../types'\nimport { createMockUser, createMockAdminUser } from '../mocks/payload'\n\ndescribe('Subscriber Access Control Security', () => {\n let mockReq: Partial<PayloadRequest>\n const mockConfig: NewsletterPluginConfig = {}\n\n beforeEach(() => {\n mockReq = {\n user: null,\n }\n })\n\n describe('adminOnly', () => {\n it('should deny access to unauthenticated users', () => {\n const access = adminOnly(mockConfig)\n expect(access({ req: mockReq as PayloadRequest })).toBe(false)\n })\n\n it('should deny access to regular users', () => {\n mockReq.user = createMockUser()\n const access = adminOnly(mockConfig)\n expect(access({ req: mockReq as PayloadRequest })).toBe(false)\n })\n\n it('should allow access to admin users', () => {\n mockReq.user = createMockAdminUser()\n const access = adminOnly(mockConfig)\n expect(access({ req: mockReq as PayloadRequest })).toBe(true)\n })\n\n it('should respect custom admin function', () => {\n const customConfig: NewsletterPluginConfig = {\n access: {\n isAdmin: (user) => user?.customRole === 'manager',\n },\n }\n\n // Regular admin should be denied\n mockReq.user = createMockAdminUser()\n const access = adminOnly(customConfig)\n expect(access({ req: mockReq as PayloadRequest })).toBe(false)\n\n // Custom role should be allowed\n mockReq.user = createMockUser({ customRole: 'manager' })\n expect(access({ req: mockReq as PayloadRequest })).toBe(true)\n })\n })\n\n describe('adminOrSelf', () => {\n it('should deny access to unauthenticated users', () => {\n const access = adminOrSelf(mockConfig)\n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-123',\n })).toBe(false)\n })\n\n it('should allow admin users to access any subscriber', () => {\n mockReq.user = createMockAdminUser()\n const access = adminOrSelf(mockConfig)\n \n // Admin can access any subscriber\n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-123',\n })).toBe(true)\n \n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-456',\n })).toBe(true)\n })\n\n it('should allow synthetic users to access their own data', () => {\n // Synthetic user (from magic link)\n mockReq.user = {\n id: 'sub-123',\n email: 'test@example.com',\n collection: 'subscribers',\n }\n \n const access = adminOrSelf(mockConfig)\n \n // Can access own data\n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-123',\n })).toBe(true)\n \n // Cannot access other subscribers\n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-456',\n })).toBe(false)\n })\n\n it('should deny regular users access to subscriber data', () => {\n mockReq.user = createMockUser({ id: 'user-123' })\n const access = adminOrSelf(mockConfig)\n \n // Regular users cannot access any subscriber data\n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-123',\n })).toBe(false)\n })\n\n it('should handle where queries for list operations', () => {\n const access = adminOrSelf(mockConfig)\n \n // Unauthenticated - no access\n expect(access({ req: mockReq as PayloadRequest })).toEqual({\n id: { equals: 'unauthorized-no-access' },\n })\n \n // Admin - full access\n mockReq.user = createMockAdminUser()\n expect(access({ req: mockReq as PayloadRequest })).toBe(true)\n \n // Synthetic user - scoped to self\n mockReq.user = {\n id: 'sub-123',\n email: 'test@example.com',\n collection: 'subscribers',\n }\n expect(access({ req: mockReq as PayloadRequest })).toEqual({\n id: { equals: 'sub-123' },\n })\n \n // Regular user - no access\n mockReq.user = createMockUser()\n expect(access({ req: mockReq as PayloadRequest })).toEqual({\n id: { equals: 'unauthorized-no-access' },\n })\n })\n\n it('should prevent data leakage through query manipulation', () => {\n // Synthetic user trying to access other data\n mockReq.user = {\n id: 'sub-123',\n email: 'attacker@example.com',\n collection: 'subscribers',\n }\n \n const access = adminOrSelf(mockConfig)\n \n // Direct ID access is properly restricted\n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-456', // Trying to access another subscriber\n })).toBe(false)\n \n // List queries are properly scoped\n const whereClause = access({ req: mockReq as PayloadRequest })\n expect(whereClause).toEqual({\n id: { equals: 'sub-123' },\n })\n })\n })\n\n describe('Cross-subscriber data isolation', () => {\n it('should prevent subscribers from accessing each other\\'s data', () => {\n const access = adminOrSelf(mockConfig)\n \n // Subscriber A\n const subscriberA = {\n id: 'sub-a',\n email: 'a@example.com',\n collection: 'subscribers',\n }\n \n // Subscriber B trying to access Subscriber A's data\n mockReq.user = {\n id: 'sub-b',\n email: 'b@example.com',\n collection: 'subscribers',\n }\n \n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-a',\n })).toBe(false)\n })\n\n it('should prevent forged synthetic users', () => {\n const access = adminOrSelf(mockConfig)\n \n // Attacker trying to forge a synthetic user\n mockReq.user = {\n id: 'sub-target',\n email: 'attacker@evil.com',\n collection: 'subscribers', // Claiming to be a subscriber\n // But this should be validated by the magic link system\n }\n \n // Even if they claim the right ID, the magic link system\n // should have validated this is the correct user\n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-target',\n })).toBe(true) // Access control trusts the auth layer\n \n // But they still can't access other subscribers\n expect(access({ \n req: mockReq as PayloadRequest,\n id: 'sub-other',\n })).toBe(false)\n })\n })\n})"],"names":["describe","it","expect","beforeEach","adminOnly","adminOrSelf","createMockUser","createMockAdminUser","mockReq","mockConfig","user","access","req","toBe","customConfig","isAdmin","customRole","id","email","collection","toEqual","equals","whereClause","subscriberA"],"mappings":"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"}
@@ -1,305 +0,0 @@
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
@@ -1 +0,0 @@
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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"}
@@ -1,38 +0,0 @@
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
@@ -1 +0,0 @@
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"}