@pixlcore/xycli 1.0.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/README.md +543 -0
- package/docs/help.md +2587 -0
- package/docs/screenshots.md +219 -0
- package/docs/sync.md +920 -0
- package/docs/tutorial.md +204 -0
- package/index.js +444 -0
- package/lib/alerts.js +735 -0
- package/lib/apikey.js +425 -0
- package/lib/buckets.js +607 -0
- package/lib/categories.js +323 -0
- package/lib/channels.js +290 -0
- package/lib/config.js +86 -0
- package/lib/dashboard.js +237 -0
- package/lib/doc.js +131 -0
- package/lib/events.js +1314 -0
- package/lib/groups.js +1038 -0
- package/lib/jobs.js +1254 -0
- package/lib/log.js +125 -0
- package/lib/marketplace.js +386 -0
- package/lib/monitors.js +412 -0
- package/lib/plugins.js +491 -0
- package/lib/secrets.js +441 -0
- package/lib/servers.js +1502 -0
- package/lib/snapshots.js +498 -0
- package/lib/sync.js +963 -0
- package/lib/system.js +612 -0
- package/lib/tags.js +250 -0
- package/lib/tickets.js +681 -0
- package/lib/transfer.js +447 -0
- package/lib/utils.js +2745 -0
- package/lib/webhook.js +414 -0
- package/logo.webp +0 -0
- package/package.json +38 -0
- package/test/README.md +97 -0
- package/test/api-validation.test.js +170 -0
- package/test/categories.test.js +246 -0
- package/test/channels.test.js +209 -0
- package/test/config.test.js +96 -0
- package/test/doc.test.js +84 -0
- package/test/groups.test.js +211 -0
- package/test/help.test.js +26 -0
- package/test/helpers/common.js +127 -0
- package/test/helpers/pagination-preload.cjs +18 -0
- package/test/log.test.js +103 -0
- package/test/markdown.test.js +61 -0
- package/test/marketplace.test.js +229 -0
- package/test/monitors.test.js +227 -0
- package/test/pagination.test.js +138 -0
- package/test/plugins.test.js +351 -0
- package/test/secrets.test.js +271 -0
- package/test/servers.test.js +303 -0
- package/test/snapshots.test.js +167 -0
- package/test/sync-unit.test.js +446 -0
- package/test/sync.test.js +542 -0
- package/test/system.test.js +94 -0
- package/test/tags.test.js +230 -0
- package/test/tickets.test.js +261 -0
- package/test/toast.test.js +73 -0
- package/test/transfer-apikey.test.js +96 -0
- package/test/transfer-workflow.test.js +122 -0
- package/test/transfer.test.js +314 -0
- package/test/webhook.test.js +332 -0
- package/test.js +49 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const Path = require('node:path');
|
|
5
|
+
const { loadTestConfig, createCheck, createTempDir, xy, json, call: apiCall, cleanupFixtures } = require('./helpers/common.js');
|
|
6
|
+
|
|
7
|
+
test('webhook', async t => {
|
|
8
|
+
loadTestConfig();
|
|
9
|
+
const check = createCheck(t);
|
|
10
|
+
|
|
11
|
+
// Copy the built-in Example Hook destination into a disposable definition.
|
|
12
|
+
// The test API performs real requests, but this fixture is never attached to
|
|
13
|
+
// an Event, Category, Alert, Server Group, or Secret Vault.
|
|
14
|
+
const stamp = Date.now();
|
|
15
|
+
const id = 'cli_hook_test_' + stamp;
|
|
16
|
+
const title = 'CLI Web Hook Test ' + stamp;
|
|
17
|
+
const ids = new Set([id]);
|
|
18
|
+
const temp = createTempDir(t, 'xycli-webhook-test-');
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const original = (await apiCall('getWebHooks')).rows;
|
|
22
|
+
const example = original.find(hook => hook.id === 'example_hook');
|
|
23
|
+
assert.ok(example, 'Built-in example_hook Web Hook required');
|
|
24
|
+
|
|
25
|
+
const headers = [
|
|
26
|
+
{ name: 'Content-Type', value: 'application/json' },
|
|
27
|
+
{ name: 'User-Agent', value: 'xycli/WebHook-Test' }
|
|
28
|
+
];
|
|
29
|
+
const body = '{\n\t"text": "{{ text }}",\n\t"source": "xycli-test"\n}\n';
|
|
30
|
+
const headersFile = Path.join(temp, 'headers.json');
|
|
31
|
+
const bodyFile = Path.join(temp, 'body.json');
|
|
32
|
+
fs.writeFileSync(headersFile, JSON.stringify(headers));
|
|
33
|
+
fs.writeFileSync(bodyFile, body);
|
|
34
|
+
|
|
35
|
+
await check('dry creation fills web editor defaults', () => {
|
|
36
|
+
const req = json(['hook', 'create', '--id', id, '--title', title, '--url', example.url, '--dry']);
|
|
37
|
+
assert.equal(req.enabled, true);
|
|
38
|
+
assert.equal(req.method, 'POST');
|
|
39
|
+
assert.equal(req.timeout, 30);
|
|
40
|
+
assert.equal(req.retries, 0);
|
|
41
|
+
assert.equal(req.follow, false);
|
|
42
|
+
assert.equal(req.ssl_cert_bypass, false);
|
|
43
|
+
assert.equal(req.max_per_day, 0);
|
|
44
|
+
assert.deepEqual(req.headers, [
|
|
45
|
+
{ name: 'Content-Type', value: 'application/json' },
|
|
46
|
+
{ name: 'User-Agent', value: 'xyOps/WebHook' }
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await check('dry creation does not persist', () => assert.deepEqual(json(['hooks', '--id', id]), []));
|
|
51
|
+
|
|
52
|
+
const created = json([
|
|
53
|
+
'hook', 'create', '--id', id, '--title', title, '--url', example.url,
|
|
54
|
+
'--method', 'post', '--headers', '@' + headersFile, '--body', '@' + bodyFile,
|
|
55
|
+
'--timeout', '20', '--retries', '0', '--follow', 'true',
|
|
56
|
+
'--ssl_cert_bypass', 'false', '--max_per_day', '12',
|
|
57
|
+
'--icon', 'mdi-webhook', '--notes', 'First line\nSecond line'
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
await check('create persists request and delivery settings', () => {
|
|
61
|
+
assert.equal(created.id, id);
|
|
62
|
+
assert.equal(created.title, title);
|
|
63
|
+
assert.equal(created.enabled, true);
|
|
64
|
+
assert.equal(created.url, example.url);
|
|
65
|
+
assert.equal(created.method, 'POST');
|
|
66
|
+
assert.deepEqual(created.headers, headers);
|
|
67
|
+
assert.equal(created.body, body);
|
|
68
|
+
assert.equal(created.timeout, 20);
|
|
69
|
+
assert.equal(created.retries, 0);
|
|
70
|
+
assert.equal(created.follow, true);
|
|
71
|
+
assert.equal(created.ssl_cert_bypass, false);
|
|
72
|
+
assert.equal(created.max_per_day, 12);
|
|
73
|
+
assert.equal(created.icon, 'webhook');
|
|
74
|
+
assert.equal(created.revision, 1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await check('short and canonical detail commands are equivalent', () => {
|
|
78
|
+
assert.deepEqual(json(['hook', id]), created);
|
|
79
|
+
assert.deepEqual(json(['webhook', id]), created);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await check('fuzzy title and named ID get', () => {
|
|
83
|
+
assert.equal(json(['hook', 'get', '--title', title.toLowerCase()]).id, id);
|
|
84
|
+
assert.equal(json(['webhook', 'get', '--id', id]).id, id);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await check('short and canonical list aliases support combined filters', () => {
|
|
88
|
+
assert.deepEqual(json(['hooks', title, '--enabled', 'true', '--method', 'post']), [created]);
|
|
89
|
+
assert.deepEqual(json(['webhooks', '--id', id]), [created]);
|
|
90
|
+
assert.deepEqual(json(['hook', 'list', '--id', id]), [created]);
|
|
91
|
+
assert.deepEqual(json(['webhook', 'list', '--id', id]), [created]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await check('JSON list ignores human pagination arguments', () => {
|
|
95
|
+
assert.deepEqual(json(['hooks', '--id', id, '--limit', '1', '--page', '2']), [created]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await check('Web Hook list is ordered alphabetically', () => {
|
|
99
|
+
const rows = json(['hooks']);
|
|
100
|
+
assert.ok(rows.every((row, idx) => !idx || rows[idx - 1].title.toLowerCase().localeCompare(row.title.toLowerCase()) <= 0));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await check('human list supports pagination and suggestions', () => {
|
|
104
|
+
const out = xy(['hooks', '--limit', '1', '--page', '1']);
|
|
105
|
+
assert.match(out, /page 1 of/i);
|
|
106
|
+
assert.match(out, /Other Commands:/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await check('empty filtered Web Hook table', () => {
|
|
110
|
+
assert.match(xy(['hooks', id, '--enabled', 'false']), /No filtered web hooks found/i);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await check('human detail shows headers, highlighted body, and multiline notes', () => {
|
|
114
|
+
const out = xy(['hook', id]);
|
|
115
|
+
for (const label of ['Web Hook Summary', 'Hook ID', 'Method', 'Timeout', 'Retries', 'Follow Redirects', 'Daily Cap', 'Web Hook Headers', 'Content-Type', 'Web Hook Body', 'Web Hook Notes', 'Second line']) {
|
|
116
|
+
assert.ok(out.toLowerCase().includes(label.toLowerCase()), label);
|
|
117
|
+
}
|
|
118
|
+
const highlightedBody = body.trim().split(/\r?\n/).map( line => ' ' + line ).join('\n');
|
|
119
|
+
assert.ok(out.includes(highlightedBody), 'Highlighted multiline body is present');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await check('Hook alias works with portable export', () => {
|
|
123
|
+
const exportFile = Path.join(temp, 'hook-export.json');
|
|
124
|
+
xy(['hook', id, '--export', exportFile]);
|
|
125
|
+
const payload = JSON.parse(fs.readFileSync(exportFile, 'utf8'));
|
|
126
|
+
assert.equal(payload.items[0].type, 'web_hook');
|
|
127
|
+
assert.equal(payload.items[0].data.id, id);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await check('human update shows target and parsed data', () => {
|
|
131
|
+
const out = xy(['hook', 'update', id, '--timeout', '45', '--dry']);
|
|
132
|
+
assert.match(out, /UPDATE WEB HOOK/);
|
|
133
|
+
assert.match(out, new RegExp('Hook ID:\\s+' + id));
|
|
134
|
+
assert.match(out, /UPDATE DATA[\s\S]*"timeout": 45/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await check('sparse dry update excludes unrelated fields', () => {
|
|
138
|
+
assert.deepEqual(json(['hook', 'update', id, '--timeout', '45', '--dry']), { timeout: 45, id: id });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
json([
|
|
142
|
+
'hook', 'update', id,
|
|
143
|
+
'--headers.0.name', 'X-Renamed', '--headers.0.value', 'New Value',
|
|
144
|
+
'--header', '{"name":"X-Added","value":"Added Value"}'
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
await check('indexed Header edits and append preserve other settings', () => {
|
|
148
|
+
const row = json(['hook', id]);
|
|
149
|
+
assert.deepEqual(row.headers, [
|
|
150
|
+
{ name: 'X-Renamed', value: 'New Value' },
|
|
151
|
+
headers[1],
|
|
152
|
+
{ name: 'X-Added', value: 'Added Value' }
|
|
153
|
+
]);
|
|
154
|
+
assert.equal(row.url, example.url);
|
|
155
|
+
assert.equal(row.body, body);
|
|
156
|
+
assert.equal(row.revision, 2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const replacementHeaders = [{ name: 'X-Only', value: 'Only Value' }];
|
|
160
|
+
json(['hook', 'update', id, '--headers', '@-'], {
|
|
161
|
+
input: JSON.stringify(replacementHeaders)
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await check('complete Header replacement from stdin', () => {
|
|
165
|
+
assert.deepEqual(json(['hook', id]).headers, replacementHeaders);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const replacementBody = '{\n "message": "replacement body"\n}\n';
|
|
169
|
+
json(['hook', 'update', id, '--body', '@-'], { input: replacementBody });
|
|
170
|
+
|
|
171
|
+
await check('JSON-looking body from stdin remains exact text', () => {
|
|
172
|
+
assert.equal(json(['hook', id]).body, replacementBody);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
json(['hook', 'update', id, '--json', '@-'], {
|
|
176
|
+
input: JSON.stringify({
|
|
177
|
+
enabled: false,
|
|
178
|
+
timeout: 15,
|
|
179
|
+
retries: 1,
|
|
180
|
+
follow: false,
|
|
181
|
+
ssl_cert_bypass: false,
|
|
182
|
+
max_per_day: 0,
|
|
183
|
+
notes: 'Updated from JSON'
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await check('complete JSON update converts native values', () => {
|
|
188
|
+
const row = json(['hook', id]);
|
|
189
|
+
assert.equal(row.enabled, false);
|
|
190
|
+
assert.equal(row.timeout, 15);
|
|
191
|
+
assert.equal(row.retries, 1);
|
|
192
|
+
assert.equal(row.follow, false);
|
|
193
|
+
assert.equal(row.max_per_day, 0);
|
|
194
|
+
assert.equal(row.notes, 'Updated from JSON');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Simulate a field added by a future xyOps release, then prove that a normal
|
|
198
|
+
// CLI update is sparse and preserves the unknown saved value.
|
|
199
|
+
await apiCall('updateWebHook', { id: id, future_property: { enabled: true } });
|
|
200
|
+
json(['hook', 'update', id, '--notes', 'Future-safe update']);
|
|
201
|
+
|
|
202
|
+
await check('sparse update preserves future server properties', () => {
|
|
203
|
+
const row = json(['hook', id]);
|
|
204
|
+
assert.equal(row.notes, 'Future-safe update');
|
|
205
|
+
assert.deepEqual(row.future_property, { enabled: true });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const beforeTest = json(['hook', id]);
|
|
209
|
+
await check('dry test previews temporary overrides without sending', () => {
|
|
210
|
+
const req = json([
|
|
211
|
+
'hook', 'test', id, '--enabled', 'true', '--timeout', '10',
|
|
212
|
+
'--header', '{"name":"X-Test-Only","value":"Yes"}', '--dry'
|
|
213
|
+
]);
|
|
214
|
+
assert.equal(req.id, id);
|
|
215
|
+
assert.equal(req.enabled, true);
|
|
216
|
+
assert.equal(req.timeout, 10);
|
|
217
|
+
assert.deepEqual(req.headers.at(-1), { name: 'X-Test-Only', value: 'Yes' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await check('live test renders the API Markdown report', () => {
|
|
221
|
+
const out = xy(['hook', 'test', id, '--enabled', 'true', '--retries', '0']);
|
|
222
|
+
assert.match(out, /WEB HOOK TEST RESULTS/);
|
|
223
|
+
assert.match(out, /Result:\s+Success \(HTTP 200 OK\)/i);
|
|
224
|
+
assert.match(out, /Method:\s+POST/i);
|
|
225
|
+
assert.match(out, /Response:\s+HTTP 200 OK/i);
|
|
226
|
+
assert.doesNotMatch(out, /\*\*Method:\*\*/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await check('live test JSON returns code, description, and Markdown details', () => {
|
|
230
|
+
const result = json(['webhook', 'test', id, '--enabled', 'true', '--retries', '0']);
|
|
231
|
+
assert.equal(result.code, 0);
|
|
232
|
+
assert.match(result.description, /HTTP 200 OK/);
|
|
233
|
+
assert.match(result.details, /\*\*Response:\*\* HTTP 200 OK/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await check('temporary test overrides do not update the saved Hook', () => {
|
|
237
|
+
assert.deepEqual(json(['hook', id]), beforeTest);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const revision = json(['hook', id]).revision;
|
|
241
|
+
await check('unconfirmed delete shows target and warning toast', () => {
|
|
242
|
+
for (const args of [['delete', id], ['delete', id, '--confirm', 'false']]) {
|
|
243
|
+
const out = xy(['hook', ...args]);
|
|
244
|
+
assert.match(out, /DELETE WEB HOOK/);
|
|
245
|
+
assert.match(out, /⚠️[\s\S]*Please confirm the Web Hook delete/);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const rejected = [
|
|
250
|
+
['update', id],
|
|
251
|
+
['update', title, '--notes', 'Wrong selector'],
|
|
252
|
+
['update', id, '--id', 'different', '--notes', 'Wrong ID'],
|
|
253
|
+
['update', id, '--title', ' '],
|
|
254
|
+
['update', id, '--url', 'ftp://example.com'],
|
|
255
|
+
['update', id, '--method', 'OPTIONS'],
|
|
256
|
+
['update', id, '--headers', '{}'],
|
|
257
|
+
['update', id, '--headers', '[{"name":"Bad Header","value":"x"}]'],
|
|
258
|
+
['update', id, '--headers', '[{"name":"X-Test","value":"bad\\nvalue"}]'],
|
|
259
|
+
['update', id, '--headers', '[{"name":"X-Test","value":"x","extra":true}]'],
|
|
260
|
+
['update', id, '--headers.99.name', 'X-Bad'],
|
|
261
|
+
['update', id, '--headers.__proto__.x', 'Bad'],
|
|
262
|
+
['update', id, '--timeout', '-1'],
|
|
263
|
+
['update', id, '--retries', '1000'],
|
|
264
|
+
['update', id, '--max_per_day', '1.5'],
|
|
265
|
+
['update', id, '--follow', 'perhaps'],
|
|
266
|
+
['update', id, '--revision', '99'],
|
|
267
|
+
['update', id, '--notess', 'Typo'],
|
|
268
|
+
['test', title],
|
|
269
|
+
['test', id, '--unknown', 'x'],
|
|
270
|
+
['delete', title, '--confirm'],
|
|
271
|
+
['delete', id, '--confirm', 'maybe']
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
for (const args of rejected) {
|
|
275
|
+
await check('reject Hook ' + args.slice(0, 4).join(' '), () => xy(['hook', ...args], { fail: true }));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await check('rejected requests leave revision unchanged', () => assert.equal(json(['hook', id]).revision, revision));
|
|
279
|
+
|
|
280
|
+
await check('duplicate Web Hook ID error is surfaced', () => {
|
|
281
|
+
assert.match(xy(['hook', 'create', '--id', id, '--title', title, '--url', example.url], { fail: true }), /already exists/i);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const auto = json(['hook', 'create', '--title', title + ' Automatic', '--url', example.url]);
|
|
285
|
+
ids.add(auto.id);
|
|
286
|
+
await check('automatic ID uses Web Hook prefix', () => assert.match(auto.id, /^w/));
|
|
287
|
+
|
|
288
|
+
const humanID = id + '_human';
|
|
289
|
+
ids.add(humanID);
|
|
290
|
+
await check('human creation prints suggested commands', () => {
|
|
291
|
+
assert.match(xy(['hook', 'create', '--id', humanID, '--title', title + ' Human', '--url', example.url]), /Other Commands:/);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Remove auxiliary definitions before the final primary delete checks.
|
|
295
|
+
for (const hookID of Array.from(ids)) {
|
|
296
|
+
if (hookID === id) continue;
|
|
297
|
+
json(['hook', 'delete', hookID, '--confirm']);
|
|
298
|
+
ids.delete(hookID);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await check('dry delete request contains only the ID', () => {
|
|
302
|
+
assert.deepEqual(json(['hook', 'delete', id, '--confirm', '--dry']), { id: id });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await check('dry delete leaves the Hook intact', () => assert.equal(json(['hook', id]).id, id));
|
|
306
|
+
|
|
307
|
+
json(['hook', 'delete', id, '--confirm']);
|
|
308
|
+
ids.delete(id);
|
|
309
|
+
|
|
310
|
+
await check('confirmed delete removes the Hook', () => assert.deepEqual(json(['hooks', '--id', id]), []));
|
|
311
|
+
|
|
312
|
+
await check('get after deletion fails', () => xy(['hook', id], { fail: true }));
|
|
313
|
+
|
|
314
|
+
for (const topic of ['hooks', 'hook', 'hook list', 'hook get', 'hook create', 'hook update', 'hook test', 'hook delete']) {
|
|
315
|
+
await check('help ' + topic, () => assert.ok(xy(['help', ...topic.split(' ')]).length > 100));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await check('existing Web Hooks remain unchanged', async () => {
|
|
319
|
+
const now = (await apiCall('getWebHooks')).rows;
|
|
320
|
+
for (const hook of original) {
|
|
321
|
+
const current = now.find(item => item.id === hook.id);
|
|
322
|
+
assert.ok(current, 'Existing Web Hook still present: ' + hook.id);
|
|
323
|
+
assert.equal(JSON.stringify(current), JSON.stringify(hook), 'Existing Web Hook unchanged: ' + hook.id);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
await cleanupFixtures([
|
|
329
|
+
{ list: 'web_hooks', method: 'deleteWebHook', match: item => ids.has(item.id) || item.title.startsWith(title) }
|
|
330
|
+
]);
|
|
331
|
+
}
|
|
332
|
+
});
|
package/test.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run the integration suites one at a time using Node's native test runner.
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const Path = require('node:path');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
const { loadTestConfig } = require('./test/helpers/common.js');
|
|
7
|
+
|
|
8
|
+
const directory = Path.join(__dirname, 'test');
|
|
9
|
+
const suites = fs.readdirSync(directory).filter(name => name.endsWith('.test.js')).sort();
|
|
10
|
+
const names = suites.map(name => name.replace(/\.test\.js$/, ''));
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
if (args.includes('--help') || args.includes('--list')) {
|
|
14
|
+
console.log('Usage: node test.js [SUITE ...]');
|
|
15
|
+
console.log('Runs all suites sequentially by default. Requires the local xyOps development server.');
|
|
16
|
+
console.log('\nAvailable suites:\n ' + names.join('\n '));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
try {
|
|
20
|
+
// Resolve names from the inventory rather than accepting arbitrary paths.
|
|
21
|
+
const selected = args.length ? [...new Set(args)] : names;
|
|
22
|
+
for (const name of selected) {
|
|
23
|
+
if (!names.includes(name)) throw new Error('Unknown suite: ' + name + '. Use --list to see available suites.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
loadTestConfig();
|
|
27
|
+
const files = selected.map(name => Path.join(directory, name + '.test.js'));
|
|
28
|
+
|
|
29
|
+
// Native file isolation keeps SDK/config/global state independent. A failed
|
|
30
|
+
// suite does not prevent the remaining suites from running and reporting.
|
|
31
|
+
const child = spawn(process.execPath, ['--test', '--test-concurrency=1', ...files], {
|
|
32
|
+
cwd: __dirname,
|
|
33
|
+
stdio: 'inherit'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
child.on('error', error => {
|
|
37
|
+
console.error('Could not start the test runner: ' + error.message);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
child.on('exit', (code, signal) => {
|
|
42
|
+
process.exitCode = signal ? 1 : (code ?? 1);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error(error.message);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
}
|