@bernierllc/email-sendgrid-plugin 1.4.5 → 1.7.0
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/dist/api/errors.js +5 -0
- package/dist/api/rate-limiter.js +3 -1
- package/dist/api/sendgrid-client.js +4 -1
- package/dist/converters/template-converter.js +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/plugin/sender-manager.js +6 -1
- package/dist/plugin/sendgrid-plugin.js +15 -4
- package/dist/plugin/sync-manager.js +4 -0
- package/dist/plugin/template-manager.js +5 -0
- package/dist/safety/ownership-validator.js +3 -1
- package/dist/safety/prefix-enforcer.js +1 -0
- package/package.json +8 -4
- package/.eslintrc.js +0 -27
- package/CHANGELOG.md +0 -117
- package/__tests__/api/rate-limiter.test.ts +0 -125
- package/__tests__/converters/attachment-header-mapping.test.ts +0 -337
- package/__tests__/converters/sender-converter.test.ts +0 -148
- package/__tests__/converters/template-converter.test.ts +0 -230
- package/__tests__/plugin/sender-manager.test.ts +0 -135
- package/__tests__/plugin/sendgrid-plugin.test.ts +0 -195
- package/__tests__/plugin/template-manager.test.ts +0 -110
- package/__tests__/safety/ownership-validator.test.ts +0 -130
- package/__tests__/safety/prefix-enforcer.test.ts +0 -136
- package/jest.config.js +0 -26
- package/src/api/errors.ts +0 -49
- package/src/api/rate-limiter.ts +0 -79
- package/src/api/sendgrid-client.ts +0 -86
- package/src/converters/sender-converter.ts +0 -138
- package/src/converters/template-converter.ts +0 -97
- package/src/index.ts +0 -61
- package/src/plugin/sender-manager.ts +0 -166
- package/src/plugin/sendgrid-plugin.ts +0 -316
- package/src/plugin/sync-manager.ts +0 -115
- package/src/plugin/template-manager.ts +0 -240
- package/src/safety/ownership-validator.ts +0 -55
- package/src/safety/prefix-enforcer.ts +0 -37
- package/src/types/config-types.ts +0 -34
- package/src/types/plugin-types.ts +0 -97
- package/src/types/sendgrid-types.ts +0 -98
- package/tsconfig.json +0 -18
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { TemplateConverter } from '../../src/converters/template-converter';
|
|
10
|
-
import { PrefixEnforcer } from '../../src/safety/prefix-enforcer';
|
|
11
|
-
import { SendGridTemplateData, SendGridTemplateVersion } from '../../src/types/sendgrid-types';
|
|
12
|
-
|
|
13
|
-
describe('TemplateConverter', () => {
|
|
14
|
-
let converter: TemplateConverter;
|
|
15
|
-
let prefixEnforcer: PrefixEnforcer;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
prefixEnforcer = new PrefixEnforcer('MyApp - ');
|
|
19
|
-
converter = new TemplateConverter(prefixEnforcer);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe('convertToProviderTemplate', () => {
|
|
23
|
-
it('should convert SendGrid template to provider template', () => {
|
|
24
|
-
const activeVersion: SendGridTemplateVersion = {
|
|
25
|
-
id: 'ver-123',
|
|
26
|
-
template_id: 'template-123',
|
|
27
|
-
active: 1,
|
|
28
|
-
name: 'v1.0.0',
|
|
29
|
-
html_content: '<html>Hello {{name}}</html>',
|
|
30
|
-
plain_content: 'Hello {{name}}',
|
|
31
|
-
subject: 'Welcome {{name}}',
|
|
32
|
-
updated_at: '2024-01-01T00:00:00Z',
|
|
33
|
-
editor: 'code'
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const sendgridData: SendGridTemplateData = {
|
|
37
|
-
id: 'template-123',
|
|
38
|
-
name: 'MyApp - Welcome Email',
|
|
39
|
-
generation: 'dynamic',
|
|
40
|
-
updated_at: '2024-01-01T00:00:00Z',
|
|
41
|
-
versions: [activeVersion]
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const result = converter.convertToProviderTemplate(sendgridData, activeVersion);
|
|
45
|
-
|
|
46
|
-
expect(result).toEqual({
|
|
47
|
-
id: 'template-123',
|
|
48
|
-
name: 'Welcome Email',
|
|
49
|
-
subject: 'Welcome {{name}}',
|
|
50
|
-
htmlContent: '<html>Hello {{name}}</html>',
|
|
51
|
-
textContent: 'Hello {{name}}',
|
|
52
|
-
version: 'v1.0.0',
|
|
53
|
-
active: true,
|
|
54
|
-
createdAt: expect.any(Date),
|
|
55
|
-
updatedAt: expect.any(Date)
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should handle template without active version', () => {
|
|
60
|
-
const sendgridData: SendGridTemplateData = {
|
|
61
|
-
id: 'template-456',
|
|
62
|
-
name: 'MyApp - Test',
|
|
63
|
-
generation: 'dynamic',
|
|
64
|
-
updated_at: '2024-01-01T00:00:00Z',
|
|
65
|
-
versions: []
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const result = converter.convertToProviderTemplate(sendgridData);
|
|
69
|
-
|
|
70
|
-
expect(result.subject).toBe('');
|
|
71
|
-
expect(result.htmlContent).toBe('');
|
|
72
|
-
expect(result.version).toBe('1.0.0');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should find active version automatically', () => {
|
|
76
|
-
const versions: SendGridTemplateVersion[] = [
|
|
77
|
-
{
|
|
78
|
-
id: 'ver-1',
|
|
79
|
-
template_id: 'template-123',
|
|
80
|
-
active: 0,
|
|
81
|
-
name: 'v1.0.0',
|
|
82
|
-
html_content: '<html>Old</html>',
|
|
83
|
-
plain_content: 'Old',
|
|
84
|
-
subject: 'Old',
|
|
85
|
-
updated_at: '2024-01-01T00:00:00Z',
|
|
86
|
-
editor: 'code'
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
id: 'ver-2',
|
|
90
|
-
template_id: 'template-123',
|
|
91
|
-
active: 1,
|
|
92
|
-
name: 'v2.0.0',
|
|
93
|
-
html_content: '<html>New</html>',
|
|
94
|
-
plain_content: 'New',
|
|
95
|
-
subject: 'New',
|
|
96
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
97
|
-
editor: 'code'
|
|
98
|
-
}
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
const sendgridData: SendGridTemplateData = {
|
|
102
|
-
id: 'template-123',
|
|
103
|
-
name: 'MyApp - Test',
|
|
104
|
-
generation: 'dynamic',
|
|
105
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
106
|
-
versions
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const result = converter.convertToProviderTemplate(sendgridData);
|
|
110
|
-
|
|
111
|
-
expect(result.version).toBe('v2.0.0');
|
|
112
|
-
expect(result.subject).toBe('New');
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('convertToLocalTemplate', () => {
|
|
117
|
-
it('should convert SendGrid template to local template', () => {
|
|
118
|
-
const activeVersion: SendGridTemplateVersion = {
|
|
119
|
-
id: 'ver-123',
|
|
120
|
-
template_id: 'template-123',
|
|
121
|
-
active: 1,
|
|
122
|
-
name: 'v1.0.0',
|
|
123
|
-
html_content: '<html>Content</html>',
|
|
124
|
-
plain_content: 'Content',
|
|
125
|
-
subject: 'Subject',
|
|
126
|
-
updated_at: '2024-01-01T00:00:00Z',
|
|
127
|
-
editor: 'code'
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const sendgridData: SendGridTemplateData = {
|
|
131
|
-
id: 'template-123',
|
|
132
|
-
name: 'MyApp - Welcome',
|
|
133
|
-
generation: 'dynamic',
|
|
134
|
-
updated_at: '2024-01-01T00:00:00Z',
|
|
135
|
-
versions: [activeVersion]
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const result = converter.convertToLocalTemplate(sendgridData, 'local-123', activeVersion);
|
|
139
|
-
|
|
140
|
-
expect(result).toEqual({
|
|
141
|
-
id: 'local-123',
|
|
142
|
-
name: 'Welcome',
|
|
143
|
-
subject: 'Subject',
|
|
144
|
-
htmlContent: '<html>Content</html>',
|
|
145
|
-
textContent: 'Content',
|
|
146
|
-
category: 'synced',
|
|
147
|
-
tags: ['sendgrid', 'synced'],
|
|
148
|
-
version: 'v1.0.0',
|
|
149
|
-
isActive: true,
|
|
150
|
-
requiredVariables: [],
|
|
151
|
-
providerTemplateIds: {
|
|
152
|
-
sendgrid: 'template-123'
|
|
153
|
-
},
|
|
154
|
-
lastSyncedAt: {
|
|
155
|
-
sendgrid: expect.any(Date)
|
|
156
|
-
},
|
|
157
|
-
syncStatus: {
|
|
158
|
-
sendgrid: 'success'
|
|
159
|
-
},
|
|
160
|
-
createdAt: expect.any(Date),
|
|
161
|
-
updatedAt: expect.any(Date),
|
|
162
|
-
createdBy: 'sendgrid-sync',
|
|
163
|
-
lastModifiedBy: 'sendgrid-sync'
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('extractVariables', () => {
|
|
169
|
-
it('should extract Handlebars variables', () => {
|
|
170
|
-
const content = 'Hello {{name}}, your order {{orderId}} is ready!';
|
|
171
|
-
|
|
172
|
-
const result = converter.extractVariables(content);
|
|
173
|
-
|
|
174
|
-
expect(result).toEqual(expect.arrayContaining(['name', 'orderId']));
|
|
175
|
-
expect(result).toHaveLength(2);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should handle variables with whitespace', () => {
|
|
179
|
-
const content = 'Hello {{ firstName }}, welcome to {{ siteName }}!';
|
|
180
|
-
|
|
181
|
-
const result = converter.extractVariables(content);
|
|
182
|
-
|
|
183
|
-
expect(result).toEqual(expect.arrayContaining(['firstName', 'siteName']));
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('should filter out Handlebars helpers', () => {
|
|
187
|
-
const content = '{{#if condition}}{{variable}}{{/if}}';
|
|
188
|
-
|
|
189
|
-
const result = converter.extractVariables(content);
|
|
190
|
-
|
|
191
|
-
expect(result).toEqual(['variable']);
|
|
192
|
-
expect(result).not.toContain('#if');
|
|
193
|
-
expect(result).not.toContain('/if');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should deduplicate variables', () => {
|
|
197
|
-
const content = 'Hello {{name}}, {{name}} is a great name!';
|
|
198
|
-
|
|
199
|
-
const result = converter.extractVariables(content);
|
|
200
|
-
|
|
201
|
-
expect(result).toEqual(['name']);
|
|
202
|
-
expect(result).toHaveLength(1);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should return empty array for content without variables', () => {
|
|
206
|
-
const content = 'This is plain text with no variables';
|
|
207
|
-
|
|
208
|
-
const result = converter.extractVariables(content);
|
|
209
|
-
|
|
210
|
-
expect(result).toEqual([]);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should handle complex HTML with variables', () => {
|
|
214
|
-
const content = `
|
|
215
|
-
<html>
|
|
216
|
-
<body>
|
|
217
|
-
<h1>Hello {{userName}}</h1>
|
|
218
|
-
<p>Your account {{accountId}} is active</p>
|
|
219
|
-
<a href="{{confirmationUrl}}">Click here</a>
|
|
220
|
-
</body>
|
|
221
|
-
</html>
|
|
222
|
-
`;
|
|
223
|
-
|
|
224
|
-
const result = converter.extractVariables(content);
|
|
225
|
-
|
|
226
|
-
expect(result).toEqual(expect.arrayContaining(['userName', 'accountId', 'confirmationUrl']));
|
|
227
|
-
expect(result).toHaveLength(3);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
});
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { SenderManager } from '../../src/plugin/sender-manager';
|
|
10
|
-
import { SendGridClient } from '../../src/api/sendgrid-client';
|
|
11
|
-
import { SenderConverter } from '../../src/converters/sender-converter';
|
|
12
|
-
|
|
13
|
-
describe('SenderManager', () => {
|
|
14
|
-
let manager: SenderManager;
|
|
15
|
-
let mockClient: jest.Mocked<SendGridClient>;
|
|
16
|
-
let converter: SenderConverter;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
mockClient = {
|
|
20
|
-
request: jest.fn(),
|
|
21
|
-
isSuccessResponse: jest.fn(),
|
|
22
|
-
parseResponseBody: jest.fn()
|
|
23
|
-
} as any;
|
|
24
|
-
|
|
25
|
-
converter = new SenderConverter();
|
|
26
|
-
manager = new SenderManager(mockClient, converter);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('getVerifiedSenders', () => {
|
|
30
|
-
it('should fetch and cache verified senders', async () => {
|
|
31
|
-
const mockSenders = {
|
|
32
|
-
results: [
|
|
33
|
-
{
|
|
34
|
-
id: 123,
|
|
35
|
-
nickname: 'Test',
|
|
36
|
-
from_email: 'test@example.com',
|
|
37
|
-
from_name: 'Test',
|
|
38
|
-
reply_to: 'test@example.com',
|
|
39
|
-
reply_to_name: 'Test',
|
|
40
|
-
address: '',
|
|
41
|
-
address2: '',
|
|
42
|
-
city: '',
|
|
43
|
-
state: '',
|
|
44
|
-
zip: '',
|
|
45
|
-
country: 'US',
|
|
46
|
-
verified: true,
|
|
47
|
-
locked: false
|
|
48
|
-
}
|
|
49
|
-
]
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
mockClient.request.mockResolvedValueOnce({
|
|
53
|
-
statusCode: 200,
|
|
54
|
-
headers: {},
|
|
55
|
-
body: JSON.stringify(mockSenders)
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
mockClient.isSuccessResponse.mockReturnValue(true);
|
|
59
|
-
mockClient.parseResponseBody.mockReturnValueOnce(mockSenders);
|
|
60
|
-
|
|
61
|
-
const result = await manager.getVerifiedSenders();
|
|
62
|
-
|
|
63
|
-
expect(result).toHaveLength(1);
|
|
64
|
-
expect(result[0].email).toBe('test@example.com');
|
|
65
|
-
expect(mockClient.request).toHaveBeenCalledTimes(1);
|
|
66
|
-
|
|
67
|
-
// Second call should use cache
|
|
68
|
-
const result2 = await manager.getVerifiedSenders();
|
|
69
|
-
expect(result2).toEqual(result);
|
|
70
|
-
expect(mockClient.request).toHaveBeenCalledTimes(1); // Still 1, used cache
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('validateSender', () => {
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
const mockSenders = {
|
|
77
|
-
results: [
|
|
78
|
-
{
|
|
79
|
-
id: 123,
|
|
80
|
-
nickname: 'Test',
|
|
81
|
-
from_email: 'verified@example.com',
|
|
82
|
-
from_name: 'Test',
|
|
83
|
-
reply_to: 'verified@example.com',
|
|
84
|
-
reply_to_name: 'Test',
|
|
85
|
-
address: '',
|
|
86
|
-
address2: '',
|
|
87
|
-
city: '',
|
|
88
|
-
state: '',
|
|
89
|
-
zip: '',
|
|
90
|
-
country: 'US',
|
|
91
|
-
verified: true,
|
|
92
|
-
locked: false
|
|
93
|
-
}
|
|
94
|
-
]
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
mockClient.request.mockResolvedValueOnce({
|
|
98
|
-
statusCode: 200,
|
|
99
|
-
headers: {},
|
|
100
|
-
body: JSON.stringify(mockSenders)
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
mockClient.isSuccessResponse.mockReturnValue(true);
|
|
104
|
-
mockClient.parseResponseBody.mockReturnValueOnce(mockSenders);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should validate exact match sender', async () => {
|
|
108
|
-
const result = await manager.validateSender('verified@example.com');
|
|
109
|
-
|
|
110
|
-
expect(result.isValid).toBe(true);
|
|
111
|
-
expect(result.verifiedSender).toBeDefined();
|
|
112
|
-
expect(result.errors).toEqual([]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should validate sender with domain match', async () => {
|
|
116
|
-
const result = await manager.validateSender('unverified@example.com');
|
|
117
|
-
|
|
118
|
-
expect(result.isValid).toBe(true); // Domain match is valid
|
|
119
|
-
expect(result.domainMatches.length).toBeGreaterThan(0);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should reject sender with no domain match', async () => {
|
|
123
|
-
const result = await manager.validateSender('test@different.com');
|
|
124
|
-
|
|
125
|
-
expect(result.isValid).toBe(false);
|
|
126
|
-
expect(result.errors.length).toBeGreaterThan(0);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('clearCache', () => {
|
|
131
|
-
it('should clear the senders cache', () => {
|
|
132
|
-
expect(() => manager.clearCache()).not.toThrow();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
});
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { SendGridTemplatePlugin } from '../../src/plugin/sendgrid-plugin';
|
|
10
|
-
import { SendGridPluginConfig } from '../../src/types/config-types';
|
|
11
|
-
|
|
12
|
-
describe('SendGridTemplatePlugin', () => {
|
|
13
|
-
let plugin: SendGridTemplatePlugin;
|
|
14
|
-
let config: SendGridPluginConfig;
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
config = {
|
|
18
|
-
apiKey: 'test-api-key',
|
|
19
|
-
templatePrefix: 'Test - ',
|
|
20
|
-
safetyMode: 'strict' as const,
|
|
21
|
-
rateLimits: {
|
|
22
|
-
templatesPerMinute: 60,
|
|
23
|
-
apiCallsPerSecond: 10
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
plugin = new SendGridTemplatePlugin(config);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('constructor', () => {
|
|
31
|
-
it('should create plugin with configuration', () => {
|
|
32
|
-
expect(plugin).toBeDefined();
|
|
33
|
-
expect(plugin.name).toBe('sendgrid-templates');
|
|
34
|
-
expect(plugin.version).toBe('1.0.0');
|
|
35
|
-
expect(plugin.providerId).toBe('sendgrid');
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('initialize', () => {
|
|
40
|
-
it('should initialize plugin successfully', async () => {
|
|
41
|
-
await expect(plugin.initialize({})).resolves.toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should be idempotent', async () => {
|
|
45
|
-
await plugin.initialize({});
|
|
46
|
-
await expect(plugin.initialize({})).resolves.toBeUndefined();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('destroy', () => {
|
|
51
|
-
it('should destroy plugin successfully', async () => {
|
|
52
|
-
await plugin.initialize({});
|
|
53
|
-
await expect(plugin.destroy()).resolves.toBeUndefined();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should handle destroy without initialization', async () => {
|
|
57
|
-
await expect(plugin.destroy()).resolves.toBeUndefined();
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('getCapabilities', () => {
|
|
62
|
-
it('should return all supported capabilities', () => {
|
|
63
|
-
const capabilities = plugin.getCapabilities();
|
|
64
|
-
|
|
65
|
-
expect(capabilities).toHaveLength(4);
|
|
66
|
-
expect(capabilities.map(c => c.feature)).toEqual([
|
|
67
|
-
'template-management',
|
|
68
|
-
'sender-verification',
|
|
69
|
-
'template-analytics',
|
|
70
|
-
'batch-operations'
|
|
71
|
-
]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should include correct methods for each capability', () => {
|
|
75
|
-
const capabilities = plugin.getCapabilities();
|
|
76
|
-
|
|
77
|
-
const templateManagement = capabilities.find(c => c.feature === 'template-management');
|
|
78
|
-
expect(templateManagement?.methods).toContain('createTemplate');
|
|
79
|
-
expect(templateManagement?.methods).toContain('updateTemplate');
|
|
80
|
-
expect(templateManagement?.methods).toContain('deleteTemplate');
|
|
81
|
-
|
|
82
|
-
const senderVerification = capabilities.find(c => c.feature === 'sender-verification');
|
|
83
|
-
expect(senderVerification?.methods).toContain('getVerifiedSenders');
|
|
84
|
-
expect(senderVerification?.methods).toContain('validateSender');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should include configuration for each capability', () => {
|
|
88
|
-
const capabilities = plugin.getCapabilities();
|
|
89
|
-
|
|
90
|
-
capabilities.forEach(capability => {
|
|
91
|
-
expect(capability.config).toBeDefined();
|
|
92
|
-
expect(typeof capability.config).toBe('object');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('supportsFeature', () => {
|
|
98
|
-
it('should return true for supported features', () => {
|
|
99
|
-
expect(plugin.supportsFeature('template-management')).toBe(true);
|
|
100
|
-
expect(plugin.supportsFeature('sender-verification')).toBe(true);
|
|
101
|
-
expect(plugin.supportsFeature('template-analytics')).toBe(true);
|
|
102
|
-
expect(plugin.supportsFeature('batch-operations')).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should return false for unsupported features', () => {
|
|
106
|
-
expect(plugin.supportsFeature('unknown-feature')).toBe(false);
|
|
107
|
-
expect(plugin.supportsFeature('custom-domains')).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('getFeatureConfig', () => {
|
|
112
|
-
it('should return config for supported features', () => {
|
|
113
|
-
const config = plugin.getFeatureConfig('template-management');
|
|
114
|
-
|
|
115
|
-
expect(config).toBeDefined();
|
|
116
|
-
expect(config?.supportsVersioning).toBe(true);
|
|
117
|
-
expect(config?.supportsActivation).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should return null for unsupported features', () => {
|
|
121
|
-
const config = plugin.getFeatureConfig('unknown-feature');
|
|
122
|
-
|
|
123
|
-
expect(config).toBeNull();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('validateRoute', () => {
|
|
128
|
-
it('should validate route successfully', () => {
|
|
129
|
-
const result = plugin.validateRoute({});
|
|
130
|
-
|
|
131
|
-
expect(result.isValid).toBe(true);
|
|
132
|
-
expect(result.errors).toEqual([]);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('template operations', () => {
|
|
137
|
-
it('should throw error when not initialized', async () => {
|
|
138
|
-
await expect(plugin.createTemplate({} as any)).rejects.toThrow('not initialized');
|
|
139
|
-
await expect(plugin.getTemplate('123')).rejects.toThrow('not initialized');
|
|
140
|
-
await expect(plugin.listTemplates()).rejects.toThrow('not initialized');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('sender operations', () => {
|
|
145
|
-
it('should throw error when not initialized', async () => {
|
|
146
|
-
await expect(plugin.getVerifiedSenders()).rejects.toThrow('not initialized');
|
|
147
|
-
await expect(plugin.validateSender('test@example.com')).rejects.toThrow('not initialized');
|
|
148
|
-
await expect(plugin.selectBestSender('test@example.com')).rejects.toThrow('not initialized');
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe('sync operations', () => {
|
|
153
|
-
it('should throw error when not initialized', async () => {
|
|
154
|
-
await expect(plugin.syncToProvider({} as any)).rejects.toThrow('not initialized');
|
|
155
|
-
await expect(plugin.syncFromProvider('123')).rejects.toThrow('not initialized');
|
|
156
|
-
await expect(plugin.batchSyncTemplates([])).rejects.toThrow('not initialized');
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('configuration variations', () => {
|
|
161
|
-
it('should work with permissive safety mode', async () => {
|
|
162
|
-
const permissiveConfig: SendGridPluginConfig = {
|
|
163
|
-
...config,
|
|
164
|
-
safetyMode: 'permissive'
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const permissivePlugin = new SendGridTemplatePlugin(permissiveConfig);
|
|
168
|
-
await expect(permissivePlugin.initialize({})).resolves.toBeUndefined();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('should work without rate limits', async () => {
|
|
172
|
-
const noLimitsConfig: SendGridPluginConfig = {
|
|
173
|
-
apiKey: 'test-key',
|
|
174
|
-
templatePrefix: 'Test - ',
|
|
175
|
-
safetyMode: 'strict'
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const noLimitsPlugin = new SendGridTemplatePlugin(noLimitsConfig);
|
|
179
|
-
await expect(noLimitsPlugin.initialize({})).resolves.toBeUndefined();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should work with webhook config', async () => {
|
|
183
|
-
const webhookConfig: SendGridPluginConfig = {
|
|
184
|
-
...config,
|
|
185
|
-
webhookConfig: {
|
|
186
|
-
secret: 'webhook-secret',
|
|
187
|
-
eventTypes: ['delivered', 'bounced', 'opened']
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const webhookPlugin = new SendGridTemplatePlugin(webhookConfig);
|
|
192
|
-
await expect(webhookPlugin.initialize({})).resolves.toBeUndefined();
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { TemplateManager } from '../../src/plugin/template-manager';
|
|
10
|
-
import { SendGridClient } from '../../src/api/sendgrid-client';
|
|
11
|
-
import { OwnershipValidator } from '../../src/safety/ownership-validator';
|
|
12
|
-
import { PrefixEnforcer } from '../../src/safety/prefix-enforcer';
|
|
13
|
-
import { TemplateConverter } from '../../src/converters/template-converter';
|
|
14
|
-
import { Template } from '../../src/types/plugin-types';
|
|
15
|
-
|
|
16
|
-
describe('TemplateManager', () => {
|
|
17
|
-
let manager: TemplateManager;
|
|
18
|
-
let mockClient: jest.Mocked<SendGridClient>;
|
|
19
|
-
let ownershipValidator: OwnershipValidator;
|
|
20
|
-
let prefixEnforcer: PrefixEnforcer;
|
|
21
|
-
let converter: TemplateConverter;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
mockClient = {
|
|
25
|
-
request: jest.fn(),
|
|
26
|
-
isSuccessResponse: jest.fn(),
|
|
27
|
-
parseResponseBody: jest.fn()
|
|
28
|
-
} as any;
|
|
29
|
-
|
|
30
|
-
ownershipValidator = new OwnershipValidator('strict');
|
|
31
|
-
prefixEnforcer = new PrefixEnforcer('Test - ');
|
|
32
|
-
converter = new TemplateConverter(prefixEnforcer);
|
|
33
|
-
|
|
34
|
-
manager = new TemplateManager(
|
|
35
|
-
mockClient,
|
|
36
|
-
ownershipValidator,
|
|
37
|
-
prefixEnforcer,
|
|
38
|
-
converter
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('createTemplate', () => {
|
|
43
|
-
it('should create template successfully', async () => {
|
|
44
|
-
const template: Template = {
|
|
45
|
-
id: 'local-123',
|
|
46
|
-
name: 'Welcome',
|
|
47
|
-
subject: 'Welcome!',
|
|
48
|
-
htmlContent: '<html>Test</html>',
|
|
49
|
-
category: 'transactional',
|
|
50
|
-
tags: [],
|
|
51
|
-
version: '1.0.0',
|
|
52
|
-
isActive: true,
|
|
53
|
-
requiredVariables: [],
|
|
54
|
-
createdAt: new Date(),
|
|
55
|
-
updatedAt: new Date(),
|
|
56
|
-
createdBy: 'test',
|
|
57
|
-
lastModifiedBy: 'test'
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
mockClient.request.mockResolvedValueOnce({
|
|
61
|
-
statusCode: 201,
|
|
62
|
-
headers: {},
|
|
63
|
-
body: JSON.stringify({ id: 'sendgrid-123', name: 'Test - Welcome', generation: 'dynamic', updated_at: new Date().toISOString() })
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
mockClient.isSuccessResponse.mockReturnValue(true);
|
|
67
|
-
mockClient.parseResponseBody.mockReturnValueOnce({ id: 'sendgrid-123', name: 'Test - Welcome', generation: 'dynamic', updated_at: new Date().toISOString() });
|
|
68
|
-
|
|
69
|
-
mockClient.request.mockResolvedValueOnce({
|
|
70
|
-
statusCode: 201,
|
|
71
|
-
headers: {},
|
|
72
|
-
body: JSON.stringify({ id: 'version-123' })
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
mockClient.isSuccessResponse.mockReturnValue(true);
|
|
76
|
-
mockClient.parseResponseBody.mockReturnValueOnce({ id: 'version-123' });
|
|
77
|
-
|
|
78
|
-
const result = await manager.createTemplate(template);
|
|
79
|
-
|
|
80
|
-
expect(result).toBe('sendgrid-123');
|
|
81
|
-
expect(mockClient.request).toHaveBeenCalledTimes(2);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('listTemplates', () => {
|
|
86
|
-
it('should list and register templates', async () => {
|
|
87
|
-
mockClient.request.mockResolvedValueOnce({
|
|
88
|
-
statusCode: 200,
|
|
89
|
-
headers: {},
|
|
90
|
-
body: JSON.stringify({
|
|
91
|
-
result: [
|
|
92
|
-
{ id: 'template-1', name: 'Test - Template 1', generation: 'dynamic', updated_at: new Date().toISOString(), versions: [] }
|
|
93
|
-
]
|
|
94
|
-
})
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
mockClient.isSuccessResponse.mockReturnValue(true);
|
|
98
|
-
mockClient.parseResponseBody.mockReturnValueOnce({
|
|
99
|
-
result: [
|
|
100
|
-
{ id: 'template-1', name: 'Test - Template 1', generation: 'dynamic', updated_at: new Date().toISOString(), versions: [] }
|
|
101
|
-
]
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const result = await manager.listTemplates();
|
|
105
|
-
|
|
106
|
-
expect(result).toHaveLength(1);
|
|
107
|
-
expect(ownershipValidator.isOwnedTemplate('template-1')).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|