@capillarytech/creatives-library 8.0.208 → 8.0.210
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/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/package.json +16 -2
- package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
- package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +403 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
- package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
- package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
- package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
- package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
- package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
- package/v2Components/HtmlEditor/constants.js +241 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
- package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
- package/v2Components/HtmlEditor/index.js +29 -0
- package/v2Components/HtmlEditor/index.lazy.js +114 -0
- package/v2Components/HtmlEditor/messages.js +389 -0
- package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
- package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
- package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
- package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
- package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
- package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
- package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
- package/v2Containers/EmailWrapper/index.js +8 -1
- package/v2Containers/Rcs/index.js +2 -0
- package/v2Containers/Templates/constants.js +8 -0
- package/v2Containers/Templates/index.js +56 -28
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
- package/v2Containers/Whatsapp/index.js +1 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contentSanitizer Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for HTML content sanitization and security validation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
sanitizeHTML,
|
|
9
|
+
prepareForProduction,
|
|
10
|
+
isContentSafe,
|
|
11
|
+
findUnsafeContent
|
|
12
|
+
} from '../contentSanitizer';
|
|
13
|
+
|
|
14
|
+
// Mock DOMPurify
|
|
15
|
+
jest.mock('dompurify', () => {
|
|
16
|
+
// Global hooks array for legacy API compatibility
|
|
17
|
+
let globalHooks = [];
|
|
18
|
+
|
|
19
|
+
const createMockSanitize = (hooksArray) => (html, config) => {
|
|
20
|
+
if (!html || typeof html !== 'string') {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Simulate DOMPurify behavior
|
|
25
|
+
let sanitized = html;
|
|
26
|
+
|
|
27
|
+
// Check for and remove script tags
|
|
28
|
+
if (/<script/gi.test(html)) {
|
|
29
|
+
sanitized = sanitized.replace(/<script[^>]*>.*?<\/script>/gi, '');
|
|
30
|
+
hooksArray.forEach(hook => {
|
|
31
|
+
if (hook.event === 'uponSanitizeElement') {
|
|
32
|
+
hook.callback({ tagName: 'script' }, { allowedTags: { script: false }, tagName: 'script' });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check for and remove event handlers
|
|
38
|
+
if (/\s*on\w+\s*=/gi.test(html)) {
|
|
39
|
+
sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
40
|
+
hooksArray.forEach(hook => {
|
|
41
|
+
if (hook.event === 'uponSanitizeAttribute') {
|
|
42
|
+
hook.callback({ tagName: 'div' }, { attrName: 'onclick', attrValue: 'alert(1)', forceKeepAttr: false });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for and remove javascript: protocols
|
|
48
|
+
if (/javascript:/gi.test(html)) {
|
|
49
|
+
sanitized = sanitized.replace(/javascript:/gi, '');
|
|
50
|
+
hooksArray.forEach(hook => {
|
|
51
|
+
if (hook.event === 'uponSanitizeAttribute') {
|
|
52
|
+
hook.callback({ tagName: 'a' }, { attrName: 'href', attrValue: 'javascript:void(0)', forceKeepAttr: false });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for and remove iframe tags
|
|
58
|
+
if (/<iframe/gi.test(html)) {
|
|
59
|
+
sanitized = sanitized.replace(/<iframe[^>]*>.*?<\/iframe>/gi, '');
|
|
60
|
+
hooksArray.forEach(hook => {
|
|
61
|
+
if (hook.event === 'uponSanitizeElement') {
|
|
62
|
+
hook.callback({ tagName: 'iframe' }, { allowedTags: { iframe: false }, tagName: 'iframe' });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return sanitized;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Create a factory function that returns DOMPurify instances with their own hooks
|
|
71
|
+
const createInstance = () => {
|
|
72
|
+
const instanceHooks = []; // Each instance gets its own hooks array
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
sanitize: createMockSanitize(instanceHooks),
|
|
76
|
+
addHook: (event, callback) => {
|
|
77
|
+
instanceHooks.push({ event, callback });
|
|
78
|
+
},
|
|
79
|
+
removeHook: (event, callback) => {
|
|
80
|
+
const index = instanceHooks.findIndex(hook => hook.event === event && hook.callback === callback);
|
|
81
|
+
if (index !== -1) {
|
|
82
|
+
instanceHooks.splice(index, 1);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
removeAllHooks: () => {
|
|
86
|
+
instanceHooks.length = 0; // Clear the instance hooks array
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
default: createInstance, // DOMPurify() creates instances
|
|
93
|
+
sanitize: createMockSanitize(globalHooks), // Legacy global methods for backward compatibility
|
|
94
|
+
addHook: (event, callback) => {
|
|
95
|
+
globalHooks.push({ event, callback });
|
|
96
|
+
},
|
|
97
|
+
removeAllHooks: () => {
|
|
98
|
+
globalHooks.length = 0; // Clear the global hooks array
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('contentSanitizer', () => {
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
// Reset global hooks before each test to prevent test pollution
|
|
106
|
+
const DOMPurify = require('dompurify');
|
|
107
|
+
if (DOMPurify.removeAllHooks) {
|
|
108
|
+
DOMPurify.removeAllHooks();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Clear module cache to ensure fresh instances
|
|
112
|
+
jest.clearAllMocks();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('sanitizeHTML', () => {
|
|
116
|
+
describe('Basic Sanitization', () => {
|
|
117
|
+
it('sanitizes valid HTML content', () => {
|
|
118
|
+
const html = '<p>Hello World</p>';
|
|
119
|
+
const result = sanitizeHTML(html);
|
|
120
|
+
|
|
121
|
+
expect(result).toHaveProperty('sanitized');
|
|
122
|
+
expect(result).toHaveProperty('isClean');
|
|
123
|
+
expect(result).toHaveProperty('removedElements');
|
|
124
|
+
expect(result).toHaveProperty('warnings');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('handles empty string', () => {
|
|
128
|
+
const result = sanitizeHTML('');
|
|
129
|
+
|
|
130
|
+
expect(result.sanitized).toBe('');
|
|
131
|
+
expect(result.isClean).toBe(true);
|
|
132
|
+
expect(result.warnings).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('handles null input', () => {
|
|
136
|
+
const result = sanitizeHTML(null);
|
|
137
|
+
|
|
138
|
+
expect(result.sanitized).toBe('');
|
|
139
|
+
expect(result.isClean).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('handles undefined input', () => {
|
|
143
|
+
const result = sanitizeHTML(undefined);
|
|
144
|
+
|
|
145
|
+
expect(result.sanitized).toBe('');
|
|
146
|
+
expect(result.isClean).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('handles non-string input', () => {
|
|
150
|
+
const result = sanitizeHTML(123);
|
|
151
|
+
|
|
152
|
+
expect(result.sanitized).toBe('');
|
|
153
|
+
expect(result.isClean).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('XSS Protection', () => {
|
|
158
|
+
it('removes script tags', () => {
|
|
159
|
+
const html = '<p>Hello</p><script>alert("xss")</script>';
|
|
160
|
+
const result = sanitizeHTML(html);
|
|
161
|
+
|
|
162
|
+
expect(result.sanitized).not.toContain('<script');
|
|
163
|
+
expect(result.isClean).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('removes event handler attributes', () => {
|
|
167
|
+
const html = '<div onclick="alert(1)">Click me</div>';
|
|
168
|
+
const result = sanitizeHTML(html);
|
|
169
|
+
|
|
170
|
+
expect(result.sanitized).not.toContain('onclick');
|
|
171
|
+
expect(result.isClean).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('removes javascript: protocol', () => {
|
|
175
|
+
const html = '<a href="javascript:void(0)">Link</a>';
|
|
176
|
+
const result = sanitizeHTML(html);
|
|
177
|
+
|
|
178
|
+
expect(result.sanitized).not.toContain('javascript:');
|
|
179
|
+
expect(result.isClean).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('removes iframe tags', () => {
|
|
183
|
+
const html = '<iframe src="evil.com"></iframe>';
|
|
184
|
+
const result = sanitizeHTML(html);
|
|
185
|
+
|
|
186
|
+
expect(result.sanitized).not.toContain('iframe');
|
|
187
|
+
expect(result.isClean).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('removes multiple dangerous elements', () => {
|
|
191
|
+
const html = `
|
|
192
|
+
<div onclick="alert(1)">
|
|
193
|
+
<script>alert("xss")</script>
|
|
194
|
+
<iframe src="evil.com"></iframe>
|
|
195
|
+
</div>
|
|
196
|
+
`;
|
|
197
|
+
const result = sanitizeHTML(html);
|
|
198
|
+
|
|
199
|
+
expect(result.sanitized).not.toContain('script');
|
|
200
|
+
expect(result.sanitized).not.toContain('iframe');
|
|
201
|
+
expect(result.sanitized).not.toContain('onclick');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('Variant-Specific Sanitization', () => {
|
|
206
|
+
it('uses email config for email variant', () => {
|
|
207
|
+
const html = '<p>Email content</p>';
|
|
208
|
+
const result = sanitizeHTML(html, 'email');
|
|
209
|
+
|
|
210
|
+
expect(result).toBeDefined();
|
|
211
|
+
expect(result.sanitized).toBeDefined();
|
|
212
|
+
expect(typeof result.sanitized).toBe('string');
|
|
213
|
+
// The mock should preserve safe HTML
|
|
214
|
+
if (result.sanitized.length > 0) {
|
|
215
|
+
expect(result.sanitized).toContain('Email content');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('uses inapp config for inapp variant', () => {
|
|
220
|
+
const html = '<p>InApp content</p>';
|
|
221
|
+
const result = sanitizeHTML(html, 'inapp');
|
|
222
|
+
|
|
223
|
+
expect(result).toBeDefined();
|
|
224
|
+
expect(result.sanitized).toBeDefined();
|
|
225
|
+
expect(typeof result.sanitized).toBe('string');
|
|
226
|
+
// The mock should preserve safe HTML
|
|
227
|
+
if (result.sanitized.length > 0) {
|
|
228
|
+
expect(result.sanitized).toContain('InApp content');
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('defaults to email variant', () => {
|
|
233
|
+
const html = '<p>Default content</p>';
|
|
234
|
+
const result = sanitizeHTML(html);
|
|
235
|
+
|
|
236
|
+
expect(result).toBeDefined();
|
|
237
|
+
expect(result.sanitized).toBeDefined();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('Security Levels', () => {
|
|
242
|
+
it('applies standard level by default', () => {
|
|
243
|
+
const html = '<p>Standard content</p>';
|
|
244
|
+
const result = sanitizeHTML(html);
|
|
245
|
+
|
|
246
|
+
expect(result).toBeDefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('applies strict level when specified', () => {
|
|
250
|
+
const html = '<p>Strict content</p>';
|
|
251
|
+
const result = sanitizeHTML(html, 'email', 'strict');
|
|
252
|
+
|
|
253
|
+
expect(result).toBeDefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('strict level is more restrictive', () => {
|
|
257
|
+
const html = '<style>.test{}</style><p>Content</p>';
|
|
258
|
+
const resultStandard = sanitizeHTML(html, 'email', 'standard');
|
|
259
|
+
const resultStrict = sanitizeHTML(html, 'email', 'strict');
|
|
260
|
+
|
|
261
|
+
expect(resultStrict).toBeDefined();
|
|
262
|
+
expect(resultStandard).toBeDefined();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('Email Variant Warnings', () => {
|
|
267
|
+
it('warns about video elements in email', () => {
|
|
268
|
+
const html = '<video src="video.mp4"></video>';
|
|
269
|
+
const result = sanitizeHTML(html, 'email');
|
|
270
|
+
|
|
271
|
+
console.log('DEBUG - Video test result:', JSON.stringify(result, null, 2));
|
|
272
|
+
expect(result.warnings).toBeDefined();
|
|
273
|
+
expect(Array.isArray(result.warnings)).toBe(true);
|
|
274
|
+
// Video warnings are added by addVariantWarnings function
|
|
275
|
+
const hasVideoWarning = result.warnings.some(w =>
|
|
276
|
+
w.message && w.message.includes('Video/audio')
|
|
277
|
+
);
|
|
278
|
+
expect(hasVideoWarning).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('warns about audio elements in email', () => {
|
|
282
|
+
const html = '<audio src="audio.mp3"></audio>';
|
|
283
|
+
const result = sanitizeHTML(html, 'email');
|
|
284
|
+
|
|
285
|
+
expect(result.warnings).toBeDefined();
|
|
286
|
+
const hasAudioWarning = result.warnings.some(w =>
|
|
287
|
+
w.message && w.message.includes('Video/audio')
|
|
288
|
+
);
|
|
289
|
+
expect(hasAudioWarning).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('warns about fixed positioning in email', () => {
|
|
293
|
+
const html = '<div style="position: fixed">Fixed</div>';
|
|
294
|
+
const result = sanitizeHTML(html, 'email');
|
|
295
|
+
|
|
296
|
+
expect(result.warnings).toBeDefined();
|
|
297
|
+
const hasPositionWarning = result.warnings.some(w =>
|
|
298
|
+
w.message && w.message.includes('Fixed/sticky positioning')
|
|
299
|
+
);
|
|
300
|
+
expect(hasPositionWarning).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('warns about sticky positioning in email', () => {
|
|
304
|
+
const html = '<div style="position: sticky">Sticky</div>';
|
|
305
|
+
const result = sanitizeHTML(html, 'email');
|
|
306
|
+
|
|
307
|
+
expect(result.warnings).toBeDefined();
|
|
308
|
+
const hasPositionWarning = result.warnings.some(w =>
|
|
309
|
+
w.message && w.message.includes('Fixed/sticky positioning')
|
|
310
|
+
);
|
|
311
|
+
expect(hasPositionWarning).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('InApp Variant Warnings', () => {
|
|
316
|
+
it('provides info about tables in mobile', () => {
|
|
317
|
+
const html = '<table><tr><td>Data</td></tr></table>';
|
|
318
|
+
const result = sanitizeHTML(html, 'inapp');
|
|
319
|
+
|
|
320
|
+
expect(result.warnings).toBeDefined();
|
|
321
|
+
const hasTableInfo = result.warnings.some(w =>
|
|
322
|
+
w.message && w.message.includes('Tables may not be mobile-friendly')
|
|
323
|
+
);
|
|
324
|
+
expect(hasTableInfo).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('suggests relative font sizes for mobile', () => {
|
|
328
|
+
const html = '<div style="font-size: 14px">Text</div>';
|
|
329
|
+
const result = sanitizeHTML(html, 'inapp');
|
|
330
|
+
|
|
331
|
+
expect(result.warnings).toBeDefined();
|
|
332
|
+
const hasFontInfo = result.warnings.some(w =>
|
|
333
|
+
w.message && w.message.includes('relative font sizes')
|
|
334
|
+
);
|
|
335
|
+
expect(hasFontInfo).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('Removed Elements Tracking', () => {
|
|
340
|
+
it('tracks removed forbidden tags', () => {
|
|
341
|
+
const html = '<script>alert(1)</script>';
|
|
342
|
+
const result = sanitizeHTML(html);
|
|
343
|
+
|
|
344
|
+
// When dangerous content is removed, isClean should be false
|
|
345
|
+
expect(result.isClean).toBe(false);
|
|
346
|
+
// removedElements array should exist
|
|
347
|
+
expect(Array.isArray(result.removedElements)).toBe(true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('tracks removed event handlers', () => {
|
|
351
|
+
const html = '<div onclick="alert(1)">Test</div>';
|
|
352
|
+
const result = sanitizeHTML(html);
|
|
353
|
+
|
|
354
|
+
// Content should be marked as not clean
|
|
355
|
+
expect(result.isClean).toBe(false);
|
|
356
|
+
expect(Array.isArray(result.removedElements)).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('tracks removed javascript protocols', () => {
|
|
360
|
+
const html = '<a href="javascript:void(0)">Link</a>';
|
|
361
|
+
const result = sanitizeHTML(html);
|
|
362
|
+
|
|
363
|
+
// Content should be marked as not clean
|
|
364
|
+
expect(result.isClean).toBe(false);
|
|
365
|
+
expect(Array.isArray(result.removedElements)).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('Error Handling', () => {
|
|
370
|
+
it('handles DOMPurify errors gracefully', () => {
|
|
371
|
+
// Since we can't easily mock a one-time error with our plain function mock,
|
|
372
|
+
// we'll test the error path by ensuring the function handles edge cases
|
|
373
|
+
const result = sanitizeHTML(null);
|
|
374
|
+
|
|
375
|
+
// Should handle null gracefully
|
|
376
|
+
expect(result.sanitized).toBe('');
|
|
377
|
+
expect(result.isClean).toBe(true);
|
|
378
|
+
expect(Array.isArray(result.warnings)).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('Content Modification Detection', () => {
|
|
383
|
+
it('detects when content is modified', () => {
|
|
384
|
+
const html = '<script>alert(1)</script><p>Safe</p>';
|
|
385
|
+
const result = sanitizeHTML(html);
|
|
386
|
+
|
|
387
|
+
expect(result.isClean).toBe(false);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('marks clean content as clean', () => {
|
|
391
|
+
const html = '<p>Clean content</p>';
|
|
392
|
+
const result = sanitizeHTML(html);
|
|
393
|
+
|
|
394
|
+
// Note: isClean will be true only if sanitized === html
|
|
395
|
+
expect(result).toBeDefined();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('prepareForProduction', () => {
|
|
401
|
+
it('sanitizes content with strict rules', () => {
|
|
402
|
+
const html = '<p>Production content</p>';
|
|
403
|
+
const result = prepareForProduction(html);
|
|
404
|
+
|
|
405
|
+
expect(result).toHaveProperty('content');
|
|
406
|
+
expect(result).toHaveProperty('isProductionReady');
|
|
407
|
+
expect(result).toHaveProperty('securityIssues');
|
|
408
|
+
expect(result).toHaveProperty('warnings');
|
|
409
|
+
expect(result).toHaveProperty('recommendations');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('marks unsafe content as not production ready', () => {
|
|
413
|
+
const html = '<script>alert(1)</script><p>Content</p>';
|
|
414
|
+
const result = prepareForProduction(html);
|
|
415
|
+
|
|
416
|
+
expect(result.isProductionReady).toBe(false);
|
|
417
|
+
expect(result.recommendations.length).toBeGreaterThan(0);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('provides recommendations for sanitized content', () => {
|
|
421
|
+
const html = '<script>alert(1)</script><p>Content</p>';
|
|
422
|
+
const result = prepareForProduction(html);
|
|
423
|
+
|
|
424
|
+
const sanitizeRec = result.recommendations.find(r =>
|
|
425
|
+
r.includes('sanitized')
|
|
426
|
+
);
|
|
427
|
+
expect(sanitizeRec).toBeDefined();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('recommends inlining CSS for email', () => {
|
|
431
|
+
const html = '<style>.test{}</style><p>Email</p>';
|
|
432
|
+
const result = prepareForProduction(html, 'email');
|
|
433
|
+
|
|
434
|
+
const cssRec = result.recommendations.find(r =>
|
|
435
|
+
r.includes('inlining CSS')
|
|
436
|
+
);
|
|
437
|
+
expect(cssRec).toBeDefined();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('warns about large content for inapp', () => {
|
|
441
|
+
const largeHtml = '<p>' + 'a'.repeat(50001) + '</p>';
|
|
442
|
+
const result = prepareForProduction(largeHtml, 'inapp');
|
|
443
|
+
|
|
444
|
+
const sizeRec = result.recommendations.find(r =>
|
|
445
|
+
r.includes('large')
|
|
446
|
+
);
|
|
447
|
+
expect(sizeRec).toBeDefined();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('handles email variant', () => {
|
|
451
|
+
const html = '<p>Email content</p>';
|
|
452
|
+
const result = prepareForProduction(html, 'email');
|
|
453
|
+
|
|
454
|
+
expect(result.content).toBeDefined();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('handles inapp variant', () => {
|
|
458
|
+
const html = '<p>InApp content</p>';
|
|
459
|
+
const result = prepareForProduction(html, 'inapp');
|
|
460
|
+
|
|
461
|
+
expect(result.content).toBeDefined();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('defaults to email variant', () => {
|
|
465
|
+
const html = '<p>Default content</p>';
|
|
466
|
+
const result = prepareForProduction(html);
|
|
467
|
+
|
|
468
|
+
expect(result.content).toBeDefined();
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('isContentSafe', () => {
|
|
473
|
+
it('returns true for safe content', () => {
|
|
474
|
+
const html = '<p>Safe content</p>';
|
|
475
|
+
const result = isContentSafe(html);
|
|
476
|
+
|
|
477
|
+
expect(result).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('returns true for empty content', () => {
|
|
481
|
+
const result = isContentSafe('');
|
|
482
|
+
|
|
483
|
+
expect(result).toBe(true);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('returns true for null content', () => {
|
|
487
|
+
const result = isContentSafe(null);
|
|
488
|
+
|
|
489
|
+
expect(result).toBe(true);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('detects javascript: protocol', () => {
|
|
493
|
+
const html = '<a href="javascript:void(0)">Link</a>';
|
|
494
|
+
const result = isContentSafe(html);
|
|
495
|
+
|
|
496
|
+
expect(result).toBe(false);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('detects data: protocol', () => {
|
|
500
|
+
const html = '<img src="data:text/html,<script>alert(1)</script>">';
|
|
501
|
+
const result = isContentSafe(html);
|
|
502
|
+
|
|
503
|
+
expect(result).toBe(false);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('detects vbscript: protocol', () => {
|
|
507
|
+
const html = '<a href="vbscript:msgbox">Link</a>';
|
|
508
|
+
const result = isContentSafe(html);
|
|
509
|
+
|
|
510
|
+
expect(result).toBe(false);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('detects script tags', () => {
|
|
514
|
+
const html = '<script>alert(1)</script>';
|
|
515
|
+
const result = isContentSafe(html);
|
|
516
|
+
|
|
517
|
+
expect(result).toBe(false);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('detects onclick handlers', () => {
|
|
521
|
+
const html = '<div onclick="alert(1)">Click</div>';
|
|
522
|
+
const result = isContentSafe(html);
|
|
523
|
+
|
|
524
|
+
expect(result).toBe(false);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('detects onload handlers', () => {
|
|
528
|
+
const html = '<body onload="alert(1)">Content</body>';
|
|
529
|
+
const result = isContentSafe(html);
|
|
530
|
+
|
|
531
|
+
expect(result).toBe(false);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('detects onerror handlers', () => {
|
|
535
|
+
const html = '<img src=x onerror="alert(1)">';
|
|
536
|
+
const result = isContentSafe(html);
|
|
537
|
+
|
|
538
|
+
expect(result).toBe(false);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('detects onmouseover handlers', () => {
|
|
542
|
+
const html = '<div onmouseover="alert(1)">Hover</div>';
|
|
543
|
+
const result = isContentSafe(html);
|
|
544
|
+
|
|
545
|
+
expect(result).toBe(false);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('detects iframe tags', () => {
|
|
549
|
+
const html = '<iframe src="evil.com"></iframe>';
|
|
550
|
+
const result = isContentSafe(html);
|
|
551
|
+
|
|
552
|
+
expect(result).toBe(false);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('detects object tags', () => {
|
|
556
|
+
const html = '<object data="evil.swf"></object>';
|
|
557
|
+
const result = isContentSafe(html);
|
|
558
|
+
|
|
559
|
+
expect(result).toBe(false);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('detects embed tags', () => {
|
|
563
|
+
const html = '<embed src="evil.swf">';
|
|
564
|
+
const result = isContentSafe(html);
|
|
565
|
+
|
|
566
|
+
expect(result).toBe(false);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('is case insensitive', () => {
|
|
570
|
+
const html = '<SCRIPT>alert(1)</SCRIPT>';
|
|
571
|
+
const result = isContentSafe(html);
|
|
572
|
+
|
|
573
|
+
expect(result).toBe(false);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('findUnsafeContent', () => {
|
|
578
|
+
it('returns empty array for safe content', () => {
|
|
579
|
+
const html = '<p>Safe content</p>';
|
|
580
|
+
const result = findUnsafeContent(html);
|
|
581
|
+
|
|
582
|
+
expect(Array.isArray(result)).toBe(true);
|
|
583
|
+
expect(result).toHaveLength(0);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('returns empty array for empty content', () => {
|
|
587
|
+
const result = findUnsafeContent('');
|
|
588
|
+
|
|
589
|
+
expect(result).toHaveLength(0);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('returns empty array for null content', () => {
|
|
593
|
+
const result = findUnsafeContent(null);
|
|
594
|
+
|
|
595
|
+
expect(result).toHaveLength(0);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('finds javascript protocol', () => {
|
|
599
|
+
const html = '<a href="javascript:void(0)">Link</a>';
|
|
600
|
+
const result = findUnsafeContent(html);
|
|
601
|
+
|
|
602
|
+
expect(result.length).toBeGreaterThan(0);
|
|
603
|
+
expect(result[0].type).toBe('JavaScript Protocol');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('finds data URLs', () => {
|
|
607
|
+
const html = '<img src="data:image/png,...">';
|
|
608
|
+
const result = findUnsafeContent(html);
|
|
609
|
+
|
|
610
|
+
expect(result.length).toBeGreaterThan(0);
|
|
611
|
+
expect(result[0].type).toBe('Data URL');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('finds script tags', () => {
|
|
615
|
+
const html = '<script>alert(1)</script>';
|
|
616
|
+
const result = findUnsafeContent(html);
|
|
617
|
+
|
|
618
|
+
expect(result.length).toBeGreaterThan(0);
|
|
619
|
+
const scriptTag = result.find(r => r.type === 'Script Tag');
|
|
620
|
+
expect(scriptTag).toBeDefined();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('finds event handlers', () => {
|
|
624
|
+
const html = '<div onclick="alert(1)">Click</div>';
|
|
625
|
+
const result = findUnsafeContent(html);
|
|
626
|
+
|
|
627
|
+
expect(result.length).toBeGreaterThan(0);
|
|
628
|
+
const eventHandler = result.find(r => r.type === 'Event Handler');
|
|
629
|
+
expect(eventHandler).toBeDefined();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('finds iframe tags', () => {
|
|
633
|
+
const html = '<iframe src="evil.com"></iframe>';
|
|
634
|
+
const result = findUnsafeContent(html);
|
|
635
|
+
|
|
636
|
+
expect(result.length).toBeGreaterThan(0);
|
|
637
|
+
const iframe = result.find(r => r.type === 'Iframe');
|
|
638
|
+
expect(iframe).toBeDefined();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('finds object and embed tags', () => {
|
|
642
|
+
const html = '<object data="evil.swf"></object>';
|
|
643
|
+
const result = findUnsafeContent(html);
|
|
644
|
+
|
|
645
|
+
expect(result.length).toBeGreaterThan(0);
|
|
646
|
+
const objectTag = result.find(r => r.type === 'Object/Embed');
|
|
647
|
+
expect(objectTag).toBeDefined();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('finds multiple unsafe patterns', () => {
|
|
651
|
+
const html = `
|
|
652
|
+
<script>alert(1)</script>
|
|
653
|
+
<div onclick="alert(2)">Click</div>
|
|
654
|
+
<iframe src="evil.com"></iframe>
|
|
655
|
+
`;
|
|
656
|
+
const result = findUnsafeContent(html);
|
|
657
|
+
|
|
658
|
+
expect(result.length).toBeGreaterThan(2);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('includes content in results', () => {
|
|
662
|
+
const html = '<script>alert(1)</script>';
|
|
663
|
+
const result = findUnsafeContent(html);
|
|
664
|
+
|
|
665
|
+
expect(result[0]).toHaveProperty('content');
|
|
666
|
+
expect(result[0].content).toContain('script');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('includes position in results', () => {
|
|
670
|
+
const html = '<p>Safe</p><script>alert(1)</script>';
|
|
671
|
+
const result = findUnsafeContent(html);
|
|
672
|
+
|
|
673
|
+
expect(result[0]).toHaveProperty('position');
|
|
674
|
+
expect(typeof result[0].position).toBe('number');
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('finds multiple occurrences of same pattern', () => {
|
|
678
|
+
const html = '<script>alert(1)</script><script>alert(2)</script>';
|
|
679
|
+
const result = findUnsafeContent(html);
|
|
680
|
+
|
|
681
|
+
const scriptTags = result.filter(r => r.type === 'Script Tag');
|
|
682
|
+
expect(scriptTags.length).toBe(2);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('is case insensitive', () => {
|
|
686
|
+
const html = '<SCRIPT>alert(1)</SCRIPT>';
|
|
687
|
+
const result = findUnsafeContent(html);
|
|
688
|
+
|
|
689
|
+
expect(result.length).toBeGreaterThan(0);
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
describe('Integration Tests', () => {
|
|
694
|
+
it('sanitizes and validates content in sequence', () => {
|
|
695
|
+
const html = '<script>alert(1)</script><p>Safe content</p>';
|
|
696
|
+
|
|
697
|
+
const isSafe = isContentSafe(html);
|
|
698
|
+
expect(isSafe).toBe(false);
|
|
699
|
+
|
|
700
|
+
const unsafe = findUnsafeContent(html);
|
|
701
|
+
expect(unsafe.length).toBeGreaterThan(0);
|
|
702
|
+
|
|
703
|
+
const sanitized = sanitizeHTML(html);
|
|
704
|
+
expect(sanitized.sanitized).not.toContain('script');
|
|
705
|
+
|
|
706
|
+
const preparedResult = prepareForProduction(html);
|
|
707
|
+
expect(preparedResult.isProductionReady).toBe(false);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('handles complex email template', () => {
|
|
711
|
+
const html = `
|
|
712
|
+
<html>
|
|
713
|
+
<head>
|
|
714
|
+
<style>.header{color:blue}</style>
|
|
715
|
+
</head>
|
|
716
|
+
<body>
|
|
717
|
+
<table>
|
|
718
|
+
<tr><td>Email Content</td></tr>
|
|
719
|
+
</table>
|
|
720
|
+
</body>
|
|
721
|
+
</html>
|
|
722
|
+
`;
|
|
723
|
+
|
|
724
|
+
const result = sanitizeHTML(html, 'email');
|
|
725
|
+
expect(result.sanitized).toBeDefined();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('handles complex inapp template', () => {
|
|
729
|
+
const html = `
|
|
730
|
+
<div style="font-size: 16px">
|
|
731
|
+
<video src="video.mp4"></video>
|
|
732
|
+
<table><tr><td>Data</td></tr></table>
|
|
733
|
+
</div>
|
|
734
|
+
`;
|
|
735
|
+
|
|
736
|
+
const result = sanitizeHTML(html, 'inapp');
|
|
737
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|