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.
- package/CHANGELOG.md +44 -1
- package/CLAUDE.md +31 -19
- package/dist/client.cjs +899 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +52 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.js +867 -0
- package/dist/client.js.map +1 -0
- package/dist/components.cjs +899 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.cts +4 -0
- package/dist/components.d.ts +4 -0
- package/dist/components.js +867 -0
- package/dist/components.js.map +1 -0
- package/dist/index.cjs +2004 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +1967 -0
- package/dist/index.js.map +1 -0
- package/dist/types.cjs +19 -0
- package/dist/types.cjs.map +1 -0
- package/dist/{types/index.d.ts → types.d.cts} +19 -17
- package/dist/types.d.ts +350 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -25
- package/dist/.tsbuildinfo +0 -1
- package/dist/collections/NewsletterSettings.d.ts +0 -4
- package/dist/collections/NewsletterSettings.d.ts.map +0 -1
- package/dist/collections/Subscribers.d.ts +0 -4
- package/dist/collections/Subscribers.d.ts.map +0 -1
- package/dist/components/MagicLinkVerify.d.ts +0 -27
- package/dist/components/MagicLinkVerify.d.ts.map +0 -1
- package/dist/components/NewsletterForm.d.ts +0 -5
- package/dist/components/NewsletterForm.d.ts.map +0 -1
- package/dist/components/PreferencesForm.d.ts +0 -5
- package/dist/components/PreferencesForm.d.ts.map +0 -1
- package/dist/components/index.d.ts +0 -5
- package/dist/components/index.d.ts.map +0 -1
- package/dist/endpoints/index.d.ts +0 -4
- package/dist/endpoints/index.d.ts.map +0 -1
- package/dist/endpoints/preferences.d.ts +0 -5
- package/dist/endpoints/preferences.d.ts.map +0 -1
- package/dist/endpoints/subscribe.d.ts +0 -4
- package/dist/endpoints/subscribe.d.ts.map +0 -1
- package/dist/endpoints/unsubscribe.d.ts +0 -4
- package/dist/endpoints/unsubscribe.d.ts.map +0 -1
- package/dist/endpoints/verify-magic-link.d.ts +0 -4
- package/dist/endpoints/verify-magic-link.d.ts.map +0 -1
- package/dist/exports/client.d.ts +0 -6
- package/dist/exports/client.d.ts.map +0 -1
- package/dist/exports/components.d.ts +0 -2
- package/dist/exports/components.d.ts.map +0 -1
- package/dist/exports/types.d.ts +0 -2
- package/dist/exports/types.d.ts.map +0 -1
- package/dist/fields/newsletterScheduling.d.ts +0 -4
- package/dist/fields/newsletterScheduling.d.ts.map +0 -1
- package/dist/hooks/useNewsletterAuth.d.ts +0 -16
- package/dist/hooks/useNewsletterAuth.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/providers/broadcast.d.ts +0 -19
- package/dist/providers/broadcast.d.ts.map +0 -1
- package/dist/providers/index.d.ts +0 -23
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/resend.d.ts +0 -20
- package/dist/providers/resend.d.ts.map +0 -1
- package/dist/providers/types.d.ts +0 -46
- package/dist/providers/types.d.ts.map +0 -1
- package/dist/src/__tests__/fixtures/newsletter-settings.js +0 -41
- package/dist/src/__tests__/fixtures/newsletter-settings.js.map +0 -1
- package/dist/src/__tests__/fixtures/subscribers.js +0 -70
- package/dist/src/__tests__/fixtures/subscribers.js.map +0 -1
- package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js +0 -356
- package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js.map +0 -1
- package/dist/src/__tests__/integration/endpoints/preferences.test.js +0 -266
- package/dist/src/__tests__/integration/endpoints/preferences.test.js.map +0 -1
- package/dist/src/__tests__/integration/endpoints/subscribe.test.js +0 -280
- package/dist/src/__tests__/integration/endpoints/subscribe.test.js.map +0 -1
- package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js +0 -187
- package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js.map +0 -1
- package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js +0 -188
- package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js.map +0 -1
- package/dist/src/__tests__/mocks/email-providers.js +0 -153
- package/dist/src/__tests__/mocks/email-providers.js.map +0 -1
- package/dist/src/__tests__/mocks/payload.js +0 -244
- package/dist/src/__tests__/mocks/payload.js.map +0 -1
- package/dist/src/__tests__/security/csrf-protection.test.js +0 -309
- package/dist/src/__tests__/security/csrf-protection.test.js.map +0 -1
- package/dist/src/__tests__/security/settings-access.test.js +0 -204
- package/dist/src/__tests__/security/settings-access.test.js.map +0 -1
- package/dist/src/__tests__/security/subscriber-access.test.js +0 -210
- package/dist/src/__tests__/security/subscriber-access.test.js.map +0 -1
- package/dist/src/__tests__/security/xss-prevention.test.js +0 -305
- package/dist/src/__tests__/security/xss-prevention.test.js.map +0 -1
- package/dist/src/__tests__/setup/integration.setup.js +0 -38
- package/dist/src/__tests__/setup/integration.setup.js.map +0 -1
- package/dist/src/__tests__/setup/unit.setup.js +0 -41
- package/dist/src/__tests__/setup/unit.setup.js.map +0 -1
- package/dist/src/__tests__/unit/utils/access.test.js +0 -116
- package/dist/src/__tests__/unit/utils/access.test.js.map +0 -1
- package/dist/src/__tests__/unit/utils/jwt.test.js +0 -238
- package/dist/src/__tests__/unit/utils/jwt.test.js.map +0 -1
- package/dist/src/collections/NewsletterSettings.js +0 -390
- package/dist/src/collections/NewsletterSettings.js.map +0 -1
- package/dist/src/collections/Subscribers.js +0 -309
- package/dist/src/collections/Subscribers.js.map +0 -1
- package/dist/src/components/MagicLinkVerify.js +0 -180
- package/dist/src/components/MagicLinkVerify.js.map +0 -1
- package/dist/src/components/NewsletterForm.js +0 -326
- package/dist/src/components/NewsletterForm.js.map +0 -1
- package/dist/src/components/PreferencesForm.js +0 -524
- package/dist/src/components/PreferencesForm.js.map +0 -1
- package/dist/src/components/index.js +0 -5
- package/dist/src/components/index.js.map +0 -1
- package/dist/src/endpoints/index.js +0 -17
- package/dist/src/endpoints/index.js.map +0 -1
- package/dist/src/endpoints/preferences.js +0 -136
- package/dist/src/endpoints/preferences.js.map +0 -1
- package/dist/src/endpoints/subscribe.js +0 -151
- package/dist/src/endpoints/subscribe.js.map +0 -1
- package/dist/src/endpoints/unsubscribe.js +0 -105
- package/dist/src/endpoints/unsubscribe.js.map +0 -1
- package/dist/src/endpoints/verify-magic-link.js +0 -103
- package/dist/src/endpoints/verify-magic-link.js.map +0 -1
- package/dist/src/exports/client.js +0 -7
- package/dist/src/exports/client.js.map +0 -1
- package/dist/src/exports/components.js +0 -6
- package/dist/src/exports/components.js.map +0 -1
- package/dist/src/exports/types.js +0 -3
- package/dist/src/exports/types.js.map +0 -1
- package/dist/src/fields/newsletterScheduling.js +0 -195
- package/dist/src/fields/newsletterScheduling.js.map +0 -1
- package/dist/src/hooks/useNewsletterAuth.js +0 -112
- package/dist/src/hooks/useNewsletterAuth.js.map +0 -1
- package/dist/src/index.js +0 -130
- package/dist/src/index.js.map +0 -1
- package/dist/src/providers/broadcast.js +0 -158
- package/dist/src/providers/broadcast.js.map +0 -1
- package/dist/src/providers/index.js +0 -63
- package/dist/src/providers/index.js.map +0 -1
- package/dist/src/providers/resend.js +0 -122
- package/dist/src/providers/resend.js.map +0 -1
- package/dist/src/providers/types.js +0 -12
- package/dist/src/providers/types.js.map +0 -1
- package/dist/src/templates/BaseTemplate.js +0 -105
- package/dist/src/templates/BaseTemplate.js.map +0 -1
- package/dist/src/templates/MagicLinkTemplate.js +0 -178
- package/dist/src/templates/MagicLinkTemplate.js.map +0 -1
- package/dist/src/templates/NewsletterTemplate.js +0 -150
- package/dist/src/templates/NewsletterTemplate.js.map +0 -1
- package/dist/src/templates/WelcomeTemplate.js +0 -192
- package/dist/src/templates/WelcomeTemplate.js.map +0 -1
- package/dist/src/templates/index.js +0 -6
- package/dist/src/templates/index.js.map +0 -1
- package/dist/src/types/index.js +0 -3
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/utils/access.js +0 -80
- package/dist/src/utils/access.js.map +0 -1
- package/dist/src/utils/jwt.js +0 -91
- package/dist/src/utils/jwt.js.map +0 -1
- package/dist/src/utils/validation.js +0 -74
- package/dist/src/utils/validation.js.map +0 -1
- package/dist/templates/BaseTemplate.d.ts +0 -45
- package/dist/templates/BaseTemplate.d.ts.map +0 -1
- package/dist/templates/MagicLinkTemplate.d.ts +0 -67
- package/dist/templates/MagicLinkTemplate.d.ts.map +0 -1
- package/dist/templates/NewsletterTemplate.d.ts +0 -112
- package/dist/templates/NewsletterTemplate.d.ts.map +0 -1
- package/dist/templates/WelcomeTemplate.d.ts +0 -55
- package/dist/templates/WelcomeTemplate.d.ts.map +0 -1
- package/dist/templates/index.d.ts +0 -7
- package/dist/templates/index.d.ts.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/utils/access.d.ts +0 -15
- package/dist/utils/access.d.ts.map +0 -1
- package/dist/utils/jwt.d.ts +0 -32
- package/dist/utils/jwt.d.ts.map +0 -1
- package/dist/utils/validation.d.ts +0 -25
- package/dist/utils/validation.d.ts.map +0 -1
|
@@ -1,41 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,116 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import jwt from 'jsonwebtoken';
|
|
3
|
-
import { generateMagicLinkToken, verifyMagicLinkToken, generateSessionToken, verifySessionToken, generateMagicLinkURL } from '../../../utils/jwt';
|
|
4
|
-
// Mock jsonwebtoken
|
|
5
|
-
vi.mock('jsonwebtoken', ()=>({
|
|
6
|
-
default: {
|
|
7
|
-
sign: vi.fn(),
|
|
8
|
-
verify: vi.fn(),
|
|
9
|
-
TokenExpiredError: class TokenExpiredError extends Error {
|
|
10
|
-
constructor(message, expiredAt){
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'TokenExpiredError';
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
JsonWebTokenError: class JsonWebTokenError extends Error {
|
|
16
|
-
constructor(message){
|
|
17
|
-
super(message);
|
|
18
|
-
this.name = 'JsonWebTokenError';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}));
|
|
23
|
-
describe('JWT Utilities Security', ()=>{
|
|
24
|
-
const mockSecret = 'test-secret-key';
|
|
25
|
-
const mockConfig = {};
|
|
26
|
-
beforeEach(()=>{
|
|
27
|
-
process.env.JWT_SECRET = mockSecret;
|
|
28
|
-
vi.clearAllMocks();
|
|
29
|
-
});
|
|
30
|
-
describe('generateMagicLinkToken', ()=>{
|
|
31
|
-
it('should generate token with correct payload', ()=>{
|
|
32
|
-
const mockToken = 'mock-magic-link-token';
|
|
33
|
-
jwt.sign.mockReturnValue(mockToken);
|
|
34
|
-
const token = generateMagicLinkToken('sub-123', 'test@example.com', mockConfig);
|
|
35
|
-
expect(jwt.sign).toHaveBeenCalledWith({
|
|
36
|
-
subscriberId: 'sub-123',
|
|
37
|
-
email: 'test@example.com',
|
|
38
|
-
type: 'magic-link'
|
|
39
|
-
}, mockSecret, {
|
|
40
|
-
expiresIn: '7d',
|
|
41
|
-
issuer: 'payload-newsletter-plugin'
|
|
42
|
-
});
|
|
43
|
-
expect(token).toBe(mockToken);
|
|
44
|
-
});
|
|
45
|
-
it('should respect custom token expiration', ()=>{
|
|
46
|
-
const customConfig = {
|
|
47
|
-
auth: {
|
|
48
|
-
tokenExpiration: '24h'
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
generateMagicLinkToken('sub-123', 'test@example.com', customConfig);
|
|
52
|
-
const options = jwt.sign.mock.calls[0][2];
|
|
53
|
-
expect(options.expiresIn).toBe('24h');
|
|
54
|
-
});
|
|
55
|
-
it('should include correct token type', ()=>{
|
|
56
|
-
generateMagicLinkToken('sub-123', 'test@example.com', mockConfig);
|
|
57
|
-
const payload = jwt.sign.mock.calls[0][0];
|
|
58
|
-
expect(payload.type).toBe('magic-link');
|
|
59
|
-
expect(payload).not.toHaveProperty('action'); // No action field in actual implementation
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
describe('verifyMagicLinkToken', ()=>{
|
|
63
|
-
it('should verify and return valid token payload', ()=>{
|
|
64
|
-
const mockPayload = {
|
|
65
|
-
subscriberId: 'sub-123',
|
|
66
|
-
email: 'test@example.com',
|
|
67
|
-
type: 'magic-link'
|
|
68
|
-
};
|
|
69
|
-
jwt.verify.mockReturnValue(mockPayload);
|
|
70
|
-
const payload = verifyMagicLinkToken('valid-token');
|
|
71
|
-
expect(jwt.verify).toHaveBeenCalledWith('valid-token', mockSecret, {
|
|
72
|
-
issuer: 'payload-newsletter-plugin'
|
|
73
|
-
});
|
|
74
|
-
expect(payload).toEqual(mockPayload);
|
|
75
|
-
});
|
|
76
|
-
it('should reject tokens with wrong type', ()=>{
|
|
77
|
-
const wrongTypePayload = {
|
|
78
|
-
subscriberId: 'sub-123',
|
|
79
|
-
email: 'test@example.com',
|
|
80
|
-
type: 'session'
|
|
81
|
-
};
|
|
82
|
-
jwt.verify.mockReturnValue(wrongTypePayload);
|
|
83
|
-
expect(()=>verifyMagicLinkToken('wrong-type-token')).toThrow('Invalid token type');
|
|
84
|
-
});
|
|
85
|
-
it('should handle expired tokens', ()=>{
|
|
86
|
-
;
|
|
87
|
-
jwt.verify.mockImplementation(()=>{
|
|
88
|
-
const error = new Error('Token expired');
|
|
89
|
-
error.name = 'TokenExpiredError';
|
|
90
|
-
throw error;
|
|
91
|
-
});
|
|
92
|
-
expect(()=>verifyMagicLinkToken('expired-token')).toThrow('Magic link has expired');
|
|
93
|
-
});
|
|
94
|
-
it('should handle malformed tokens', ()=>{
|
|
95
|
-
;
|
|
96
|
-
jwt.verify.mockImplementation(()=>{
|
|
97
|
-
const error = new Error('Malformed token');
|
|
98
|
-
error.name = 'JsonWebTokenError';
|
|
99
|
-
throw error;
|
|
100
|
-
});
|
|
101
|
-
expect(()=>verifyMagicLinkToken('malformed-token')).toThrow('Invalid magic link token');
|
|
102
|
-
});
|
|
103
|
-
it('should validate required fields', ()=>{
|
|
104
|
-
const incompletePayload = {
|
|
105
|
-
subscriberId: 'sub-123',
|
|
106
|
-
// Missing email
|
|
107
|
-
type: 'magic-link'
|
|
108
|
-
};
|
|
109
|
-
jwt.verify.mockReturnValue(incompletePayload);
|
|
110
|
-
// The actual implementation doesn't validate fields, just type
|
|
111
|
-
const payload = verifyMagicLinkToken('incomplete-token');
|
|
112
|
-
expect(payload).toEqual(incompletePayload);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
describe('generateSessionToken', ()=>{
|
|
116
|
-
it('should generate session token with correct payload', ()=>{
|
|
117
|
-
const mockToken = 'mock-session-token';
|
|
118
|
-
jwt.sign.mockReturnValue(mockToken);
|
|
119
|
-
const token = generateSessionToken('sub-123', 'test@example.com');
|
|
120
|
-
expect(jwt.sign).toHaveBeenCalledWith({
|
|
121
|
-
subscriberId: 'sub-123',
|
|
122
|
-
email: 'test@example.com',
|
|
123
|
-
type: 'session'
|
|
124
|
-
}, mockSecret, {
|
|
125
|
-
expiresIn: '30d',
|
|
126
|
-
issuer: 'payload-newsletter-plugin'
|
|
127
|
-
});
|
|
128
|
-
expect(token).toBe(mockToken);
|
|
129
|
-
});
|
|
130
|
-
it('should set correct token type', ()=>{
|
|
131
|
-
generateSessionToken('sub-123', 'test@example.com');
|
|
132
|
-
const payload = jwt.sign.mock.calls[0][0];
|
|
133
|
-
expect(payload.type).toBe('session');
|
|
134
|
-
expect(payload.subscriberId).toBe('sub-123');
|
|
135
|
-
});
|
|
136
|
-
it('should set longer expiration for sessions', ()=>{
|
|
137
|
-
generateSessionToken('sub-123', 'test@example.com');
|
|
138
|
-
const options = jwt.sign.mock.calls[0][2];
|
|
139
|
-
expect(options.expiresIn).toBe('30d');
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
describe('verifySessionToken', ()=>{
|
|
143
|
-
it('should verify and return valid session payload', ()=>{
|
|
144
|
-
const mockPayload = {
|
|
145
|
-
subscriberId: 'sub-123',
|
|
146
|
-
email: 'test@example.com',
|
|
147
|
-
type: 'session'
|
|
148
|
-
};
|
|
149
|
-
jwt.verify.mockReturnValue(mockPayload);
|
|
150
|
-
const payload = verifySessionToken('valid-session');
|
|
151
|
-
expect(jwt.verify).toHaveBeenCalledWith('valid-session', mockSecret, {
|
|
152
|
-
issuer: 'payload-newsletter-plugin'
|
|
153
|
-
});
|
|
154
|
-
expect(payload).toEqual(mockPayload);
|
|
155
|
-
});
|
|
156
|
-
it('should reject non-session tokens', ()=>{
|
|
157
|
-
const magicLinkPayload = {
|
|
158
|
-
subscriberId: 'sub-123',
|
|
159
|
-
email: 'test@example.com',
|
|
160
|
-
type: 'magic-link'
|
|
161
|
-
};
|
|
162
|
-
jwt.verify.mockReturnValue(magicLinkPayload);
|
|
163
|
-
expect(()=>verifySessionToken('magic-link-token')).toThrow('Invalid token type');
|
|
164
|
-
});
|
|
165
|
-
it('should handle expired session tokens', ()=>{
|
|
166
|
-
;
|
|
167
|
-
jwt.verify.mockImplementation(()=>{
|
|
168
|
-
const error = new Error('Token expired');
|
|
169
|
-
error.name = 'TokenExpiredError';
|
|
170
|
-
throw error;
|
|
171
|
-
});
|
|
172
|
-
expect(()=>verifySessionToken('expired-token')).toThrow('Session has expired');
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
describe('Environment Configuration', ()=>{
|
|
176
|
-
it('should use fallback secret if JWT_SECRET is not set', ()=>{
|
|
177
|
-
delete process.env.JWT_SECRET;
|
|
178
|
-
delete process.env.PAYLOAD_SECRET;
|
|
179
|
-
// Should not throw, uses development placeholder
|
|
180
|
-
expect(()=>generateMagicLinkToken('sub-123', 'test@example.com', mockConfig)).not.toThrow();
|
|
181
|
-
// Check that it uses the placeholder secret
|
|
182
|
-
expect(jwt.sign).toHaveBeenCalledWith(expect.any(Object), 'INSECURE_DEVELOPMENT_SECRET_PLEASE_SET_JWT_SECRET', expect.any(Object));
|
|
183
|
-
});
|
|
184
|
-
it('should use PAYLOAD_SECRET as fallback', ()=>{
|
|
185
|
-
delete process.env.JWT_SECRET;
|
|
186
|
-
process.env.PAYLOAD_SECRET = 'payload-secret';
|
|
187
|
-
generateMagicLinkToken('sub-123', 'test@example.com', mockConfig);
|
|
188
|
-
expect(jwt.sign).toHaveBeenCalledWith(expect.any(Object), 'payload-secret', expect.any(Object));
|
|
189
|
-
});
|
|
190
|
-
it('should not expose secret in error messages', ()=>{
|
|
191
|
-
process.env.JWT_SECRET = 'super-secret-key-12345';
|
|
192
|
-
jwt.verify.mockImplementation(()=>{
|
|
193
|
-
throw new Error('invalid signature');
|
|
194
|
-
});
|
|
195
|
-
try {
|
|
196
|
-
verifyMagicLinkToken('bad-token');
|
|
197
|
-
} catch (error) {
|
|
198
|
-
expect(error.message).not.toContain('super-secret-key-12345');
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
describe('Token Security', ()=>{
|
|
203
|
-
it('should not allow algorithm switching attacks', ()=>{
|
|
204
|
-
// Ensure tokens are verified with the expected algorithm
|
|
205
|
-
const maliciousToken = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWJzY3JpYmVySWQiOiJzdWItMTIzIn0.';
|
|
206
|
-
jwt.verify.mockImplementation(()=>{
|
|
207
|
-
const error = new Error('invalid algorithm');
|
|
208
|
-
error.name = 'JsonWebTokenError';
|
|
209
|
-
throw error;
|
|
210
|
-
});
|
|
211
|
-
expect(()=>verifyMagicLinkToken(maliciousToken)).toThrow('Invalid magic link token');
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
describe('generateMagicLinkURL', ()=>{
|
|
215
|
-
it('should generate valid magic link URLs', ()=>{
|
|
216
|
-
const token = 'test-token-123';
|
|
217
|
-
const baseURL = 'https://example.com';
|
|
218
|
-
const url = generateMagicLinkURL(token, baseURL, mockConfig);
|
|
219
|
-
expect(url).toBe('https://example.com/newsletter/verify?token=test-token-123');
|
|
220
|
-
});
|
|
221
|
-
it('should use custom path from config', ()=>{
|
|
222
|
-
const customConfig = {
|
|
223
|
-
auth: {
|
|
224
|
-
magicLinkPath: '/auth/magic'
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
const url = generateMagicLinkURL('token-123', 'https://example.com', customConfig);
|
|
228
|
-
expect(url).toBe('https://example.com/auth/magic?token=token-123');
|
|
229
|
-
});
|
|
230
|
-
it('should properly encode token in URL', ()=>{
|
|
231
|
-
const tokenWithSpecialChars = 'token+with/special=chars';
|
|
232
|
-
const url = generateMagicLinkURL(tokenWithSpecialChars, 'https://example.com', mockConfig);
|
|
233
|
-
expect(url).toContain(encodeURIComponent(tokenWithSpecialChars));
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
//# sourceMappingURL=jwt.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../src/__tests__/unit/utils/jwt.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport jwt from 'jsonwebtoken'\nimport {\n generateMagicLinkToken,\n verifyMagicLinkToken,\n generateSessionToken,\n verifySessionToken,\n generateMagicLinkURL,\n} from '../../../utils/jwt'\nimport type { NewsletterPluginConfig } from '../../../types'\n\n// Mock jsonwebtoken\nvi.mock('jsonwebtoken', () => ({\n default: {\n sign: vi.fn(),\n verify: vi.fn(),\n TokenExpiredError: class TokenExpiredError extends Error {\n constructor(message: string, expiredAt: Date) {\n super(message)\n this.name = 'TokenExpiredError'\n }\n },\n JsonWebTokenError: class JsonWebTokenError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'JsonWebTokenError'\n }\n },\n },\n}))\n\ndescribe('JWT Utilities Security', () => {\n const mockSecret = 'test-secret-key'\n const mockConfig: NewsletterPluginConfig = {}\n \n beforeEach(() => {\n process.env.JWT_SECRET = mockSecret\n vi.clearAllMocks()\n })\n\n describe('generateMagicLinkToken', () => {\n it('should generate token with correct payload', () => {\n const mockToken = 'mock-magic-link-token'\n ;(jwt.sign as any).mockReturnValue(mockToken)\n\n const token = generateMagicLinkToken('sub-123', 'test@example.com', mockConfig)\n\n expect(jwt.sign).toHaveBeenCalledWith(\n {\n subscriberId: 'sub-123',\n email: 'test@example.com',\n type: 'magic-link',\n },\n mockSecret,\n {\n expiresIn: '7d',\n issuer: 'payload-newsletter-plugin',\n }\n )\n expect(token).toBe(mockToken)\n })\n\n it('should respect custom token expiration', () => {\n const customConfig: NewsletterPluginConfig = {\n auth: {\n tokenExpiration: '24h',\n },\n }\n \n generateMagicLinkToken('sub-123', 'test@example.com', customConfig)\n\n const options = (jwt.sign as any).mock.calls[0][2]\n expect(options.expiresIn).toBe('24h')\n })\n\n it('should include correct token type', () => {\n generateMagicLinkToken('sub-123', 'test@example.com', mockConfig)\n\n const payload = (jwt.sign as any).mock.calls[0][0]\n expect(payload.type).toBe('magic-link')\n expect(payload).not.toHaveProperty('action') // No action field in actual implementation\n })\n })\n\n describe('verifyMagicLinkToken', () => {\n it('should verify and return valid token payload', () => {\n const mockPayload = {\n subscriberId: 'sub-123',\n email: 'test@example.com',\n type: 'magic-link',\n }\n ;(jwt.verify as any).mockReturnValue(mockPayload)\n\n const payload = verifyMagicLinkToken('valid-token')\n\n expect(jwt.verify).toHaveBeenCalledWith('valid-token', mockSecret, {\n issuer: 'payload-newsletter-plugin',\n })\n expect(payload).toEqual(mockPayload)\n })\n\n it('should reject tokens with wrong type', () => {\n const wrongTypePayload = {\n subscriberId: 'sub-123',\n email: 'test@example.com',\n type: 'session', // Wrong type\n }\n ;(jwt.verify as any).mockReturnValue(wrongTypePayload)\n\n expect(() => verifyMagicLinkToken('wrong-type-token')).toThrow('Invalid token type')\n })\n\n it('should handle expired tokens', () => {\n ;(jwt.verify as any).mockImplementation(() => {\n const error = new Error('Token expired')\n error.name = 'TokenExpiredError'\n throw error\n })\n\n expect(() => verifyMagicLinkToken('expired-token')).toThrow('Magic link has expired')\n })\n\n it('should handle malformed tokens', () => {\n ;(jwt.verify as any).mockImplementation(() => {\n const error = new Error('Malformed token')\n error.name = 'JsonWebTokenError'\n throw error\n })\n\n expect(() => verifyMagicLinkToken('malformed-token')).toThrow('Invalid magic link token')\n })\n\n it('should validate required fields', () => {\n const incompletePayload = {\n subscriberId: 'sub-123',\n // Missing email\n type: 'magic-link',\n }\n ;(jwt.verify as any).mockReturnValue(incompletePayload)\n\n // The actual implementation doesn't validate fields, just type\n const payload = verifyMagicLinkToken('incomplete-token')\n expect(payload).toEqual(incompletePayload)\n })\n })\n\n describe('generateSessionToken', () => {\n it('should generate session token with correct payload', () => {\n const mockToken = 'mock-session-token'\n ;(jwt.sign as any).mockReturnValue(mockToken)\n\n const token = generateSessionToken('sub-123', 'test@example.com')\n\n expect(jwt.sign).toHaveBeenCalledWith(\n {\n subscriberId: 'sub-123',\n email: 'test@example.com',\n type: 'session',\n },\n mockSecret,\n {\n expiresIn: '30d',\n issuer: 'payload-newsletter-plugin',\n }\n )\n expect(token).toBe(mockToken)\n })\n\n it('should set correct token type', () => {\n generateSessionToken('sub-123', 'test@example.com')\n\n const payload = (jwt.sign as any).mock.calls[0][0]\n expect(payload.type).toBe('session')\n expect(payload.subscriberId).toBe('sub-123')\n })\n\n it('should set longer expiration for sessions', () => {\n generateSessionToken('sub-123', 'test@example.com')\n\n const options = (jwt.sign as any).mock.calls[0][2]\n expect(options.expiresIn).toBe('30d')\n })\n })\n\n describe('verifySessionToken', () => {\n it('should verify and return valid session payload', () => {\n const mockPayload = {\n subscriberId: 'sub-123',\n email: 'test@example.com',\n type: 'session',\n }\n ;(jwt.verify as any).mockReturnValue(mockPayload)\n\n const payload = verifySessionToken('valid-session')\n\n expect(jwt.verify).toHaveBeenCalledWith('valid-session', mockSecret, {\n issuer: 'payload-newsletter-plugin',\n })\n expect(payload).toEqual(mockPayload)\n })\n\n it('should reject non-session tokens', () => {\n const magicLinkPayload = {\n subscriberId: 'sub-123',\n email: 'test@example.com',\n type: 'magic-link', // Wrong type for session\n }\n ;(jwt.verify as any).mockReturnValue(magicLinkPayload)\n\n expect(() => verifySessionToken('magic-link-token')).toThrow('Invalid token type')\n })\n\n it('should handle expired session tokens', () => {\n ;(jwt.verify as any).mockImplementation(() => {\n const error = new Error('Token expired')\n error.name = 'TokenExpiredError'\n throw error\n })\n\n expect(() => verifySessionToken('expired-token')).toThrow('Session has expired')\n })\n })\n\n describe('Environment Configuration', () => {\n it('should use fallback secret if JWT_SECRET is not set', () => {\n delete process.env.JWT_SECRET\n delete process.env.PAYLOAD_SECRET\n\n // Should not throw, uses development placeholder\n expect(() => generateMagicLinkToken('sub-123', 'test@example.com', mockConfig)).not.toThrow()\n \n // Check that it uses the placeholder secret\n expect(jwt.sign).toHaveBeenCalledWith(\n expect.any(Object),\n 'INSECURE_DEVELOPMENT_SECRET_PLEASE_SET_JWT_SECRET',\n expect.any(Object)\n )\n })\n\n it('should use PAYLOAD_SECRET as fallback', () => {\n delete process.env.JWT_SECRET\n process.env.PAYLOAD_SECRET = 'payload-secret'\n\n generateMagicLinkToken('sub-123', 'test@example.com', mockConfig)\n\n expect(jwt.sign).toHaveBeenCalledWith(\n expect.any(Object),\n 'payload-secret',\n expect.any(Object)\n )\n })\n\n it('should not expose secret in error messages', () => {\n process.env.JWT_SECRET = 'super-secret-key-12345'\n ;(jwt.verify as any).mockImplementation(() => {\n throw new Error('invalid signature')\n })\n\n try {\n verifyMagicLinkToken('bad-token')\n } catch (error: any) {\n expect(error.message).not.toContain('super-secret-key-12345')\n }\n })\n })\n\n describe('Token Security', () => {\n it('should not allow algorithm switching attacks', () => {\n // Ensure tokens are verified with the expected algorithm\n const maliciousToken = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWJzY3JpYmVySWQiOiJzdWItMTIzIn0.'\n \n ;(jwt.verify as any).mockImplementation(() => {\n const error = new Error('invalid algorithm')\n error.name = 'JsonWebTokenError'\n throw error\n })\n\n expect(() => verifyMagicLinkToken(maliciousToken)).toThrow('Invalid magic link token')\n })\n })\n\n describe('generateMagicLinkURL', () => {\n it('should generate valid magic link URLs', () => {\n const token = 'test-token-123'\n const baseURL = 'https://example.com'\n \n const url = generateMagicLinkURL(token, baseURL, mockConfig)\n \n expect(url).toBe('https://example.com/newsletter/verify?token=test-token-123')\n })\n\n it('should use custom path from config', () => {\n const customConfig: NewsletterPluginConfig = {\n auth: {\n magicLinkPath: '/auth/magic',\n },\n }\n \n const url = generateMagicLinkURL('token-123', 'https://example.com', customConfig)\n \n expect(url).toBe('https://example.com/auth/magic?token=token-123')\n })\n\n it('should properly encode token in URL', () => {\n const tokenWithSpecialChars = 'token+with/special=chars'\n \n const url = generateMagicLinkURL(tokenWithSpecialChars, 'https://example.com', mockConfig)\n \n expect(url).toContain(encodeURIComponent(tokenWithSpecialChars))\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","jwt","generateMagicLinkToken","verifyMagicLinkToken","generateSessionToken","verifySessionToken","generateMagicLinkURL","mock","default","sign","fn","verify","TokenExpiredError","Error","message","expiredAt","name","JsonWebTokenError","mockSecret","mockConfig","process","env","JWT_SECRET","clearAllMocks","mockToken","mockReturnValue","token","toHaveBeenCalledWith","subscriberId","email","type","expiresIn","issuer","toBe","customConfig","auth","tokenExpiration","options","calls","payload","not","toHaveProperty","mockPayload","toEqual","wrongTypePayload","toThrow","mockImplementation","error","incompletePayload","magicLinkPayload","PAYLOAD_SECRET","any","Object","toContain","maliciousToken","baseURL","url","magicLinkPath","tokenWithSpecialChars","encodeURIComponent"],"mappings":"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"}
|