imapflow 1.4.8 → 1.4.9
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +10 -1
- package/Gruntfile.js +3 -1
- package/lib/commands/authenticate.js +8 -3
- package/lib/commands/enable.js +13 -4
- package/lib/commands/expunge.js +2 -2
- package/lib/commands/idle.js +6 -3
- package/lib/commands/list.js +237 -60
- package/lib/commands/move.js +2 -2
- package/lib/commands/namespace.js +3 -1
- package/lib/commands/search.js +88 -13
- package/lib/commands/status.js +13 -25
- package/lib/imap-flow.d.ts +5 -3
- package/lib/imap-flow.js +39 -7
- package/lib/search-compiler.js +15 -1
- package/lib/tools.js +141 -9
- package/package.json +3 -2
- package/test/commands-branches-test.js +11 -4
- package/test/commands-integration-test.js +1163 -72
- package/test/fixtures/test-tls.js +2 -2
- package/test/imap-flow-coverage-test.js +8 -1
- package/test/imap-flow-fetch-download-test.js +1 -4
- package/test/imap-flow-internals-test.js +2 -2
- package/test/imap-flow-methods-test.js +65 -6
- package/test/imap-parser-test.js +1 -2
- package/test/integration/README.md +40 -0
- package/test/integration/dovecot-test.conf +27 -0
- package/test/integration/rev2-live-test.js +242 -0
- package/test/integration/run-rev2-tests.sh +61 -0
- package/test/reliability-improvements-test.js +4 -1
- package/test/search-compiler-test.js +19 -0
- package/test/search-test.js +52 -54
- package/test/tools-test.js +134 -15
package/test/search-test.js
CHANGED
|
@@ -14,6 +14,7 @@ function makeConnection({ hasEsearch = true } = {}) {
|
|
|
14
14
|
state: 'SELECTED',
|
|
15
15
|
states: { SELECTED: 'SELECTED' },
|
|
16
16
|
capabilities: caps,
|
|
17
|
+
enabled: new Set(),
|
|
17
18
|
exec: async () => ({ next: () => {} }),
|
|
18
19
|
log: { warn: () => {} }
|
|
19
20
|
};
|
|
@@ -72,23 +73,6 @@ module.exports['ESEARCH: parseEsearchResponse PARTIAL (Array form)'] = test => {
|
|
|
72
73
|
test.done();
|
|
73
74
|
};
|
|
74
75
|
|
|
75
|
-
module.exports['ESEARCH: parseEsearchResponse PARTIAL (LIST object form)'] = test => {
|
|
76
|
-
// Also handle {type: 'LIST', attributes: [...]} form for robustness
|
|
77
|
-
const attrs = [
|
|
78
|
-
{ type: 'ATOM', value: 'PARTIAL' },
|
|
79
|
-
{
|
|
80
|
-
type: 'LIST',
|
|
81
|
-
attributes: [
|
|
82
|
-
{ type: 'ATOM', value: '1:100' },
|
|
83
|
-
{ type: 'ATOM', value: '1001,1003:1010,1015' }
|
|
84
|
-
]
|
|
85
|
-
}
|
|
86
|
-
];
|
|
87
|
-
const result = parseEsearchResponse(attrs);
|
|
88
|
-
test.deepEqual(result.partial, { range: '1:100', messages: '1001,1003:1010,1015' });
|
|
89
|
-
test.done();
|
|
90
|
-
};
|
|
91
|
-
|
|
92
76
|
module.exports['ESEARCH: parseEsearchResponse COUNT + PARTIAL combined'] = test => {
|
|
93
77
|
const attrs = [
|
|
94
78
|
{ type: 'ATOM', value: 'COUNT' },
|
|
@@ -116,12 +100,14 @@ module.exports['ESEARCH: emits RETURN clause when returnOptions present and serv
|
|
|
116
100
|
capturedAttributes = JSON.stringify(attributes);
|
|
117
101
|
return { next: () => {} };
|
|
118
102
|
};
|
|
119
|
-
searchCmd(conn, { seen: false }, { uid: true, returnOptions: ['COUNT'] })
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
searchCmd(conn, { seen: false }, { uid: true, returnOptions: ['COUNT'] })
|
|
104
|
+
.then(() => {
|
|
105
|
+
test.equal(capturedCommand, 'UID SEARCH');
|
|
106
|
+
test.ok(capturedAttributes.includes('"RETURN"'), 'should include RETURN atom');
|
|
107
|
+
test.ok(capturedAttributes.includes('"COUNT"'), 'should include COUNT in return list');
|
|
108
|
+
test.done();
|
|
109
|
+
})
|
|
110
|
+
.catch(err => test.done(err));
|
|
125
111
|
};
|
|
126
112
|
|
|
127
113
|
module.exports['ESEARCH: RETURN clause includes PARTIAL range atom'] = test => {
|
|
@@ -131,11 +117,13 @@ module.exports['ESEARCH: RETURN clause includes PARTIAL range atom'] = test => {
|
|
|
131
117
|
capturedAttributes = JSON.stringify(attributes);
|
|
132
118
|
return { next: () => {} };
|
|
133
119
|
};
|
|
134
|
-
searchCmd(conn, { seen: false }, { uid: true, returnOptions: [{ partial: '1:100' }] })
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
searchCmd(conn, { seen: false }, { uid: true, returnOptions: [{ partial: '1:100' }] })
|
|
121
|
+
.then(() => {
|
|
122
|
+
test.ok(capturedAttributes.includes('"PARTIAL"'), 'should include PARTIAL atom');
|
|
123
|
+
test.ok(capturedAttributes.includes('"1:100"'), 'should include range string');
|
|
124
|
+
test.done();
|
|
125
|
+
})
|
|
126
|
+
.catch(err => test.done(err));
|
|
139
127
|
};
|
|
140
128
|
|
|
141
129
|
module.exports['ESEARCH: no RETURN clause when server lacks ESEARCH capability'] = test => {
|
|
@@ -152,12 +140,14 @@ module.exports['ESEARCH: no RETURN clause when server lacks ESEARCH capability']
|
|
|
152
140
|
}
|
|
153
141
|
return { next: () => {} };
|
|
154
142
|
};
|
|
155
|
-
searchCmd(conn, { seen: false }, { uid: true, returnOptions: ['COUNT', 'ALL'] })
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
searchCmd(conn, { seen: false }, { uid: true, returnOptions: ['COUNT', 'ALL'] })
|
|
144
|
+
.then(result => {
|
|
145
|
+
test.ok(!capturedAttributes.includes('"RETURN"'), 'should NOT include RETURN when no ESEARCH');
|
|
146
|
+
test.ok(Array.isArray(result), 'should return number[] when ESEARCH unavailable');
|
|
147
|
+
test.deepEqual(result, [1, 2, 3]);
|
|
148
|
+
test.done();
|
|
149
|
+
})
|
|
150
|
+
.catch(err => test.done(err));
|
|
161
151
|
};
|
|
162
152
|
|
|
163
153
|
module.exports['ESEARCH: parseEsearchResponse skips non-ATOM tokens'] = test => {
|
|
@@ -247,11 +237,13 @@ module.exports['ESEARCH: backward compat — no returnOptions returns number[]']
|
|
|
247
237
|
return { next: () => {} };
|
|
248
238
|
};
|
|
249
239
|
// No returnOptions — must return number[] even if server has ESEARCH
|
|
250
|
-
searchCmd(conn, { seen: true }, { uid: true })
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
240
|
+
searchCmd(conn, { seen: true }, { uid: true })
|
|
241
|
+
.then(result => {
|
|
242
|
+
test.ok(Array.isArray(result));
|
|
243
|
+
test.deepEqual(result, [10, 20]);
|
|
244
|
+
test.done();
|
|
245
|
+
})
|
|
246
|
+
.catch(err => test.done(err));
|
|
255
247
|
};
|
|
256
248
|
|
|
257
249
|
// ── imap-flow.js public API fallback test ─────────────────────────────────
|
|
@@ -271,16 +263,19 @@ module.exports['imap-flow: search() derives ESearchResult when server has no ESE
|
|
|
271
263
|
// Stub run() to return a sorted number[]
|
|
272
264
|
client.run = async () => [10, 20, 30, 40, 50];
|
|
273
265
|
|
|
274
|
-
client
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
266
|
+
client
|
|
267
|
+
.search({ seen: false }, { uid: true, returnOptions: ['COUNT', 'MIN', 'MAX', 'ALL'] })
|
|
268
|
+
.then(result => {
|
|
269
|
+
test.equal(typeof result, 'object', 'should return object, not array');
|
|
270
|
+
test.ok(!Array.isArray(result), 'should not be an array');
|
|
271
|
+
test.equal(result.count, 5);
|
|
272
|
+
test.equal(result.min, 10);
|
|
273
|
+
test.equal(result.max, 50);
|
|
274
|
+
// packMessageRange([10,20,30,40,50]) → "10,20,30,40,50" (non-contiguous)
|
|
275
|
+
test.ok(typeof result.all === 'string' && result.all.length > 0, 'all should be non-empty compact string');
|
|
276
|
+
test.done();
|
|
277
|
+
})
|
|
278
|
+
.catch(err => test.done(err));
|
|
284
279
|
};
|
|
285
280
|
|
|
286
281
|
module.exports['imap-flow: search() fallback with empty result set'] = test => {
|
|
@@ -294,9 +289,12 @@ module.exports['imap-flow: search() fallback with empty result set'] = test => {
|
|
|
294
289
|
client.state = client.states.SELECTED;
|
|
295
290
|
client.capabilities = new Map();
|
|
296
291
|
client.run = async () => [];
|
|
297
|
-
client
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
292
|
+
client
|
|
293
|
+
.search({}, { uid: true, returnOptions: ['COUNT', 'ALL'] })
|
|
294
|
+
.then(result => {
|
|
295
|
+
test.equal(result.count, 0);
|
|
296
|
+
test.equal(result.all, undefined, 'all should be absent for empty result');
|
|
297
|
+
test.done();
|
|
298
|
+
})
|
|
299
|
+
.catch(err => test.done(err));
|
|
302
300
|
};
|
package/test/tools-test.js
CHANGED
|
@@ -7,7 +7,8 @@ const iconv = require('iconv-lite');
|
|
|
7
7
|
|
|
8
8
|
// Mock connection for testing
|
|
9
9
|
let createMockConnection = (options = {}) => ({
|
|
10
|
-
enabled: new Set(options.utf8 ? ['UTF8=ACCEPT'] : []),
|
|
10
|
+
enabled: new Set(options.enabled || (options.utf8 ? ['UTF8=ACCEPT'] : [])),
|
|
11
|
+
capabilities: new Map(options.capabilities || []),
|
|
11
12
|
namespace: options.namespace || null
|
|
12
13
|
});
|
|
13
14
|
|
|
@@ -162,6 +163,102 @@ module.exports['Tools: updateCapabilities with valid list'] = test => {
|
|
|
162
163
|
test.done();
|
|
163
164
|
};
|
|
164
165
|
|
|
166
|
+
module.exports['Tools: updateCapabilities normalizes IMAP4rev2 casing'] = test => {
|
|
167
|
+
let list = [{ value: 'IMAP4rev2' }, { value: 'imap4rev1' }];
|
|
168
|
+
let result = tools.updateCapabilities(list);
|
|
169
|
+
// Wire tokens are uppercased, but the rev1/rev2 keys use the RFC spelling
|
|
170
|
+
test.equal(result.get('IMAP4rev2'), true);
|
|
171
|
+
test.equal(result.get('IMAP4rev1'), true);
|
|
172
|
+
test.equal(result.has('IMAP4REV2'), false);
|
|
173
|
+
test.done();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
module.exports['Tools: isRev2Active for rev2-only server'] = test => {
|
|
177
|
+
// rev2 without rev1 means rev2 is the base protocol, no ENABLE needed
|
|
178
|
+
let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
|
|
179
|
+
test.equal(tools.isRev2Active(connection), true);
|
|
180
|
+
test.done();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
module.exports['Tools: isRev2Active for dual server without ENABLE'] = test => {
|
|
184
|
+
// Advertising both keeps the session in rev1 mode until ENABLE IMAP4rev2
|
|
185
|
+
let connection = createMockConnection({
|
|
186
|
+
capabilities: [
|
|
187
|
+
['IMAP4rev1', true],
|
|
188
|
+
['IMAP4rev2', true]
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
test.equal(tools.isRev2Active(connection), false);
|
|
192
|
+
test.done();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
module.exports['Tools: isRev2Active for dual server with ENABLE'] = test => {
|
|
196
|
+
let connection = createMockConnection({
|
|
197
|
+
capabilities: [
|
|
198
|
+
['IMAP4rev1', true],
|
|
199
|
+
['IMAP4rev2', true]
|
|
200
|
+
],
|
|
201
|
+
enabled: ['IMAP4REV2']
|
|
202
|
+
});
|
|
203
|
+
test.equal(tools.isRev2Active(connection), true);
|
|
204
|
+
test.done();
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
module.exports['Tools: isRev2Active for rev1-only server'] = test => {
|
|
208
|
+
let connection = createMockConnection({ capabilities: [['IMAP4rev1', true]] });
|
|
209
|
+
test.equal(tools.isRev2Active(connection), false);
|
|
210
|
+
test.done();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
module.exports['Tools: hasCapability with advertised token'] = test => {
|
|
214
|
+
let connection = createMockConnection({
|
|
215
|
+
capabilities: [
|
|
216
|
+
['IMAP4rev1', true],
|
|
217
|
+
['UIDPLUS', true]
|
|
218
|
+
]
|
|
219
|
+
});
|
|
220
|
+
test.equal(tools.hasCapability(connection, 'UIDPLUS'), true);
|
|
221
|
+
test.equal(tools.hasCapability(connection, 'MOVE'), false);
|
|
222
|
+
test.done();
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
module.exports['Tools: hasCapability folds extensions into active rev2'] = test => {
|
|
226
|
+
let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
|
|
227
|
+
// RFC 9051 Appendix E folds these into base IMAP4rev2
|
|
228
|
+
for (let capability of ['UIDPLUS', 'MOVE', 'NAMESPACE', 'ESEARCH', 'LITERAL-', 'LIST-EXTENDED', 'LIST-STATUS', 'SPECIAL-USE', 'ENABLE']) {
|
|
229
|
+
test.equal(tools.hasCapability(connection, capability), true, `${capability} should be folded into rev2`);
|
|
230
|
+
}
|
|
231
|
+
// BINARY is intentionally not folded
|
|
232
|
+
test.equal(tools.hasCapability(connection, 'BINARY'), false);
|
|
233
|
+
test.done();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
module.exports['Tools: hasCapability does not fold on unenabled dual server'] = test => {
|
|
237
|
+
let connection = createMockConnection({
|
|
238
|
+
capabilities: [
|
|
239
|
+
['IMAP4rev1', true],
|
|
240
|
+
['IMAP4rev2', true]
|
|
241
|
+
]
|
|
242
|
+
});
|
|
243
|
+
// Session is in rev1 mode - only explicitly advertised tokens count
|
|
244
|
+
test.equal(tools.hasCapability(connection, 'UIDPLUS'), false);
|
|
245
|
+
test.done();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
module.exports['Tools: encodePath keeps UTF-8 when rev2 is active'] = test => {
|
|
249
|
+
let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
|
|
250
|
+
// rev2 mailbox names are native UTF-8, modified UTF-7 must not be applied
|
|
251
|
+
test.equal(tools.encodePath(connection, 'T\u00f5rva'), 'T\u00f5rva');
|
|
252
|
+
test.done();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
module.exports['Tools: decodePath keeps ampersand sequences when rev2 is active'] = test => {
|
|
256
|
+
let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
|
|
257
|
+
// Under rev2 an "&"-sequence is a literal name, not modified UTF-7
|
|
258
|
+
test.equal(tools.decodePath(connection, 'A&AOQ-B'), 'A&AOQ-B');
|
|
259
|
+
test.done();
|
|
260
|
+
};
|
|
261
|
+
|
|
165
262
|
module.exports['Tools: updateCapabilities with APPENDLIMIT'] = test => {
|
|
166
263
|
let list = [{ value: 'APPENDLIMIT=52428800' }];
|
|
167
264
|
let result = tools.updateCapabilities(list);
|
|
@@ -493,6 +590,30 @@ module.exports['Tools: expandRange with same start/end'] = test => {
|
|
|
493
590
|
test.done();
|
|
494
591
|
};
|
|
495
592
|
|
|
593
|
+
module.exports['Tools: expandRange skips entries that are not valid nz-numbers'] = test => {
|
|
594
|
+
// Server-supplied garbage must not produce bogus ids or endless loops
|
|
595
|
+
test.deepEqual(tools.expandRange('Infinity:5'), []);
|
|
596
|
+
test.deepEqual(tools.expandRange('0:3'), []);
|
|
597
|
+
test.deepEqual(tools.expandRange('abc,4,1:x'), [4]);
|
|
598
|
+
test.deepEqual(tools.expandRange('*'), []);
|
|
599
|
+
test.deepEqual(tools.expandRange('4294967296'), []);
|
|
600
|
+
test.done();
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
module.exports['Tools: expandRange caps hostile range spans'] = test => {
|
|
604
|
+
// A hostile range like 1:4294967295 is cut off at the expansion limit
|
|
605
|
+
// instead of exhausting memory
|
|
606
|
+
let result = tools.expandRange('1:4294967295');
|
|
607
|
+
test.equal(result.length, 0x1000000);
|
|
608
|
+
test.equal(result[0], 1);
|
|
609
|
+
test.equal(result[result.length - 1], 0x1000000);
|
|
610
|
+
|
|
611
|
+
let reverse = tools.expandRange('4294967295:4278190080');
|
|
612
|
+
test.equal(reverse.length, 0x1000000);
|
|
613
|
+
test.equal(reverse[0], 4294967295);
|
|
614
|
+
test.done();
|
|
615
|
+
};
|
|
616
|
+
|
|
496
617
|
// ============================================
|
|
497
618
|
// packMessageRange tests
|
|
498
619
|
// ============================================
|
|
@@ -1303,9 +1424,7 @@ module.exports['Tools: parseBodystructure parses message/rfc822 with envelope an
|
|
|
1303
1424
|
};
|
|
1304
1425
|
|
|
1305
1426
|
module.exports['Tools: parseBodystructure parses multipart with params and disposition'] = async test => {
|
|
1306
|
-
let bs =
|
|
1307
|
-
'(("TEXT" "PLAIN" NIL NIL NIL "7BIT" 10 1)("TEXT" "HTML" NIL NIL NIL "7BIT" 20 2) ' +
|
|
1308
|
-
'"ALTERNATIVE" ("BOUNDARY" "xyz") ("inline" NIL) ("en"))';
|
|
1427
|
+
let bs = '(("TEXT" "PLAIN" NIL NIL NIL "7BIT" 10 1)("TEXT" "HTML" NIL NIL NIL "7BIT" 20 2) "ALTERNATIVE" ("BOUNDARY" "xyz") ("inline" NIL) ("en"))';
|
|
1309
1428
|
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1310
1429
|
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1311
1430
|
test.equal(node.type, 'multipart/alternative');
|
|
@@ -1348,7 +1467,7 @@ module.exports['Tools: parseBodystructure handles minimal NIL fields'] = async t
|
|
|
1348
1467
|
};
|
|
1349
1468
|
|
|
1350
1469
|
module.exports['Tools: parseBodystructure decodes RFC 2231 charset continuation params'] = async test => {
|
|
1351
|
-
let bs =
|
|
1470
|
+
let bs = '("TEXT" "PLAIN" ("name*0*" "utf-8\'\'%E2%82%AC abc" "name*1" "def") NIL NIL "7BIT" 10 1)';
|
|
1352
1471
|
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1353
1472
|
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1354
1473
|
test.equal(node.parameters.name, '€ abcdef');
|
|
@@ -1453,9 +1572,7 @@ module.exports['Tools: parseBodystructure empty-string size/linecount/language/l
|
|
|
1453
1572
|
};
|
|
1454
1573
|
|
|
1455
1574
|
module.exports['Tools: parseBodystructure message/rfc822 empty-string linecount'] = async test => {
|
|
1456
|
-
let bs =
|
|
1457
|
-
'("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 100 ("d" "s" NIL NIL NIL NIL NIL NIL NIL NIL) ' +
|
|
1458
|
-
'("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) "")';
|
|
1575
|
+
let bs = '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 100 ("d" "s" NIL NIL NIL NIL NIL NIL NIL NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) "")';
|
|
1459
1576
|
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1460
1577
|
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1461
1578
|
test.equal(node.type, 'message/rfc822');
|
|
@@ -1468,7 +1585,7 @@ module.exports['Tools: parseBodystructure decodes RFC 2231 with empty charset an
|
|
|
1468
1585
|
// exercise the 2-char hex escape branch; the empty *1* continuation segment too.
|
|
1469
1586
|
// value includes '=' / '?' (2-char hex escapes), a space (-> '_') and a TAB
|
|
1470
1587
|
// (a <0x10 control char -> single-hex-digit escape needing a '0' pad).
|
|
1471
|
-
let bs =
|
|
1588
|
+
let bs = '("TEXT" "PLAIN" ("name*0*" "\'\'a=b?c d\te" "name*1*" "") NIL NIL "7BIT" 1 1)';
|
|
1472
1589
|
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1473
1590
|
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1474
1591
|
test.ok(node.parameters.name.includes('a=b?c'));
|
|
@@ -1485,13 +1602,15 @@ module.exports['Tools: parseBodystructure parses empty-string body location'] =
|
|
|
1485
1602
|
test.done();
|
|
1486
1603
|
};
|
|
1487
1604
|
|
|
1488
|
-
module.exports['Tools: expandRange descending range to non-numeric second bound'] = test => {
|
|
1489
|
-
|
|
1605
|
+
module.exports['Tools: expandRange skips descending range to non-numeric second bound'] = test => {
|
|
1606
|
+
// Endpoints that are not valid nz-numbers make the whole entry invalid -
|
|
1607
|
+
// the pre-hardening behavior expanded '5:abc' down to 0
|
|
1608
|
+
test.deepEqual(tools.expandRange('5:abc'), []);
|
|
1490
1609
|
test.done();
|
|
1491
1610
|
};
|
|
1492
1611
|
|
|
1493
|
-
module.exports['Tools: expandRange
|
|
1494
|
-
test.deepEqual(tools.expandRange('abc,:5,1:3'), [
|
|
1612
|
+
module.exports['Tools: expandRange skips non-numeric entries but keeps valid ones'] = test => {
|
|
1613
|
+
test.deepEqual(tools.expandRange('abc,:5,1:3'), [1, 2, 3]);
|
|
1495
1614
|
test.done();
|
|
1496
1615
|
};
|
|
1497
1616
|
|
|
@@ -1508,7 +1627,7 @@ module.exports['Tools: encodePath keeps name as-is when iconv encode throws'] =
|
|
|
1508
1627
|
throw new Error('encode boom');
|
|
1509
1628
|
};
|
|
1510
1629
|
try {
|
|
1511
|
-
let connection = { enabled: new Set() };
|
|
1630
|
+
let connection = { enabled: new Set(), capabilities: new Map() };
|
|
1512
1631
|
let result = tools.encodePath(connection, 'Tärä');
|
|
1513
1632
|
test.equal(result, 'Tärä', 'falls back to the raw path');
|
|
1514
1633
|
} finally {
|
|
@@ -1523,7 +1642,7 @@ module.exports['Tools: decodePath keeps name as-is when iconv decode throws'] =
|
|
|
1523
1642
|
throw new Error('decode boom');
|
|
1524
1643
|
};
|
|
1525
1644
|
try {
|
|
1526
|
-
let connection = { enabled: new Set() };
|
|
1645
|
+
let connection = { enabled: new Set(), capabilities: new Map() };
|
|
1527
1646
|
let result = tools.decodePath(connection, 'Inbox&AOk-');
|
|
1528
1647
|
test.equal(result, 'Inbox&AOk-');
|
|
1529
1648
|
} finally {
|