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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NewsletterSettings.d.ts","sourceRoot":"","sources":["../../src/collections/NewsletterSettings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"NewsletterSettings.d.ts","sourceRoot":"","sources":["../../src/collections/NewsletterSettings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAGtD,eAAO,MAAM,kCAAkC,GAC7C,cAAc,sBAAsB,KACnC,gBAiXF,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Subscribers.d.ts","sourceRoot":"","sources":["../../src/collections/Subscribers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"Subscribers.d.ts","sourceRoot":"","sources":["../../src/collections/Subscribers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAgE,MAAM,SAAS,CAAA;AAC7G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAGtD,eAAO,MAAM,2BAA2B,GACtC,cAAc,sBAAsB,KACnC,gBA4SF,CAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const mockNewsletterSettings = {
|
|
2
|
+
id: 'settings-1',
|
|
3
|
+
name: 'Test Configuration',
|
|
4
|
+
active: true,
|
|
5
|
+
provider: 'resend',
|
|
6
|
+
resendSettings: {
|
|
7
|
+
apiKey: 'test-api-key',
|
|
8
|
+
audienceIds: []
|
|
9
|
+
},
|
|
10
|
+
from: {
|
|
11
|
+
email: 'newsletter@example.com',
|
|
12
|
+
name: 'Test Newsletter'
|
|
13
|
+
},
|
|
14
|
+
subscriptionSettings: {
|
|
15
|
+
requireDoubleOptIn: true,
|
|
16
|
+
allowedDomains: [],
|
|
17
|
+
maxSubscribersPerIP: 10
|
|
18
|
+
},
|
|
19
|
+
emailTemplates: {
|
|
20
|
+
welcome: {
|
|
21
|
+
enabled: true,
|
|
22
|
+
subject: 'Welcome to Our Newsletter',
|
|
23
|
+
preheader: 'Thank you for subscribing!'
|
|
24
|
+
},
|
|
25
|
+
magicLink: {
|
|
26
|
+
subject: 'Sign in to {{fromName}}',
|
|
27
|
+
preheader: 'Click the link to access your preferences',
|
|
28
|
+
expirationTime: '7d'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
createdAt: new Date('2024-01-01').toISOString(),
|
|
32
|
+
updatedAt: new Date('2024-01-01').toISOString()
|
|
33
|
+
};
|
|
34
|
+
export const createMockSettings = (overrides = {})=>{
|
|
35
|
+
return {
|
|
36
|
+
...mockNewsletterSettings,
|
|
37
|
+
...overrides
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=newsletter-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/__tests__/fixtures/newsletter-settings.ts"],"sourcesContent":["export const mockNewsletterSettings = {\n id: 'settings-1',\n name: 'Test Configuration',\n active: true,\n provider: 'resend',\n resendSettings: {\n apiKey: 'test-api-key',\n audienceIds: [],\n },\n from: {\n email: 'newsletter@example.com',\n name: 'Test Newsletter',\n },\n subscriptionSettings: {\n requireDoubleOptIn: true,\n allowedDomains: [],\n maxSubscribersPerIP: 10,\n },\n emailTemplates: {\n welcome: {\n enabled: true,\n subject: 'Welcome to Our Newsletter',\n preheader: 'Thank you for subscribing!',\n },\n magicLink: {\n subject: 'Sign in to {{fromName}}',\n preheader: 'Click the link to access your preferences',\n expirationTime: '7d',\n },\n },\n createdAt: new Date('2024-01-01').toISOString(),\n updatedAt: new Date('2024-01-01').toISOString(),\n}\n\nexport const createMockSettings = (overrides: any = {}) => {\n return {\n ...mockNewsletterSettings,\n ...overrides,\n }\n}"],"names":["mockNewsletterSettings","id","name","active","provider","resendSettings","apiKey","audienceIds","from","email","subscriptionSettings","requireDoubleOptIn","allowedDomains","maxSubscribersPerIP","emailTemplates","welcome","enabled","subject","preheader","magicLink","expirationTime","createdAt","Date","toISOString","updatedAt","createMockSettings","overrides"],"mappings":"AAAA,OAAO,MAAMA,yBAAyB;IACpCC,IAAI;IACJC,MAAM;IACNC,QAAQ;IACRC,UAAU;IACVC,gBAAgB;QACdC,QAAQ;QACRC,aAAa,EAAE;IACjB;IACAC,MAAM;QACJC,OAAO;QACPP,MAAM;IACR;IACAQ,sBAAsB;QACpBC,oBAAoB;QACpBC,gBAAgB,EAAE;QAClBC,qBAAqB;IACvB;IACAC,gBAAgB;QACdC,SAAS;YACPC,SAAS;YACTC,SAAS;YACTC,WAAW;QACb;QACAC,WAAW;YACTF,SAAS;YACTC,WAAW;YACXE,gBAAgB;QAClB;IACF;IACAC,WAAW,IAAIC,KAAK,cAAcC,WAAW;IAC7CC,WAAW,IAAIF,KAAK,cAAcC,WAAW;AAC/C,EAAC;AAED,OAAO,MAAME,qBAAqB,CAACC,YAAiB,CAAC,CAAC;IACpD,OAAO;QACL,GAAG1B,sBAAsB;QACzB,GAAG0B,SAAS;IACd;AACF,EAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const mockSubscribers = [
|
|
2
|
+
{
|
|
3
|
+
id: 'sub-1',
|
|
4
|
+
email: 'active@example.com',
|
|
5
|
+
name: 'Active Subscriber',
|
|
6
|
+
locale: 'en',
|
|
7
|
+
subscriptionStatus: 'active',
|
|
8
|
+
emailPreferences: {
|
|
9
|
+
newsletter: true,
|
|
10
|
+
announcements: true
|
|
11
|
+
},
|
|
12
|
+
createdAt: new Date('2024-01-01').toISOString(),
|
|
13
|
+
updatedAt: new Date('2024-01-01').toISOString()
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'sub-2',
|
|
17
|
+
email: 'pending@example.com',
|
|
18
|
+
name: 'Pending Subscriber',
|
|
19
|
+
locale: 'en',
|
|
20
|
+
subscriptionStatus: 'pending',
|
|
21
|
+
emailPreferences: {
|
|
22
|
+
newsletter: true,
|
|
23
|
+
announcements: true
|
|
24
|
+
},
|
|
25
|
+
createdAt: new Date('2024-01-02').toISOString(),
|
|
26
|
+
updatedAt: new Date('2024-01-02').toISOString()
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'sub-3',
|
|
30
|
+
email: 'unsubscribed@example.com',
|
|
31
|
+
name: 'Former Subscriber',
|
|
32
|
+
locale: 'en',
|
|
33
|
+
subscriptionStatus: 'unsubscribed',
|
|
34
|
+
unsubscribedAt: new Date('2024-01-10').toISOString(),
|
|
35
|
+
emailPreferences: {
|
|
36
|
+
newsletter: false,
|
|
37
|
+
announcements: false
|
|
38
|
+
},
|
|
39
|
+
createdAt: new Date('2024-01-03').toISOString(),
|
|
40
|
+
updatedAt: new Date('2024-01-10').toISOString()
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
export const createMockSubscriber = (overrides = {})=>{
|
|
44
|
+
return {
|
|
45
|
+
id: `sub-${Date.now()}`,
|
|
46
|
+
email: 'test@example.com',
|
|
47
|
+
name: 'Test Subscriber',
|
|
48
|
+
locale: 'en',
|
|
49
|
+
subscriptionStatus: 'pending',
|
|
50
|
+
emailPreferences: {
|
|
51
|
+
newsletter: true,
|
|
52
|
+
announcements: true
|
|
53
|
+
},
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
...overrides
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
export const mockMagicLinkTokenData = {
|
|
60
|
+
subscriberId: 'sub-1',
|
|
61
|
+
email: 'active@example.com',
|
|
62
|
+
action: 'verify'
|
|
63
|
+
};
|
|
64
|
+
export const mockPreferencesTokenData = {
|
|
65
|
+
subscriberId: 'sub-1',
|
|
66
|
+
email: 'active@example.com',
|
|
67
|
+
action: 'preferences'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=subscribers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/__tests__/fixtures/subscribers.ts"],"sourcesContent":["export const mockSubscribers = [\n {\n id: 'sub-1',\n email: 'active@example.com',\n name: 'Active Subscriber',\n locale: 'en',\n subscriptionStatus: 'active',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n },\n createdAt: new Date('2024-01-01').toISOString(),\n updatedAt: new Date('2024-01-01').toISOString(),\n },\n {\n id: 'sub-2',\n email: 'pending@example.com',\n name: 'Pending Subscriber',\n locale: 'en',\n subscriptionStatus: 'pending',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n },\n createdAt: new Date('2024-01-02').toISOString(),\n updatedAt: new Date('2024-01-02').toISOString(),\n },\n {\n id: 'sub-3',\n email: 'unsubscribed@example.com',\n name: 'Former Subscriber',\n locale: 'en',\n subscriptionStatus: 'unsubscribed',\n unsubscribedAt: new Date('2024-01-10').toISOString(),\n emailPreferences: {\n newsletter: false,\n announcements: false,\n },\n createdAt: new Date('2024-01-03').toISOString(),\n updatedAt: new Date('2024-01-10').toISOString(),\n },\n]\n\nexport const createMockSubscriber = (overrides: any = {}) => {\n return {\n id: `sub-${Date.now()}`,\n email: 'test@example.com',\n name: 'Test Subscriber',\n locale: 'en',\n subscriptionStatus: 'pending',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n },\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n ...overrides,\n }\n}\n\nexport const mockMagicLinkTokenData = {\n subscriberId: 'sub-1',\n email: 'active@example.com',\n action: 'verify' as const,\n}\n\nexport const mockPreferencesTokenData = {\n subscriberId: 'sub-1',\n email: 'active@example.com',\n action: 'preferences' as const,\n}"],"names":["mockSubscribers","id","email","name","locale","subscriptionStatus","emailPreferences","newsletter","announcements","createdAt","Date","toISOString","updatedAt","unsubscribedAt","createMockSubscriber","overrides","now","mockMagicLinkTokenData","subscriberId","action","mockPreferencesTokenData"],"mappings":"AAAA,OAAO,MAAMA,kBAAkB;IAC7B;QACEC,IAAI;QACJC,OAAO;QACPC,MAAM;QACNC,QAAQ;QACRC,oBAAoB;QACpBC,kBAAkB;YAChBC,YAAY;YACZC,eAAe;QACjB;QACAC,WAAW,IAAIC,KAAK,cAAcC,WAAW;QAC7CC,WAAW,IAAIF,KAAK,cAAcC,WAAW;IAC/C;IACA;QACEV,IAAI;QACJC,OAAO;QACPC,MAAM;QACNC,QAAQ;QACRC,oBAAoB;QACpBC,kBAAkB;YAChBC,YAAY;YACZC,eAAe;QACjB;QACAC,WAAW,IAAIC,KAAK,cAAcC,WAAW;QAC7CC,WAAW,IAAIF,KAAK,cAAcC,WAAW;IAC/C;IACA;QACEV,IAAI;QACJC,OAAO;QACPC,MAAM;QACNC,QAAQ;QACRC,oBAAoB;QACpBQ,gBAAgB,IAAIH,KAAK,cAAcC,WAAW;QAClDL,kBAAkB;YAChBC,YAAY;YACZC,eAAe;QACjB;QACAC,WAAW,IAAIC,KAAK,cAAcC,WAAW;QAC7CC,WAAW,IAAIF,KAAK,cAAcC,WAAW;IAC/C;CACD,CAAA;AAED,OAAO,MAAMG,uBAAuB,CAACC,YAAiB,CAAC,CAAC;IACtD,OAAO;QACLd,IAAI,CAAC,IAAI,EAAES,KAAKM,GAAG,IAAI;QACvBd,OAAO;QACPC,MAAM;QACNC,QAAQ;QACRC,oBAAoB;QACpBC,kBAAkB;YAChBC,YAAY;YACZC,eAAe;QACjB;QACAC,WAAW,IAAIC,OAAOC,WAAW;QACjCC,WAAW,IAAIF,OAAOC,WAAW;QACjC,GAAGI,SAAS;IACd;AACF,EAAC;AAED,OAAO,MAAME,yBAAyB;IACpCC,cAAc;IACdhB,OAAO;IACPiB,QAAQ;AACV,EAAC;AAED,OAAO,MAAMC,2BAA2B;IACtCF,cAAc;IACdhB,OAAO;IACPiB,QAAQ;AACV,EAAC"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { createPayloadRequestMock, seedCollection, clearCollections, createMockAdminUser } from '../../mocks/payload';
|
|
3
|
+
import { mockSubscribers } from '../../fixtures/subscribers';
|
|
4
|
+
import { createResendMock } from '../../mocks/email-providers';
|
|
5
|
+
// Mock email service
|
|
6
|
+
vi.mock('../../../services/email', ()=>({
|
|
7
|
+
getEmailService: vi.fn()
|
|
8
|
+
}));
|
|
9
|
+
import { getEmailService } from '../../../services/email';
|
|
10
|
+
describe('Subscriber Collection Hooks Security', ()=>{
|
|
11
|
+
let mockReq;
|
|
12
|
+
let mockEmailService;
|
|
13
|
+
const config = {
|
|
14
|
+
subscribersSlug: 'subscribers'
|
|
15
|
+
};
|
|
16
|
+
beforeEach(()=>{
|
|
17
|
+
clearCollections();
|
|
18
|
+
seedCollection('subscribers', mockSubscribers);
|
|
19
|
+
const payloadMock = createPayloadRequestMock();
|
|
20
|
+
mockReq = {
|
|
21
|
+
payload: payloadMock.payload,
|
|
22
|
+
user: null
|
|
23
|
+
};
|
|
24
|
+
mockEmailService = createResendMock();
|
|
25
|
+
getEmailService.mockResolvedValue(mockEmailService);
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
describe('beforeChange Hook', ()=>{
|
|
29
|
+
it('should normalize email addresses', async ()=>{
|
|
30
|
+
const beforeChangeHook = async ({ data, req, operation })=>{
|
|
31
|
+
if (operation === 'create' && data.email) {
|
|
32
|
+
data.email = data.email.trim().toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
};
|
|
36
|
+
const result = await beforeChangeHook({
|
|
37
|
+
data: {
|
|
38
|
+
email: ' USER@EXAMPLE.COM ',
|
|
39
|
+
name: 'Test'
|
|
40
|
+
},
|
|
41
|
+
req: mockReq,
|
|
42
|
+
operation: 'create',
|
|
43
|
+
originalDoc: null
|
|
44
|
+
});
|
|
45
|
+
expect(result.email).toBe('user@example.com');
|
|
46
|
+
});
|
|
47
|
+
it('should prevent email changes on update', async ()=>{
|
|
48
|
+
const beforeChangeHook = async ({ data, req, operation, originalDoc })=>{
|
|
49
|
+
if (operation === 'update' && data.email && originalDoc?.email !== data.email) {
|
|
50
|
+
throw new Error('Email cannot be changed');
|
|
51
|
+
}
|
|
52
|
+
return data;
|
|
53
|
+
};
|
|
54
|
+
await expect(beforeChangeHook({
|
|
55
|
+
data: {
|
|
56
|
+
email: 'newemail@example.com'
|
|
57
|
+
},
|
|
58
|
+
req: mockReq,
|
|
59
|
+
operation: 'update',
|
|
60
|
+
originalDoc: {
|
|
61
|
+
email: 'oldemail@example.com'
|
|
62
|
+
}
|
|
63
|
+
})).rejects.toThrow('Email cannot be changed');
|
|
64
|
+
});
|
|
65
|
+
it('should validate subscription status transitions', async ()=>{
|
|
66
|
+
const beforeChangeHook = async ({ data, req, operation, originalDoc })=>{
|
|
67
|
+
if (operation === 'update' && data.subscriptionStatus) {
|
|
68
|
+
const oldStatus = originalDoc?.subscriptionStatus;
|
|
69
|
+
const newStatus = data.subscriptionStatus;
|
|
70
|
+
// Validate allowed transitions
|
|
71
|
+
const allowedTransitions = {
|
|
72
|
+
pending: [
|
|
73
|
+
'active',
|
|
74
|
+
'unsubscribed'
|
|
75
|
+
],
|
|
76
|
+
active: [
|
|
77
|
+
'unsubscribed'
|
|
78
|
+
],
|
|
79
|
+
unsubscribed: [
|
|
80
|
+
'pending'
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
if (!allowedTransitions[oldStatus]?.includes(newStatus)) {
|
|
84
|
+
throw new Error(`Invalid status transition from ${oldStatus} to ${newStatus}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return data;
|
|
88
|
+
};
|
|
89
|
+
// Valid transition
|
|
90
|
+
const result1 = await beforeChangeHook({
|
|
91
|
+
data: {
|
|
92
|
+
subscriptionStatus: 'active'
|
|
93
|
+
},
|
|
94
|
+
req: mockReq,
|
|
95
|
+
operation: 'update',
|
|
96
|
+
originalDoc: {
|
|
97
|
+
subscriptionStatus: 'pending'
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
expect(result1.subscriptionStatus).toBe('active');
|
|
101
|
+
// Invalid transition
|
|
102
|
+
await expect(beforeChangeHook({
|
|
103
|
+
data: {
|
|
104
|
+
subscriptionStatus: 'pending'
|
|
105
|
+
},
|
|
106
|
+
req: mockReq,
|
|
107
|
+
operation: 'update',
|
|
108
|
+
originalDoc: {
|
|
109
|
+
subscriptionStatus: 'active'
|
|
110
|
+
}
|
|
111
|
+
})).rejects.toThrow('Invalid status transition');
|
|
112
|
+
});
|
|
113
|
+
it('should set timestamps appropriately', async ()=>{
|
|
114
|
+
const beforeChangeHook = async ({ data, req, operation })=>{
|
|
115
|
+
const now = new Date();
|
|
116
|
+
if (operation === 'update') {
|
|
117
|
+
// Set unsubscribedAt when unsubscribing
|
|
118
|
+
if (data.subscriptionStatus === 'unsubscribed' && !data.unsubscribedAt) {
|
|
119
|
+
data.unsubscribedAt = now;
|
|
120
|
+
} else if (data.subscriptionStatus === 'pending' && data.unsubscribedAt !== undefined) {
|
|
121
|
+
data.unsubscribedAt = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return data;
|
|
125
|
+
};
|
|
126
|
+
// Unsubscribing
|
|
127
|
+
const result1 = await beforeChangeHook({
|
|
128
|
+
data: {
|
|
129
|
+
subscriptionStatus: 'unsubscribed'
|
|
130
|
+
},
|
|
131
|
+
req: mockReq,
|
|
132
|
+
operation: 'update',
|
|
133
|
+
originalDoc: {
|
|
134
|
+
subscriptionStatus: 'active'
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
expect(result1.unsubscribedAt).toBeInstanceOf(Date);
|
|
138
|
+
// Re-subscribing
|
|
139
|
+
const result2 = await beforeChangeHook({
|
|
140
|
+
data: {
|
|
141
|
+
subscriptionStatus: 'pending',
|
|
142
|
+
unsubscribedAt: new Date()
|
|
143
|
+
},
|
|
144
|
+
req: mockReq,
|
|
145
|
+
operation: 'update',
|
|
146
|
+
originalDoc: {
|
|
147
|
+
subscriptionStatus: 'unsubscribed'
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
expect(result2.unsubscribedAt).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
it('should prevent duplicate emails on create', async ()=>{
|
|
153
|
+
const beforeChangeHook = async ({ data, req, operation })=>{
|
|
154
|
+
if (operation === 'create' && data.email) {
|
|
155
|
+
const existing = await req.payload.find({
|
|
156
|
+
collection: 'subscribers',
|
|
157
|
+
where: {
|
|
158
|
+
email: {
|
|
159
|
+
equals: data.email
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (existing.docs.length > 0) {
|
|
164
|
+
throw new Error('Email already exists');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return data;
|
|
168
|
+
};
|
|
169
|
+
seedCollection('subscribers', [
|
|
170
|
+
{
|
|
171
|
+
id: 'existing',
|
|
172
|
+
email: 'existing@example.com',
|
|
173
|
+
subscriptionStatus: 'active'
|
|
174
|
+
}
|
|
175
|
+
]);
|
|
176
|
+
await expect(beforeChangeHook({
|
|
177
|
+
data: {
|
|
178
|
+
email: 'existing@example.com'
|
|
179
|
+
},
|
|
180
|
+
req: mockReq,
|
|
181
|
+
operation: 'create',
|
|
182
|
+
originalDoc: null
|
|
183
|
+
})).rejects.toThrow('Email already exists');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe('afterChange Hook', ()=>{
|
|
187
|
+
it('should sync with email provider on create', async ()=>{
|
|
188
|
+
const afterChangeHook = async ({ doc, req, operation })=>{
|
|
189
|
+
if (operation === 'create') {
|
|
190
|
+
const emailService = await getEmailService({
|
|
191
|
+
provider: 'resend',
|
|
192
|
+
apiKey: 'test-key',
|
|
193
|
+
fromEmail: 'test@example.com',
|
|
194
|
+
fromName: 'Test'
|
|
195
|
+
});
|
|
196
|
+
await emailService.emails.send({
|
|
197
|
+
from: 'test@example.com',
|
|
198
|
+
to: [
|
|
199
|
+
doc.email
|
|
200
|
+
],
|
|
201
|
+
subject: 'Welcome',
|
|
202
|
+
html: '<p>Welcome!</p>'
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return doc;
|
|
206
|
+
};
|
|
207
|
+
await afterChangeHook({
|
|
208
|
+
doc: {
|
|
209
|
+
id: 'new-sub',
|
|
210
|
+
email: 'new@example.com'
|
|
211
|
+
},
|
|
212
|
+
req: mockReq,
|
|
213
|
+
operation: 'create',
|
|
214
|
+
previousDoc: null
|
|
215
|
+
});
|
|
216
|
+
expect(mockEmailService.emails.send).toHaveBeenCalledWith(expect.objectContaining({
|
|
217
|
+
to: [
|
|
218
|
+
'new@example.com'
|
|
219
|
+
]
|
|
220
|
+
}));
|
|
221
|
+
});
|
|
222
|
+
it('should handle email provider sync failures gracefully', async ()=>{
|
|
223
|
+
mockEmailService.emails.send.mockRejectedValueOnce(new Error('Provider error'));
|
|
224
|
+
const afterChangeHook = async ({ doc, req, operation })=>{
|
|
225
|
+
if (operation === 'create') {
|
|
226
|
+
try {
|
|
227
|
+
const emailService = await getEmailService({
|
|
228
|
+
provider: 'resend',
|
|
229
|
+
apiKey: 'test-key',
|
|
230
|
+
fromEmail: 'test@example.com',
|
|
231
|
+
fromName: 'Test'
|
|
232
|
+
});
|
|
233
|
+
await emailService.emails.send({
|
|
234
|
+
from: 'test@example.com',
|
|
235
|
+
to: [
|
|
236
|
+
doc.email
|
|
237
|
+
],
|
|
238
|
+
subject: 'Welcome',
|
|
239
|
+
html: '<p>Welcome!</p>'
|
|
240
|
+
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// Log error but don't fail the operation
|
|
243
|
+
console.error('Failed to sync with email provider:', error);
|
|
244
|
+
// Could update a sync status field
|
|
245
|
+
doc.emailProviderSyncStatus = 'failed';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return doc;
|
|
249
|
+
};
|
|
250
|
+
const result = await afterChangeHook({
|
|
251
|
+
doc: {
|
|
252
|
+
id: 'new-sub',
|
|
253
|
+
email: 'new@example.com'
|
|
254
|
+
},
|
|
255
|
+
req: mockReq,
|
|
256
|
+
operation: 'create',
|
|
257
|
+
previousDoc: null
|
|
258
|
+
});
|
|
259
|
+
expect(result.emailProviderSyncStatus).toBe('failed');
|
|
260
|
+
});
|
|
261
|
+
it('should clean up magic link tokens after verification', async ()=>{
|
|
262
|
+
const afterChangeHook = async ({ doc, req, operation, previousDoc })=>{
|
|
263
|
+
if (operation === 'update' && previousDoc?.subscriptionStatus === 'pending' && doc.subscriptionStatus === 'active') {
|
|
264
|
+
// Clear any magic link tokens
|
|
265
|
+
await req.payload.update({
|
|
266
|
+
collection: 'subscribers',
|
|
267
|
+
id: doc.id,
|
|
268
|
+
data: {
|
|
269
|
+
magicLinkToken: null,
|
|
270
|
+
magicLinkTokenExpiry: null
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return doc;
|
|
275
|
+
};
|
|
276
|
+
mockReq.payload.update.mockResolvedValueOnce({
|
|
277
|
+
success: true
|
|
278
|
+
});
|
|
279
|
+
await afterChangeHook({
|
|
280
|
+
doc: {
|
|
281
|
+
id: 'sub-123',
|
|
282
|
+
subscriptionStatus: 'active'
|
|
283
|
+
},
|
|
284
|
+
req: mockReq,
|
|
285
|
+
operation: 'update',
|
|
286
|
+
previousDoc: {
|
|
287
|
+
subscriptionStatus: 'pending'
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
expect(mockReq.payload.update).toHaveBeenCalledWith(expect.objectContaining({
|
|
291
|
+
data: {
|
|
292
|
+
magicLinkToken: null,
|
|
293
|
+
magicLinkTokenExpiry: null
|
|
294
|
+
}
|
|
295
|
+
}));
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
describe('beforeDelete Hook', ()=>{
|
|
299
|
+
it('should prevent deletion of active subscribers by non-admins', async ()=>{
|
|
300
|
+
const beforeDeleteHook = async ({ req, id })=>{
|
|
301
|
+
const subscriber = await req.payload.findByID({
|
|
302
|
+
collection: 'subscribers',
|
|
303
|
+
id
|
|
304
|
+
});
|
|
305
|
+
if (subscriber?.subscriptionStatus === 'active' && !req.user?.roles?.includes('admin')) {
|
|
306
|
+
throw new Error('Only admins can delete active subscribers');
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
mockReq.user = {
|
|
310
|
+
id: 'user-123',
|
|
311
|
+
collection: 'users'
|
|
312
|
+
};
|
|
313
|
+
await expect(beforeDeleteHook({
|
|
314
|
+
req: mockReq,
|
|
315
|
+
id: 'sub-1'
|
|
316
|
+
})).rejects.toThrow('Only admins can delete active subscribers');
|
|
317
|
+
// Admin should be allowed
|
|
318
|
+
mockReq.user = createMockAdminUser();
|
|
319
|
+
await expect(beforeDeleteHook({
|
|
320
|
+
req: mockReq,
|
|
321
|
+
id: 'sub-1'
|
|
322
|
+
})).resolves.not.toThrow();
|
|
323
|
+
});
|
|
324
|
+
it('should clean up email provider data on delete', async ()=>{
|
|
325
|
+
const afterDeleteHook = async ({ doc, req })=>{
|
|
326
|
+
try {
|
|
327
|
+
const emailService = await getEmailService({
|
|
328
|
+
provider: 'broadcast',
|
|
329
|
+
apiKey: 'test-key',
|
|
330
|
+
fromEmail: 'test@example.com',
|
|
331
|
+
fromName: 'Test'
|
|
332
|
+
});
|
|
333
|
+
// Remove from email provider
|
|
334
|
+
if (doc.emailProviderId) {
|
|
335
|
+
await emailService.contacts.delete(doc.emailProviderId);
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('Failed to remove from email provider:', error);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const mockBroadcast = createBroadcastMock();
|
|
342
|
+
getEmailService.mockResolvedValue(mockBroadcast);
|
|
343
|
+
await afterDeleteHook({
|
|
344
|
+
doc: {
|
|
345
|
+
id: 'sub-123',
|
|
346
|
+
email: 'test@example.com',
|
|
347
|
+
emailProviderId: 'contact-123'
|
|
348
|
+
},
|
|
349
|
+
req: mockReq
|
|
350
|
+
});
|
|
351
|
+
expect(mockBroadcast.contacts.delete).toHaveBeenCalledWith('contact-123');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
//# sourceMappingURL=subscriber-hooks.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/__tests__/integration/collections/subscriber-hooks.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport type { CollectionBeforeChangeHook, CollectionAfterChangeHook } from 'payload'\nimport { createPayloadRequestMock, seedCollection, clearCollections, createMockAdminUser } from '../../mocks/payload'\nimport { mockSubscribers } from '../../fixtures/subscribers'\nimport { createResendMock } from '../../mocks/email-providers'\nimport type { NewsletterPluginConfig } from '../../../types'\n\n// Mock email service\nvi.mock('../../../services/email', () => ({\n getEmailService: vi.fn(),\n}))\n\nimport { getEmailService } from '../../../services/email'\n\ndescribe('Subscriber Collection Hooks Security', () => {\n let mockReq: any\n let mockEmailService: any\n const config: NewsletterPluginConfig = {\n subscribersSlug: 'subscribers',\n }\n\n beforeEach(() => {\n clearCollections()\n seedCollection('subscribers', mockSubscribers)\n \n const payloadMock = createPayloadRequestMock()\n mockReq = {\n payload: payloadMock.payload,\n user: null,\n }\n \n mockEmailService = createResendMock()\n ;(getEmailService as any).mockResolvedValue(mockEmailService)\n \n vi.clearAllMocks()\n })\n\n describe('beforeChange Hook', () => {\n it('should normalize email addresses', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation }) => {\n if (operation === 'create' && data.email) {\n data.email = data.email.trim().toLowerCase()\n }\n return data\n }\n\n const result = await beforeChangeHook({\n data: { email: ' USER@EXAMPLE.COM ', name: 'Test' },\n req: mockReq,\n operation: 'create',\n originalDoc: null,\n })\n\n expect(result.email).toBe('user@example.com')\n })\n\n it('should prevent email changes on update', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation, originalDoc }) => {\n if (operation === 'update' && data.email && originalDoc?.email !== data.email) {\n throw new Error('Email cannot be changed')\n }\n return data\n }\n\n await expect(\n beforeChangeHook({\n data: { email: 'newemail@example.com' },\n req: mockReq,\n operation: 'update',\n originalDoc: { email: 'oldemail@example.com' },\n })\n ).rejects.toThrow('Email cannot be changed')\n })\n\n it('should validate subscription status transitions', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation, originalDoc }) => {\n if (operation === 'update' && data.subscriptionStatus) {\n const oldStatus = originalDoc?.subscriptionStatus\n const newStatus = data.subscriptionStatus\n \n // Validate allowed transitions\n const allowedTransitions: Record<string, string[]> = {\n pending: ['active', 'unsubscribed'],\n active: ['unsubscribed'],\n unsubscribed: ['pending'], // Re-subscription\n }\n \n if (!allowedTransitions[oldStatus]?.includes(newStatus)) {\n throw new Error(`Invalid status transition from ${oldStatus} to ${newStatus}`)\n }\n }\n return data\n }\n\n // Valid transition\n const result1 = await beforeChangeHook({\n data: { subscriptionStatus: 'active' },\n req: mockReq,\n operation: 'update',\n originalDoc: { subscriptionStatus: 'pending' },\n })\n expect(result1.subscriptionStatus).toBe('active')\n\n // Invalid transition\n await expect(\n beforeChangeHook({\n data: { subscriptionStatus: 'pending' },\n req: mockReq,\n operation: 'update',\n originalDoc: { subscriptionStatus: 'active' },\n })\n ).rejects.toThrow('Invalid status transition')\n })\n\n it('should set timestamps appropriately', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation }) => {\n const now = new Date()\n \n if (operation === 'update') {\n // Set unsubscribedAt when unsubscribing\n if (data.subscriptionStatus === 'unsubscribed' && !data.unsubscribedAt) {\n data.unsubscribedAt = now\n }\n // Clear unsubscribedAt when re-subscribing\n else if (data.subscriptionStatus === 'pending' && data.unsubscribedAt !== undefined) {\n data.unsubscribedAt = null\n }\n }\n \n return data\n }\n\n // Unsubscribing\n const result1 = await beforeChangeHook({\n data: { subscriptionStatus: 'unsubscribed' },\n req: mockReq,\n operation: 'update',\n originalDoc: { subscriptionStatus: 'active' },\n })\n expect(result1.unsubscribedAt).toBeInstanceOf(Date)\n\n // Re-subscribing\n const result2 = await beforeChangeHook({\n data: { subscriptionStatus: 'pending', unsubscribedAt: new Date() },\n req: mockReq,\n operation: 'update',\n originalDoc: { subscriptionStatus: 'unsubscribed' },\n })\n expect(result2.unsubscribedAt).toBeNull()\n })\n\n it('should prevent duplicate emails on create', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation }) => {\n if (operation === 'create' && data.email) {\n const existing = await req.payload.find({\n collection: 'subscribers',\n where: {\n email: { equals: data.email },\n },\n })\n \n if (existing.docs.length > 0) {\n throw new Error('Email already exists')\n }\n }\n return data\n }\n\n seedCollection('subscribers', [{\n id: 'existing',\n email: 'existing@example.com',\n subscriptionStatus: 'active',\n }])\n\n await expect(\n beforeChangeHook({\n data: { email: 'existing@example.com' },\n req: mockReq,\n operation: 'create',\n originalDoc: null,\n })\n ).rejects.toThrow('Email already exists')\n })\n })\n\n describe('afterChange Hook', () => {\n it('should sync with email provider on create', async () => {\n const afterChangeHook: CollectionAfterChangeHook = async ({ doc, req, operation }) => {\n if (operation === 'create') {\n const emailService = await getEmailService({\n provider: 'resend',\n apiKey: 'test-key',\n fromEmail: 'test@example.com',\n fromName: 'Test',\n })\n \n await emailService.emails.send({\n from: 'test@example.com',\n to: [doc.email],\n subject: 'Welcome',\n html: '<p>Welcome!</p>',\n })\n }\n return doc\n }\n\n await afterChangeHook({\n doc: { id: 'new-sub', email: 'new@example.com' },\n req: mockReq,\n operation: 'create',\n previousDoc: null,\n })\n\n expect(mockEmailService.emails.send).toHaveBeenCalledWith(\n expect.objectContaining({\n to: ['new@example.com'],\n })\n )\n })\n\n it('should handle email provider sync failures gracefully', async () => {\n mockEmailService.emails.send.mockRejectedValueOnce(new Error('Provider error'))\n\n const afterChangeHook: CollectionAfterChangeHook = async ({ doc, req, operation }) => {\n if (operation === 'create') {\n try {\n const emailService = await getEmailService({\n provider: 'resend',\n apiKey: 'test-key',\n fromEmail: 'test@example.com',\n fromName: 'Test',\n })\n \n await emailService.emails.send({\n from: 'test@example.com',\n to: [doc.email],\n subject: 'Welcome',\n html: '<p>Welcome!</p>',\n })\n } catch (error) {\n // Log error but don't fail the operation\n console.error('Failed to sync with email provider:', error)\n // Could update a sync status field\n doc.emailProviderSyncStatus = 'failed'\n }\n }\n return doc\n }\n\n const result = await afterChangeHook({\n doc: { id: 'new-sub', email: 'new@example.com' },\n req: mockReq,\n operation: 'create',\n previousDoc: null,\n })\n\n expect(result.emailProviderSyncStatus).toBe('failed')\n })\n\n it('should clean up magic link tokens after verification', async () => {\n const afterChangeHook: CollectionAfterChangeHook = async ({ doc, req, operation, previousDoc }) => {\n if (operation === 'update' && \n previousDoc?.subscriptionStatus === 'pending' && \n doc.subscriptionStatus === 'active') {\n // Clear any magic link tokens\n await req.payload.update({\n collection: 'subscribers',\n id: doc.id,\n data: {\n magicLinkToken: null,\n magicLinkTokenExpiry: null,\n },\n })\n }\n return doc\n }\n\n mockReq.payload.update.mockResolvedValueOnce({ success: true })\n\n await afterChangeHook({\n doc: { id: 'sub-123', subscriptionStatus: 'active' },\n req: mockReq,\n operation: 'update',\n previousDoc: { subscriptionStatus: 'pending' },\n })\n\n expect(mockReq.payload.update).toHaveBeenCalledWith(\n expect.objectContaining({\n data: {\n magicLinkToken: null,\n magicLinkTokenExpiry: null,\n },\n })\n )\n })\n })\n\n describe('beforeDelete Hook', () => {\n it('should prevent deletion of active subscribers by non-admins', async () => {\n const beforeDeleteHook = async ({ req, id }: any) => {\n const subscriber = await req.payload.findByID({\n collection: 'subscribers',\n id,\n })\n \n if (subscriber?.subscriptionStatus === 'active' && !req.user?.roles?.includes('admin')) {\n throw new Error('Only admins can delete active subscribers')\n }\n }\n\n mockReq.user = { id: 'user-123', collection: 'users' }\n\n await expect(\n beforeDeleteHook({\n req: mockReq,\n id: 'sub-1', // Active subscriber\n })\n ).rejects.toThrow('Only admins can delete active subscribers')\n\n // Admin should be allowed\n mockReq.user = createMockAdminUser()\n await expect(\n beforeDeleteHook({\n req: mockReq,\n id: 'sub-1',\n })\n ).resolves.not.toThrow()\n })\n\n it('should clean up email provider data on delete', async () => {\n const afterDeleteHook = async ({ doc, req }: any) => {\n try {\n const emailService = await getEmailService({\n provider: 'broadcast',\n apiKey: 'test-key',\n fromEmail: 'test@example.com',\n fromName: 'Test',\n })\n \n // Remove from email provider\n if (doc.emailProviderId) {\n await emailService.contacts.delete(doc.emailProviderId)\n }\n } catch (error) {\n console.error('Failed to remove from email provider:', error)\n }\n }\n\n const mockBroadcast = createBroadcastMock()\n ;(getEmailService as any).mockResolvedValue(mockBroadcast)\n\n await afterDeleteHook({\n doc: { id: 'sub-123', email: 'test@example.com', emailProviderId: 'contact-123' },\n req: mockReq,\n })\n\n expect(mockBroadcast.contacts.delete).toHaveBeenCalledWith('contact-123')\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","createPayloadRequestMock","seedCollection","clearCollections","createMockAdminUser","mockSubscribers","createResendMock","mock","getEmailService","fn","mockReq","mockEmailService","config","subscribersSlug","payloadMock","payload","user","mockResolvedValue","clearAllMocks","beforeChangeHook","data","req","operation","email","trim","toLowerCase","result","name","originalDoc","toBe","Error","rejects","toThrow","subscriptionStatus","oldStatus","newStatus","allowedTransitions","pending","active","unsubscribed","includes","result1","now","Date","unsubscribedAt","undefined","toBeInstanceOf","result2","toBeNull","existing","find","collection","where","equals","docs","length","id","afterChangeHook","doc","emailService","provider","apiKey","fromEmail","fromName","emails","send","from","to","subject","html","previousDoc","toHaveBeenCalledWith","objectContaining","mockRejectedValueOnce","error","console","emailProviderSyncStatus","update","magicLinkToken","magicLinkTokenExpiry","mockResolvedValueOnce","success","beforeDeleteHook","subscriber","findByID","roles","resolves","not","afterDeleteHook","emailProviderId","contacts","delete","mockBroadcast","createBroadcastMock"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,EAAE,EAAEC,MAAM,EAAEC,UAAU,EAAEC,EAAE,QAAQ,SAAQ;AAE7D,SAASC,wBAAwB,EAAEC,cAAc,EAAEC,gBAAgB,EAAEC,mBAAmB,QAAQ,sBAAqB;AACrH,SAASC,eAAe,QAAQ,6BAA4B;AAC5D,SAASC,gBAAgB,QAAQ,8BAA6B;AAG9D,qBAAqB;AACrBN,GAAGO,IAAI,CAAC,2BAA2B,IAAO,CAAA;QACxCC,iBAAiBR,GAAGS,EAAE;IACxB,CAAA;AAEA,SAASD,eAAe,QAAQ,0BAAyB;AAEzDZ,SAAS,wCAAwC;IAC/C,IAAIc;IACJ,IAAIC;IACJ,MAAMC,SAAiC;QACrCC,iBAAiB;IACnB;IAEAd,WAAW;QACTI;QACAD,eAAe,eAAeG;QAE9B,MAAMS,cAAcb;QACpBS,UAAU;YACRK,SAASD,YAAYC,OAAO;YAC5BC,MAAM;QACR;QAEAL,mBAAmBL;QACjBE,gBAAwBS,iBAAiB,CAACN;QAE5CX,GAAGkB,aAAa;IAClB;IAEAtB,SAAS,qBAAqB;QAC5BC,GAAG,oCAAoC;YACrC,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAE;gBAClF,IAAIA,cAAc,YAAYF,KAAKG,KAAK,EAAE;oBACxCH,KAAKG,KAAK,GAAGH,KAAKG,KAAK,CAACC,IAAI,GAAGC,WAAW;gBAC5C;gBACA,OAAOL;YACT;YAEA,MAAMM,SAAS,MAAMP,iBAAiB;gBACpCC,MAAM;oBAAEG,OAAO;oBAAwBI,MAAM;gBAAO;gBACpDN,KAAKX;gBACLY,WAAW;gBACXM,aAAa;YACf;YAEA9B,OAAO4B,OAAOH,KAAK,EAAEM,IAAI,CAAC;QAC5B;QAEAhC,GAAG,0CAA0C;YAC3C,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAEM,WAAW,EAAE;gBAC/F,IAAIN,cAAc,YAAYF,KAAKG,KAAK,IAAIK,aAAaL,UAAUH,KAAKG,KAAK,EAAE;oBAC7E,MAAM,IAAIO,MAAM;gBAClB;gBACA,OAAOV;YACT;YAEA,MAAMtB,OACJqB,iBAAiB;gBACfC,MAAM;oBAAEG,OAAO;gBAAuB;gBACtCF,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEL,OAAO;gBAAuB;YAC/C,IACAQ,OAAO,CAACC,OAAO,CAAC;QACpB;QAEAnC,GAAG,mDAAmD;YACpD,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAEM,WAAW,EAAE;gBAC/F,IAAIN,cAAc,YAAYF,KAAKa,kBAAkB,EAAE;oBACrD,MAAMC,YAAYN,aAAaK;oBAC/B,MAAME,YAAYf,KAAKa,kBAAkB;oBAEzC,+BAA+B;oBAC/B,MAAMG,qBAA+C;wBACnDC,SAAS;4BAAC;4BAAU;yBAAe;wBACnCC,QAAQ;4BAAC;yBAAe;wBACxBC,cAAc;4BAAC;yBAAU;oBAC3B;oBAEA,IAAI,CAACH,kBAAkB,CAACF,UAAU,EAAEM,SAASL,YAAY;wBACvD,MAAM,IAAIL,MAAM,CAAC,+BAA+B,EAAEI,UAAU,IAAI,EAAEC,WAAW;oBAC/E;gBACF;gBACA,OAAOf;YACT;YAEA,mBAAmB;YACnB,MAAMqB,UAAU,MAAMtB,iBAAiB;gBACrCC,MAAM;oBAAEa,oBAAoB;gBAAS;gBACrCZ,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEK,oBAAoB;gBAAU;YAC/C;YACAnC,OAAO2C,QAAQR,kBAAkB,EAAEJ,IAAI,CAAC;YAExC,qBAAqB;YACrB,MAAM/B,OACJqB,iBAAiB;gBACfC,MAAM;oBAAEa,oBAAoB;gBAAU;gBACtCZ,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEK,oBAAoB;gBAAS;YAC9C,IACAF,OAAO,CAACC,OAAO,CAAC;QACpB;QAEAnC,GAAG,uCAAuC;YACxC,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAE;gBAClF,MAAMoB,MAAM,IAAIC;gBAEhB,IAAIrB,cAAc,UAAU;oBAC1B,wCAAwC;oBACxC,IAAIF,KAAKa,kBAAkB,KAAK,kBAAkB,CAACb,KAAKwB,cAAc,EAAE;wBACtExB,KAAKwB,cAAc,GAAGF;oBACxB,OAEK,IAAItB,KAAKa,kBAAkB,KAAK,aAAab,KAAKwB,cAAc,KAAKC,WAAW;wBACnFzB,KAAKwB,cAAc,GAAG;oBACxB;gBACF;gBAEA,OAAOxB;YACT;YAEA,gBAAgB;YAChB,MAAMqB,UAAU,MAAMtB,iBAAiB;gBACrCC,MAAM;oBAAEa,oBAAoB;gBAAe;gBAC3CZ,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEK,oBAAoB;gBAAS;YAC9C;YACAnC,OAAO2C,QAAQG,cAAc,EAAEE,cAAc,CAACH;YAE9C,iBAAiB;YACjB,MAAMI,UAAU,MAAM5B,iBAAiB;gBACrCC,MAAM;oBAAEa,oBAAoB;oBAAWW,gBAAgB,IAAID;gBAAO;gBAClEtB,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEK,oBAAoB;gBAAe;YACpD;YACAnC,OAAOiD,QAAQH,cAAc,EAAEI,QAAQ;QACzC;QAEAnD,GAAG,6CAA6C;YAC9C,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAE;gBAClF,IAAIA,cAAc,YAAYF,KAAKG,KAAK,EAAE;oBACxC,MAAM0B,WAAW,MAAM5B,IAAIN,OAAO,CAACmC,IAAI,CAAC;wBACtCC,YAAY;wBACZC,OAAO;4BACL7B,OAAO;gCAAE8B,QAAQjC,KAAKG,KAAK;4BAAC;wBAC9B;oBACF;oBAEA,IAAI0B,SAASK,IAAI,CAACC,MAAM,GAAG,GAAG;wBAC5B,MAAM,IAAIzB,MAAM;oBAClB;gBACF;gBACA,OAAOV;YACT;YAEAlB,eAAe,eAAe;gBAAC;oBAC7BsD,IAAI;oBACJjC,OAAO;oBACPU,oBAAoB;gBACtB;aAAE;YAEF,MAAMnC,OACJqB,iBAAiB;gBACfC,MAAM;oBAAEG,OAAO;gBAAuB;gBACtCF,KAAKX;gBACLY,WAAW;gBACXM,aAAa;YACf,IACAG,OAAO,CAACC,OAAO,CAAC;QACpB;IACF;IAEApC,SAAS,oBAAoB;QAC3BC,GAAG,6CAA6C;YAC9C,MAAM4D,kBAA6C,OAAO,EAAEC,GAAG,EAAErC,GAAG,EAAEC,SAAS,EAAE;gBAC/E,IAAIA,cAAc,UAAU;oBAC1B,MAAMqC,eAAe,MAAMnD,gBAAgB;wBACzCoD,UAAU;wBACVC,QAAQ;wBACRC,WAAW;wBACXC,UAAU;oBACZ;oBAEA,MAAMJ,aAAaK,MAAM,CAACC,IAAI,CAAC;wBAC7BC,MAAM;wBACNC,IAAI;4BAACT,IAAInC,KAAK;yBAAC;wBACf6C,SAAS;wBACTC,MAAM;oBACR;gBACF;gBACA,OAAOX;YACT;YAEA,MAAMD,gBAAgB;gBACpBC,KAAK;oBAAEF,IAAI;oBAAWjC,OAAO;gBAAkB;gBAC/CF,KAAKX;gBACLY,WAAW;gBACXgD,aAAa;YACf;YAEAxE,OAAOa,iBAAiBqD,MAAM,CAACC,IAAI,EAAEM,oBAAoB,CACvDzE,OAAO0E,gBAAgB,CAAC;gBACtBL,IAAI;oBAAC;iBAAkB;YACzB;QAEJ;QAEAtE,GAAG,yDAAyD;YAC1Dc,iBAAiBqD,MAAM,CAACC,IAAI,CAACQ,qBAAqB,CAAC,IAAI3C,MAAM;YAE7D,MAAM2B,kBAA6C,OAAO,EAAEC,GAAG,EAAErC,GAAG,EAAEC,SAAS,EAAE;gBAC/E,IAAIA,cAAc,UAAU;oBAC1B,IAAI;wBACF,MAAMqC,eAAe,MAAMnD,gBAAgB;4BACzCoD,UAAU;4BACVC,QAAQ;4BACRC,WAAW;4BACXC,UAAU;wBACZ;wBAEA,MAAMJ,aAAaK,MAAM,CAACC,IAAI,CAAC;4BAC7BC,MAAM;4BACNC,IAAI;gCAACT,IAAInC,KAAK;6BAAC;4BACf6C,SAAS;4BACTC,MAAM;wBACR;oBACF,EAAE,OAAOK,OAAO;wBACd,yCAAyC;wBACzCC,QAAQD,KAAK,CAAC,uCAAuCA;wBACrD,mCAAmC;wBACnChB,IAAIkB,uBAAuB,GAAG;oBAChC;gBACF;gBACA,OAAOlB;YACT;YAEA,MAAMhC,SAAS,MAAM+B,gBAAgB;gBACnCC,KAAK;oBAAEF,IAAI;oBAAWjC,OAAO;gBAAkB;gBAC/CF,KAAKX;gBACLY,WAAW;gBACXgD,aAAa;YACf;YAEAxE,OAAO4B,OAAOkD,uBAAuB,EAAE/C,IAAI,CAAC;QAC9C;QAEAhC,GAAG,wDAAwD;YACzD,MAAM4D,kBAA6C,OAAO,EAAEC,GAAG,EAAErC,GAAG,EAAEC,SAAS,EAAEgD,WAAW,EAAE;gBAC5F,IAAIhD,cAAc,YACdgD,aAAarC,uBAAuB,aACpCyB,IAAIzB,kBAAkB,KAAK,UAAU;oBACvC,8BAA8B;oBAC9B,MAAMZ,IAAIN,OAAO,CAAC8D,MAAM,CAAC;wBACvB1B,YAAY;wBACZK,IAAIE,IAAIF,EAAE;wBACVpC,MAAM;4BACJ0D,gBAAgB;4BAChBC,sBAAsB;wBACxB;oBACF;gBACF;gBACA,OAAOrB;YACT;YAEAhD,QAAQK,OAAO,CAAC8D,MAAM,CAACG,qBAAqB,CAAC;gBAAEC,SAAS;YAAK;YAE7D,MAAMxB,gBAAgB;gBACpBC,KAAK;oBAAEF,IAAI;oBAAWvB,oBAAoB;gBAAS;gBACnDZ,KAAKX;gBACLY,WAAW;gBACXgD,aAAa;oBAAErC,oBAAoB;gBAAU;YAC/C;YAEAnC,OAAOY,QAAQK,OAAO,CAAC8D,MAAM,EAAEN,oBAAoB,CACjDzE,OAAO0E,gBAAgB,CAAC;gBACtBpD,MAAM;oBACJ0D,gBAAgB;oBAChBC,sBAAsB;gBACxB;YACF;QAEJ;IACF;IAEAnF,SAAS,qBAAqB;QAC5BC,GAAG,+DAA+D;YAChE,MAAMqF,mBAAmB,OAAO,EAAE7D,GAAG,EAAEmC,EAAE,EAAO;gBAC9C,MAAM2B,aAAa,MAAM9D,IAAIN,OAAO,CAACqE,QAAQ,CAAC;oBAC5CjC,YAAY;oBACZK;gBACF;gBAEA,IAAI2B,YAAYlD,uBAAuB,YAAY,CAACZ,IAAIL,IAAI,EAAEqE,OAAO7C,SAAS,UAAU;oBACtF,MAAM,IAAIV,MAAM;gBAClB;YACF;YAEApB,QAAQM,IAAI,GAAG;gBAAEwC,IAAI;gBAAYL,YAAY;YAAQ;YAErD,MAAMrD,OACJoF,iBAAiB;gBACf7D,KAAKX;gBACL8C,IAAI;YACN,IACAzB,OAAO,CAACC,OAAO,CAAC;YAElB,0BAA0B;YAC1BtB,QAAQM,IAAI,GAAGZ;YACf,MAAMN,OACJoF,iBAAiB;gBACf7D,KAAKX;gBACL8C,IAAI;YACN,IACA8B,QAAQ,CAACC,GAAG,CAACvD,OAAO;QACxB;QAEAnC,GAAG,iDAAiD;YAClD,MAAM2F,kBAAkB,OAAO,EAAE9B,GAAG,EAAErC,GAAG,EAAO;gBAC9C,IAAI;oBACF,MAAMsC,eAAe,MAAMnD,gBAAgB;wBACzCoD,UAAU;wBACVC,QAAQ;wBACRC,WAAW;wBACXC,UAAU;oBACZ;oBAEA,6BAA6B;oBAC7B,IAAIL,IAAI+B,eAAe,EAAE;wBACvB,MAAM9B,aAAa+B,QAAQ,CAACC,MAAM,CAACjC,IAAI+B,eAAe;oBACxD;gBACF,EAAE,OAAOf,OAAO;oBACdC,QAAQD,KAAK,CAAC,yCAAyCA;gBACzD;YACF;YAEA,MAAMkB,gBAAgBC;YACpBrF,gBAAwBS,iBAAiB,CAAC2E;YAE5C,MAAMJ,gBAAgB;gBACpB9B,KAAK;oBAAEF,IAAI;oBAAWjC,OAAO;oBAAoBkE,iBAAiB;gBAAc;gBAChFpE,KAAKX;YACP;YAEAZ,OAAO8F,cAAcF,QAAQ,CAACC,MAAM,EAAEpB,oBAAoB,CAAC;QAC7D;IACF;AACF"}
|