@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,303 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { loadTestConfig, createCheck, xy, json, call: apiCall } = require('./helpers/common.js');
|
|
4
|
+
|
|
5
|
+
test('servers', async t => {
|
|
6
|
+
loadTestConfig();
|
|
7
|
+
const check = createCheck(t);
|
|
8
|
+
|
|
9
|
+
// These chapters are read-only except for short-lived bootstrap tokens. No
|
|
10
|
+
// generated installation command is ever executed by this suite.
|
|
11
|
+
const multiple = await apiCall('getMultiple', {
|
|
12
|
+
lists: ['groups'],
|
|
13
|
+
jobs: 1,
|
|
14
|
+
alerts: 1,
|
|
15
|
+
servers: 1,
|
|
16
|
+
serverCache: 1
|
|
17
|
+
});
|
|
18
|
+
const active = Object.values(multiple.servers || {});
|
|
19
|
+
const cached = Object.values(multiple.serverCache || {});
|
|
20
|
+
assert.ok(active.length, 'Server tests require at least one connected Server');
|
|
21
|
+
|
|
22
|
+
await check('active list combines current and cached Servers without duplicates', () => {
|
|
23
|
+
const rows = json(['servers']);
|
|
24
|
+
const ids = rows.map(row => row.id);
|
|
25
|
+
assert.equal(new Set(ids).size, ids.length);
|
|
26
|
+
active.forEach(server => assert.ok(ids.includes(server.id), 'Connected Server appears in list'));
|
|
27
|
+
cached.filter(server => !active.some(item => item.id == server.id)).forEach(server => {
|
|
28
|
+
assert.ok(ids.includes(server.id), 'Recently offline Server appears in list');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await check('active list is sorted by label or hostname', () => {
|
|
33
|
+
const rows = json(['servers']);
|
|
34
|
+
const labels = rows.map(row => String(row.title || row.hostname || row.id).toLowerCase());
|
|
35
|
+
assert.ok(labels.every((label, idx) => !idx || labels[idx - 1].localeCompare(label) <= 0));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await check('active list supports positional and partial IP filters', () => {
|
|
39
|
+
const server = active[0];
|
|
40
|
+
const by_id = json(['servers', server.id]);
|
|
41
|
+
assert.ok(by_id.length && by_id.every(row => row.id.includes(server.id)));
|
|
42
|
+
|
|
43
|
+
const ip = String((server.info && server.info.ip) || server.ip || '');
|
|
44
|
+
assert.ok(ip, 'Connected Server has an IP address');
|
|
45
|
+
const partial = ip.slice(0, Math.max(1, Math.floor(ip.length / 2)));
|
|
46
|
+
const by_ip = json(['servers', '--ip', partial]);
|
|
47
|
+
assert.ok(by_ip.length, 'Partial IP filter finds at least one Server');
|
|
48
|
+
assert.ok(by_ip.every(row => String((row.info && row.info.ip) || row.ip || '').toLowerCase().includes(partial.toLowerCase())));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await check('active list supports OS and online filters', () => {
|
|
52
|
+
const server = active[0];
|
|
53
|
+
const os = String((server.info && server.info.os && server.info.os.platform) || (server.info && server.info.platform) || '');
|
|
54
|
+
assert.ok(os, 'Connected Server has an OS platform');
|
|
55
|
+
const rows = json(['servers', '--os', os.toLowerCase(), '--online', 'true']);
|
|
56
|
+
assert.ok(rows.some(row => row.id == server.id));
|
|
57
|
+
assert.ok(rows.every(row => !row.offline));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await check('active list human output supports pagination and suggestions', () => {
|
|
61
|
+
const out = xy(['servers', '--limit', '1', '--page', '1']);
|
|
62
|
+
assert.match(out, /ACTIVE SERVERS/i);
|
|
63
|
+
assert.match(out, /Other Commands:/);
|
|
64
|
+
if ((active.length + cached.length) > 1) assert.match(out, /page 1 of/i);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await check('active list rejects misspelled filters with a suggestion', () => {
|
|
68
|
+
const out = xy(['servers', '--platfrm', 'linux'], { fail: true });
|
|
69
|
+
assert.match(out, /Unsupported Server list option: "--platfrm"/);
|
|
70
|
+
assert.match(out, /Did you mean "--platform"\?/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await check('dry installer request preserves custom Server metadata', () => {
|
|
74
|
+
const args = [
|
|
75
|
+
'server', 'add', '--platform', 'linux', '--title', 'CLI Bootstrap Test',
|
|
76
|
+
'--enabled', 'false', '--icon', 'mdi-fire-truck', '--expires', '3600', '--dry'
|
|
77
|
+
];
|
|
78
|
+
if (multiple.groups && multiple.groups.length) args.push('--groups', multiple.groups[0].id);
|
|
79
|
+
const request = json(args);
|
|
80
|
+
assert.equal(request.title, 'CLI Bootstrap Test');
|
|
81
|
+
assert.equal(request.enabled, false);
|
|
82
|
+
assert.equal(request.icon, 'fire-truck');
|
|
83
|
+
assert.equal(request.expires, 3600);
|
|
84
|
+
if (multiple.groups && multiple.groups.length) assert.deepEqual(request.groups, [multiple.groups[0].id]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await check('all installer platforms expand into complete one-line commands', () => {
|
|
88
|
+
const expectations = {
|
|
89
|
+
linux: ['curl ', '/satellite/install?t=', 'sudo sh'],
|
|
90
|
+
macos: ['curl ', '/satellite/install?t=', 'os=macos', 'sudo sh'],
|
|
91
|
+
windows: ['powershell', '/satellite/install?t=', 'os=windows'],
|
|
92
|
+
docker: ['docker run', '/satellite/config?t=', 'ghcr.io']
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const platform of Object.keys(expectations)) {
|
|
96
|
+
const result = json(['server', 'add', '--platform', platform, '--expires', '60']);
|
|
97
|
+
assert.equal(result.platform, platform);
|
|
98
|
+
assert.equal(typeof(result.command), 'string');
|
|
99
|
+
assert.ok(!result.command.includes('\n'), platform + ' installer stays on one line');
|
|
100
|
+
assert.ok(!result.command.match(/\[(?:base_url|token|image|version|unique)\]/), platform + ' installer expands every macro');
|
|
101
|
+
expectations[platform].forEach(text => {
|
|
102
|
+
assert.ok(result.command.toLowerCase().includes(text.toLowerCase()), platform + ' installer contains ' + text);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await check('installer rejects unsupported platforms and options', () => {
|
|
108
|
+
const platform = xy(['server', 'add', '--platform', 'solaris'], { fail: true });
|
|
109
|
+
assert.match(platform, /Server platform must be one of: linux, macos, windows, docker/);
|
|
110
|
+
|
|
111
|
+
const option = xy(['server', 'add', '--platform', 'linux', '--titel', 'Typo'], { fail: true });
|
|
112
|
+
assert.match(option, /Unsupported Server add option: --titel/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await check('historical search supports singular and plural routes', () => {
|
|
116
|
+
const singular = json(['server', 'search', active[0].hostname, '--limit', '5']);
|
|
117
|
+
const plural = json(['servers', 'search', active[0].hostname, '--limit', '5']);
|
|
118
|
+
assert.ok(singular.some(row => row.id == active[0].id));
|
|
119
|
+
assert.deepEqual(plural.map(row => row.id), singular.map(row => row.id));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await check('historical search supports indexed named fields', () => {
|
|
123
|
+
const server = active[0];
|
|
124
|
+
const platform = String((server.info && server.info.os && server.info.os.platform) || (server.info && server.info.platform) || '').toLowerCase();
|
|
125
|
+
const arch = String((server.info && server.info.os && server.info.os.arch) || (server.info && server.info.arch) || '').toLowerCase();
|
|
126
|
+
const rows = json(['server', 'search', '--os_platform', platform, '--os_arch', arch, '--limit', '5']);
|
|
127
|
+
assert.ok(rows.length, 'Indexed OS search returns Servers');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await check('historical search human output supports pagination and suggestions', () => {
|
|
131
|
+
const out = xy(['server', 'search', '--limit', '1', '--page', '1']);
|
|
132
|
+
assert.match(out, /ALL HISTORICAL SERVERS/i);
|
|
133
|
+
assert.match(out, /Other Commands:/);
|
|
134
|
+
assert.match(out, /page 1 of/i);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await check('historical search rejects misspelled fields with a suggestion', () => {
|
|
138
|
+
const out = xy(['server', 'search', '--os_platfrm', 'linux'], { fail: true });
|
|
139
|
+
assert.match(out, /Unsupported Server search option: "--os_platfrm"/);
|
|
140
|
+
assert.match(out, /Did you mean "--os_platform"\?/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await check('Server view returns live snapshot data as JSON', () => {
|
|
144
|
+
// Prefer a labeled Server so title lookup is covered when available, but
|
|
145
|
+
// keep this test valid for installations where no Server has a title.
|
|
146
|
+
const server = active.find(item => item.title) || active[0];
|
|
147
|
+
const result = json(['server', server.id]);
|
|
148
|
+
const explicit = json(['server', 'get', server.id]);
|
|
149
|
+
const hostname = json(['server', server.hostname]);
|
|
150
|
+
assert.equal(result.server.id, server.id);
|
|
151
|
+
assert.equal(explicit.server.id, server.id);
|
|
152
|
+
assert.equal(hostname.server.id, server.id);
|
|
153
|
+
if (server.title) {
|
|
154
|
+
const title = json(['server', server.title]);
|
|
155
|
+
assert.equal(title.server.id, server.id);
|
|
156
|
+
}
|
|
157
|
+
assert.equal(result.online, true);
|
|
158
|
+
assert.ok(result.data && result.data.data, 'Server view includes monitoring data');
|
|
159
|
+
assert.ok(Array.isArray(result.quickmon), 'Server view includes a Quick Look collection');
|
|
160
|
+
assert.ok(Array.isArray(result.monitors), 'Server view includes a monitor timeline collection');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await check('Server view renders standard sections and expansion hints', () => {
|
|
164
|
+
const out = xy(['server', active[0].id, '--limit', '2']);
|
|
165
|
+
assert.match(out, /LIVE SERVER VIEW - Real-time/);
|
|
166
|
+
assert.ok(out.indexOf('LIVE SERVER VIEW - Real-time') < out.indexOf('SERVER SUMMARY'));
|
|
167
|
+
assert.match(out, /SERVER SUMMARY/);
|
|
168
|
+
assert.match(out, /SERVER ALERTS/);
|
|
169
|
+
assert.match(out, /SERVER JOBS/);
|
|
170
|
+
assert.match(out, /MEMORY DETAILS/);
|
|
171
|
+
assert.match(out, /CPU DETAILS/);
|
|
172
|
+
assert.match(out, /SERVER MONITOR SUMMARY/);
|
|
173
|
+
assert.match(out, /SERVER MONITORS\n \(Shown in verbose mode, or add --monitors\.\)/);
|
|
174
|
+
assert.match(out, /SERVER PROCESSES\n \(Shown in verbose mode, or add --processes\.\)/);
|
|
175
|
+
assert.match(out, /NETWORK CONNECTIONS\n \(Shown in verbose mode, or add --connections\.\)/);
|
|
176
|
+
assert.match(out, /NETWORK INTERFACES/);
|
|
177
|
+
assert.match(out, /FILESYSTEMS/);
|
|
178
|
+
assert.match(out, /Other Commands:/);
|
|
179
|
+
assert.match(out, new RegExp('xy server ' + active[0].id + ' --pid 1234'));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await check('Server view loads opt-in monitor timeline data', () => {
|
|
183
|
+
const result = json(['server', active[0].id, '--monitors']);
|
|
184
|
+
assert.ok(Array.isArray(result.monitors));
|
|
185
|
+
assert.ok(result.monitors.length, 'Connected Server has hourly monitor data');
|
|
186
|
+
assert.ok(result.monitors.every(row => row.date && row.totals));
|
|
187
|
+
|
|
188
|
+
const out = xy(['server', active[0].id, '--monitors', '--limit', '1']);
|
|
189
|
+
assert.match(out, /SERVER MONITORS - LAST HOUR/);
|
|
190
|
+
assert.doesNotMatch(out, /SERVER MONITORS\n \(Shown in verbose mode/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await check('Server view renders process and connection tables with shared pagination', () => {
|
|
194
|
+
const out = xy(['server', active[0].id, '--processes', '--connections', '--limit', '1', '--page', '1']);
|
|
195
|
+
assert.match(out, /SERVER PROCESSES/);
|
|
196
|
+
assert.doesNotMatch(out, /SERVER PROCESSES\n \(Shown in verbose mode/);
|
|
197
|
+
assert.match(out, /NETWORK CONNECTIONS/);
|
|
198
|
+
assert.doesNotMatch(out, /NETWORK CONNECTIONS\n \(Shown in verbose mode/);
|
|
199
|
+
assert.match(out, /NETWORK INTERFACES/);
|
|
200
|
+
assert.match(out, /FILESYSTEMS/);
|
|
201
|
+
assert.match(out, /page 1 of/i);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const snapshot = await apiCall('getServer', { id: active[0].id });
|
|
205
|
+
const process = snapshot.data && snapshot.data.data && snapshot.data.data.processes && snapshot.data.data.processes.list && snapshot.data.data.processes.list[0];
|
|
206
|
+
if (process) {
|
|
207
|
+
await check('process detail is a focused page with its family tree', () => {
|
|
208
|
+
const out = xy(['server', active[0].id, '--pid', String(process.pid)]);
|
|
209
|
+
const serverName = active[0].title || active[0].hostname;
|
|
210
|
+
const escapedServerName = serverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
211
|
+
assert.match(out, /PROCESS DETAILS/);
|
|
212
|
+
assert.match(out, /PROCESS FAMILY/);
|
|
213
|
+
assert.match(out, new RegExp('Server:\\s+' + escapedServerName));
|
|
214
|
+
assert.match(out, new RegExp('Process ID:\\s+' + process.pid));
|
|
215
|
+
assert.match(out, /Command:/);
|
|
216
|
+
assert.doesNotMatch(out, /SERVER SUMMARY/);
|
|
217
|
+
assert.doesNotMatch(out, /LIVE SERVER VIEW|OFFLINE SERVER/);
|
|
218
|
+
assert.doesNotMatch(out, /QUICK LOOK/);
|
|
219
|
+
assert.doesNotMatch(out, /MEMORY DETAILS/);
|
|
220
|
+
assert.doesNotMatch(out, /Other Commands:/);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await check('Server view rejects unknown options and malformed process IDs', () => {
|
|
225
|
+
const option = xy(['server', active[0].id, '--proceses'], { fail: true });
|
|
226
|
+
assert.match(option, /Unsupported Server view option: "--proceses"/);
|
|
227
|
+
assert.match(option, /Did you mean "--processes"\?/);
|
|
228
|
+
|
|
229
|
+
const pid = xy(['server', active[0].id, '--pid', 'abc'], { fail: true });
|
|
230
|
+
assert.match(pid, /Server process ID must be a positive integer/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const latest = await apiCall('getLatestMonitorData', { server: active[0].id, sys: 'hourly', limit: 1 });
|
|
234
|
+
assert.ok(latest.rows && latest.rows.length, 'Server history tests require recent monitoring data');
|
|
235
|
+
const historyDate = new Date(latest.rows[latest.rows.length - 1].date * 1000);
|
|
236
|
+
const pad = number => String(number).padStart(2, '0');
|
|
237
|
+
const historyParts = {
|
|
238
|
+
year: String(historyDate.getFullYear()),
|
|
239
|
+
month: historyDate.getFullYear() + '/' + pad(historyDate.getMonth() + 1),
|
|
240
|
+
day: historyDate.getFullYear() + '/' + pad(historyDate.getMonth() + 1) + '/' + pad(historyDate.getDate()),
|
|
241
|
+
hour: historyDate.getFullYear() + '/' + pad(historyDate.getMonth() + 1) + '/' + pad(historyDate.getDate()) + '/' + pad(historyDate.getHours())
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
await check('historical view supports all four fixed zoom levels', () => {
|
|
245
|
+
const cases = [
|
|
246
|
+
[historyParts.hour, 'hourly'],
|
|
247
|
+
[historyParts.day, 'daily'],
|
|
248
|
+
[historyParts.month, 'monthly'],
|
|
249
|
+
[historyParts.year, 'yearly']
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
for (const [date, mode] of cases) {
|
|
253
|
+
const result = json(['server', 'history', active[0].id, date, '--limit', '1']);
|
|
254
|
+
assert.equal(result.server.id, active[0].id);
|
|
255
|
+
assert.equal(result.range.mode, mode);
|
|
256
|
+
assert.ok(result.range.start < result.range.end);
|
|
257
|
+
assert.ok(result.range.monitor_limit > 0);
|
|
258
|
+
assert.ok(Array.isArray(result.monitors));
|
|
259
|
+
assert.ok(Array.isArray(result.alerts.rows));
|
|
260
|
+
assert.ok(Array.isArray(result.jobs.rows));
|
|
261
|
+
assert.ok(result.alerts.rows.length <= 1);
|
|
262
|
+
assert.ok(result.jobs.rows.length <= 1);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await check('historical view accepts a local Server name', () => {
|
|
267
|
+
const serverName = active[0].title || active[0].hostname || active[0].id;
|
|
268
|
+
const result = json(['server', 'history', serverName, historyParts.hour]);
|
|
269
|
+
assert.equal(result.server.id, active[0].id);
|
|
270
|
+
assert.equal(result.range.mode, 'hourly');
|
|
271
|
+
assert.ok(result.monitors.length, 'Selected hour contains monitoring data');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await check('historical view renders summary, monitors, alerts and jobs in order', () => {
|
|
275
|
+
const out = xy(['server', 'history', active[0].hostname, historyParts.hour, '--limit', '1']);
|
|
276
|
+
const sections = [
|
|
277
|
+
out.indexOf('SERVER SUMMARY'),
|
|
278
|
+
out.indexOf('SERVER MONITORS'),
|
|
279
|
+
out.indexOf('SERVER ALERTS'),
|
|
280
|
+
out.indexOf('SERVER JOBS')
|
|
281
|
+
];
|
|
282
|
+
assert.ok(sections.every(index => index >= 0), 'Every historical section is shown');
|
|
283
|
+
assert.ok(sections.every((index, idx) => !idx || sections[idx - 1] < index), 'Historical sections are in display order');
|
|
284
|
+
assert.match(out, /Other Commands:/);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await check('historical view validates dates and options', () => {
|
|
288
|
+
const format = xy(['server', 'history', active[0].id, '2026/01/02/03/04'], { fail: true });
|
|
289
|
+
assert.match(format, /Server history date must use YYYY/);
|
|
290
|
+
|
|
291
|
+
const date = xy(['server', 'history', active[0].id, '2026/02/30'], { fail: true });
|
|
292
|
+
assert.match(date, /Invalid Server history date: 2026\/02\/30/);
|
|
293
|
+
|
|
294
|
+
const option = xy(['server', 'history', active[0].id, historyParts.day, '--bogus'], { fail: true });
|
|
295
|
+
assert.match(option, /Unsupported Server history option: "--bogus"/);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await check('Server help chapters render', () => {
|
|
299
|
+
for (const section of ['servers', 'server', 'server get', 'server history', 'server add', 'server search']) {
|
|
300
|
+
assert.match(xy(['help', ...section.split(' ')]), new RegExp('HELP: ' + section.toUpperCase()));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { loadTestConfig, createCheck, xy, json, call: apiCall } = require('./helpers/common.js');
|
|
4
|
+
|
|
5
|
+
test('snapshots', async t => {
|
|
6
|
+
loadTestConfig();
|
|
7
|
+
const check = createCheck(t);
|
|
8
|
+
|
|
9
|
+
// Snapshot tests are intentionally read-only. Reuse saved records from the
|
|
10
|
+
// local development system instead of creating and deleting test snapshots.
|
|
11
|
+
const search = await apiCall('searchSnapshots', {
|
|
12
|
+
query: '*',
|
|
13
|
+
offset: 0,
|
|
14
|
+
limit: 100,
|
|
15
|
+
sort_by: '_id',
|
|
16
|
+
sort_dir: -1,
|
|
17
|
+
verbose: true
|
|
18
|
+
});
|
|
19
|
+
const serverSnapshot = search.rows.find(item => item.type != 'group');
|
|
20
|
+
const groupSnapshot = search.rows.find(item => item.type == 'group');
|
|
21
|
+
assert.ok(serverSnapshot, 'Snapshot tests require one saved Server snapshot');
|
|
22
|
+
assert.ok(groupSnapshot, 'Snapshot tests require one saved Group snapshot');
|
|
23
|
+
|
|
24
|
+
await check('list returns snapshots in descending ID order with pagination metadata', () => {
|
|
25
|
+
const rows = json(['snapshots', '--limit', '2']);
|
|
26
|
+
assert.ok(rows.length > 0 && rows.length <= 2);
|
|
27
|
+
assert.ok(rows.every(row => row.id && row.type && row.date));
|
|
28
|
+
assert.ok(rows.every((row, idx) => !idx || rows[idx - 1].id >= row.id));
|
|
29
|
+
|
|
30
|
+
const output = xy(['snapshots', '--limit', '1']);
|
|
31
|
+
assert.match(output, /ALL SNAPSHOTS/);
|
|
32
|
+
assert.match(output, /Snapshot ID/);
|
|
33
|
+
assert.match(output, /Other Commands:/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await check('list supports source, Server and Group filters', () => {
|
|
37
|
+
const source = json(['snapshots', '--source', serverSnapshot.source, '--limit', '10']);
|
|
38
|
+
assert.ok(source.length);
|
|
39
|
+
assert.ok(source.every(row => row.source == serverSnapshot.source));
|
|
40
|
+
|
|
41
|
+
const server = json(['snapshots', '--server', serverSnapshot.server, '--limit', '10']);
|
|
42
|
+
assert.ok(server.length);
|
|
43
|
+
assert.ok(server.every(row => row.server == serverSnapshot.server));
|
|
44
|
+
|
|
45
|
+
const groupID = groupSnapshot.groups[0];
|
|
46
|
+
const group = json(['snapshots', '--group', groupID, '--limit', '10']);
|
|
47
|
+
assert.ok(group.length);
|
|
48
|
+
assert.ok(group.every(row => (row.groups || []).includes(groupID)));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await check('Server snapshot JSON includes saved and related records', () => {
|
|
52
|
+
const result = json(['snapshot', serverSnapshot.id]);
|
|
53
|
+
assert.equal(result.snapshot.id, serverSnapshot.id);
|
|
54
|
+
assert.equal(result.snapshot.type, serverSnapshot.type);
|
|
55
|
+
assert.equal(result.server.id, serverSnapshot.server);
|
|
56
|
+
assert.ok(Array.isArray(result.alerts));
|
|
57
|
+
assert.ok(Array.isArray(result.jobs));
|
|
58
|
+
assert.deepEqual(result.monitors, []);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await check('Group snapshot JSON preserves positional Server data', () => {
|
|
62
|
+
const result = json(['snapshot', groupSnapshot.id]);
|
|
63
|
+
assert.equal(result.snapshot.id, groupSnapshot.id);
|
|
64
|
+
assert.equal(result.snapshot.type, 'group');
|
|
65
|
+
assert.equal(result.group.id, groupSnapshot.group_def.id);
|
|
66
|
+
assert.equal(result.servers.length, groupSnapshot.servers.length);
|
|
67
|
+
assert.ok(Array.isArray(result.alerts));
|
|
68
|
+
assert.ok(Array.isArray(result.jobs));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await check('Server monitor history ends at the snapshot minute', () => {
|
|
72
|
+
const result = json(['snapshot', serverSnapshot.id, '--monitors']);
|
|
73
|
+
const snapshotMinute = Math.floor(result.snapshot.date / 60) * 60;
|
|
74
|
+
const dates = result.monitors.map(row => row.date);
|
|
75
|
+
assert.ok(dates.length, 'Saved Server has historical monitor rows');
|
|
76
|
+
assert.equal(Math.max(...dates), snapshotMinute);
|
|
77
|
+
assert.ok(Math.min(...dates) >= snapshotMinute - (59 * 60));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await check('Group monitor history uses the same trailing window per Server', () => {
|
|
81
|
+
const result = json(['snapshot', groupSnapshot.id, '--monitors']);
|
|
82
|
+
const snapshotMinute = Math.floor(result.snapshot.date / 60) * 60;
|
|
83
|
+
assert.equal(result.monitors.length, result.servers.length);
|
|
84
|
+
|
|
85
|
+
result.monitors.forEach(item => {
|
|
86
|
+
const dates = item.rows.map(row => row.date);
|
|
87
|
+
if (!dates.length) return;
|
|
88
|
+
assert.equal(Math.max(...dates), snapshotMinute);
|
|
89
|
+
assert.ok(Math.min(...dates) >= snapshotMinute - (59 * 60));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await check('Server human view renders saved point-in-time sections', () => {
|
|
94
|
+
const output = xy(['snapshot', serverSnapshot.id, '--limit', '1']);
|
|
95
|
+
const sections = [
|
|
96
|
+
'SERVER SNAPSHOT - POINT-IN-TIME VIEW',
|
|
97
|
+
'SNAPSHOT SUMMARY',
|
|
98
|
+
'SNAPSHOT JOBS',
|
|
99
|
+
'MEMORY DETAILS',
|
|
100
|
+
'CPU DETAILS',
|
|
101
|
+
'SERVER MONITOR SUMMARY',
|
|
102
|
+
'NETWORK INTERFACES',
|
|
103
|
+
'FILESYSTEMS'
|
|
104
|
+
];
|
|
105
|
+
sections.forEach(section => assert.match(output, new RegExp(section)));
|
|
106
|
+
if (serverSnapshot.quickmon && serverSnapshot.quickmon.length) assert.match(output, /QUICK LOOK - MINUTE LEADING TO SNAPSHOT/);
|
|
107
|
+
assert.match(output, /SNAPSHOT MONITORS/);
|
|
108
|
+
assert.match(output, /SNAPSHOT PROCESSES/);
|
|
109
|
+
assert.match(output, /SNAPSHOT NETWORK CONNECTIONS/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await check('Group human view renders saved point-in-time sections', () => {
|
|
113
|
+
const output = xy(['snapshot', groupSnapshot.id, '--limit', '1']);
|
|
114
|
+
const sections = [
|
|
115
|
+
'GROUP SNAPSHOT - POINT-IN-TIME VIEW',
|
|
116
|
+
'GROUP SNAPSHOT SUMMARY',
|
|
117
|
+
'GROUP SERVERS',
|
|
118
|
+
'SNAPSHOT JOBS',
|
|
119
|
+
'GROUP MEMORY DETAILS',
|
|
120
|
+
'GROUP CPU DETAILS',
|
|
121
|
+
'GROUP MONITOR SUMMARY'
|
|
122
|
+
];
|
|
123
|
+
sections.forEach(section => assert.match(output, new RegExp(section)));
|
|
124
|
+
if ((groupSnapshot.quickmons || []).some(rows => rows && rows.length)) assert.match(output, /GROUP QUICK LOOK - MINUTE LEADING TO SNAPSHOT/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await check('optional process and connection tables expand independently', () => {
|
|
128
|
+
const server = xy(['snapshot', serverSnapshot.id, '--processes', '--connections', '--limit', '1']);
|
|
129
|
+
assert.match(server, /SERVER PROCESSES/);
|
|
130
|
+
assert.match(server, /NETWORK CONNECTIONS/);
|
|
131
|
+
|
|
132
|
+
const group = xy(['snapshot', groupSnapshot.id, '--processes', '--connections', '--limit', '1']);
|
|
133
|
+
assert.match(group, /GROUP PROCESSES/);
|
|
134
|
+
assert.match(group, /GROUP NETWORK CONNECTIONS/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await check('delete without confirmation is a safe preview', async () => {
|
|
138
|
+
const before = await apiCall('searchSnapshots', { query: '#id:' + serverSnapshot.id, verbose: true, limit: 1 });
|
|
139
|
+
const output = xy(['snapshot', serverSnapshot.id, '--delete']);
|
|
140
|
+
const after = await apiCall('searchSnapshots', { query: '#id:' + serverSnapshot.id, verbose: true, limit: 1 });
|
|
141
|
+
assert.equal(before.rows.length, 1);
|
|
142
|
+
assert.equal(after.rows.length, 1);
|
|
143
|
+
assert.match(output, /DELETE SNAPSHOT/);
|
|
144
|
+
assert.match(output, /--confirm/);
|
|
145
|
+
assert.match(output, /continue/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await check('commands reject unsupported searches and suggest close options', () => {
|
|
149
|
+
const keyword = xy(['snapshots', 'manual'], { fail: true });
|
|
150
|
+
assert.match(keyword, /HELP: SNAPSHOTS/);
|
|
151
|
+
|
|
152
|
+
const typo = xy(['snapshots', '--sorce', 'user'], { fail: true });
|
|
153
|
+
assert.match(typo, /Did you mean "--source"\?/);
|
|
154
|
+
|
|
155
|
+
const source = xy(['snapshots', '--source', 'manual'], { fail: true });
|
|
156
|
+
assert.match(source, /Snapshot source must be one of/);
|
|
157
|
+
|
|
158
|
+
const missing = xy(['snapshot', 'notarealsnapshotid'], { fail: true });
|
|
159
|
+
assert.match(missing, /Could not find Snapshot/);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await check('Snapshot help chapters render', () => {
|
|
163
|
+
for (const section of ['snapshots', 'snapshot']) {
|
|
164
|
+
assert.match(xy(['help', section]), new RegExp('HELP: ' + section.toUpperCase()));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|