datagrok-tools 6.1.9 → 6.1.11

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,406 @@
1
+ "use strict";
2
+
3
+ var _vitest = require("vitest");
4
+ var _nodeDapi = require("../utils/node-dapi");
5
+ var _testUtils = require("../utils/test-utils");
6
+ /**
7
+ * Integration tests for NodeApiClient, NodeDapi, and all data sources.
8
+ * Requires a running Datagrok server configured in ~/.grok/config.yaml.
9
+ *
10
+ * Environment variables:
11
+ * HOST Server alias or URL (default: config default)
12
+ * GROK_TEST_FUNC Function to test with functions.run() (default: System:GetCurrentUser)
13
+ * GROK_TEST_FILES File path to test with files.list() (default: System:AppData)
14
+ *
15
+ * Run: npm run test:integration
16
+ * Run against a specific server: HOST=dev npm run test:integration
17
+ */
18
+
19
+ // ─── Config ──────────────────────────────────────────────────────────────────
20
+
21
+ const HOST = process.env['HOST'] ?? '';
22
+ const TEST_FUNC = process.env['GROK_TEST_FUNC'] ?? 'System:GetCurrentUser';
23
+ const TEST_FILES_PATH = process.env['GROK_TEST_FILES'] ?? 'System:AppData';
24
+
25
+ // ─── Setup ───────────────────────────────────────────────────────────────────
26
+
27
+ let client;
28
+ let dapi;
29
+ let offline = false;
30
+
31
+ // Pre-fetched IDs to avoid redundant round trips in find() tests
32
+ const seed = {};
33
+ (0, _vitest.beforeAll)(async () => {
34
+ try {
35
+ const {
36
+ url,
37
+ key
38
+ } = (0, _testUtils.getDevKey)(HOST);
39
+ client = await _nodeDapi.NodeApiClient.login(url, key);
40
+ dapi = new _nodeDapi.NodeDapi(client);
41
+
42
+ // Pre-seed IDs — each is best-effort; individual tests skip gracefully if absent
43
+ const [users, groups, functions, connections, queries, scripts, packages, reports] = await Promise.allSettled([dapi.users.by(1).list(), dapi.groups.by(1).list(), dapi.functions.by(10).list(),
44
+ // Fetch more to find a callable function
45
+ dapi.connections.by(1).list(), dapi.queries.by(1).list(), dapi.scripts.by(1).list(), dapi.packages.by(1).list(), dapi.reports.by(1).list()]);
46
+ if (users.status === 'fulfilled' && users.value.length) seed.userId = users.value[0].id;
47
+ if (groups.status === 'fulfilled' && groups.value.length) seed.groupId = groups.value[0].id;
48
+ if (functions.status === 'fulfilled' && functions.value.length) {
49
+ seed.functionId = functions.value[0].id;
50
+ // Find a callable function: prefer env var override, then any nqName we can discover
51
+ if (process.env['GROK_TEST_FUNC']) seed.testFuncName = process.env['GROK_TEST_FUNC'];else {
52
+ const fn = functions.value.find(f => f.nqName);
53
+ seed.testFuncName = fn?.nqName;
54
+ }
55
+ }
56
+ if (connections.status === 'fulfilled' && connections.value.length) seed.connectionId = connections.value[0].id;
57
+ if (queries.status === 'fulfilled' && queries.value.length) seed.queryId = queries.value[0].id;
58
+ if (scripts.status === 'fulfilled' && scripts.value.length) seed.scriptId = scripts.value[0].id;
59
+ if (packages.status === 'fulfilled' && packages.value.length) seed.packageId = packages.value[0].id;
60
+ if (reports.status === 'fulfilled' && reports.value.length) seed.reportId = reports.value[0].id;
61
+ } catch (err) {
62
+ console.warn(`[integration] Server unreachable — all tests will be skipped: ${err}`);
63
+ offline = true;
64
+ }
65
+ });
66
+
67
+ /** Wraps a test so it shows as skipped when the server is offline. */
68
+ function stest(name, fn) {
69
+ return (0, _vitest.it)(name, async ctx => {
70
+ if (offline) {
71
+ ctx.skip();
72
+ return;
73
+ }
74
+ await fn();
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Like stest, but also skips gracefully when the endpoint returns 404.
80
+ * Use for entities whose /public/v1/ endpoint may not exist on all server versions.
81
+ */
82
+ function stestEndpoint(name, fn) {
83
+ return stest(name, async () => {
84
+ try {
85
+ await fn();
86
+ } catch (e) {
87
+ if (e.message === 'Not Found') {
88
+ console.warn(`[integration] ${name}: endpoint not available on this server`);
89
+ return;
90
+ }
91
+ throw e;
92
+ }
93
+ });
94
+ }
95
+
96
+ // ─── NodeApiClient ────────────────────────────────────────────────────────────
97
+
98
+ (0, _vitest.describe)('NodeApiClient', () => {
99
+ stest('login() returns a client with a non-empty token', async () => {
100
+ const {
101
+ url,
102
+ key
103
+ } = (0, _testUtils.getDevKey)(HOST);
104
+ const c = await _nodeDapi.NodeApiClient.login(url, key);
105
+ (0, _vitest.expect)(c.token).toBeTruthy();
106
+ (0, _vitest.expect)(typeof c.token).toBe('string');
107
+ });
108
+ stest('login() throws on an invalid dev key', async () => {
109
+ const {
110
+ url
111
+ } = (0, _testUtils.getDevKey)(HOST);
112
+ await (0, _vitest.expect)(_nodeDapi.NodeApiClient.login(url, 'invalid-key-xyz')).rejects.toThrow();
113
+ });
114
+ stest('get() returns parsed JSON for a known endpoint', async () => {
115
+ const result = await client.get('/public/v1/users?limit=1');
116
+ (0, _vitest.expect)(Array.isArray(result)).toBe(true);
117
+ });
118
+ stest('request() propagates HTTP errors as structured exceptions', async () => {
119
+ await (0, _vitest.expect)(client.get('/public/v1/nonexistent-entity-xyz')).rejects.toThrow();
120
+ });
121
+ });
122
+
123
+ // ─── users ────────────────────────────────────────────────────────────────────
124
+
125
+ (0, _vitest.describe)('users', () => {
126
+ stest('list() returns an array', async () => {
127
+ const users = await dapi.users.list();
128
+ (0, _vitest.expect)(Array.isArray(users)).toBe(true);
129
+ });
130
+ stest('list() results have id and login fields', async () => {
131
+ const users = await dapi.users.by(5).list();
132
+ (0, _vitest.expect)(users.length).toBeGreaterThan(0);
133
+ for (const u of users) (0, _vitest.expect)(u).toMatchObject({
134
+ id: _vitest.expect.any(String),
135
+ login: _vitest.expect.any(String)
136
+ });
137
+ });
138
+ stest('count() returns a non-negative integer', async () => {
139
+ const n = await dapi.users.count();
140
+ (0, _vitest.expect)(typeof n).toBe('number');
141
+ (0, _vitest.expect)(n).toBeGreaterThanOrEqual(0);
142
+ });
143
+ stest('filter() narrows results', async () => {
144
+ const all = await dapi.users.count();
145
+ const filtered = await dapi.users.filter('admin').count();
146
+ (0, _vitest.expect)(filtered).toBeLessThanOrEqual(all);
147
+ });
148
+ stest('by() passes limit parameter and returns an array', async () => {
149
+ const users = await dapi.users.by(2).list();
150
+ (0, _vitest.expect)(Array.isArray(users)).toBe(true);
151
+ // Server may not honor small limits, but the call must succeed
152
+ (0, _vitest.expect)(users.length).toBeGreaterThanOrEqual(0);
153
+ });
154
+ stest('page() advances the offset', async () => {
155
+ const page0 = await dapi.users.by(5).page(0).list();
156
+ const page1 = await dapi.users.by(5).page(1).list();
157
+ // If enough users exist, pages should differ
158
+ if (page0.length === 5 && page1.length > 0) (0, _vitest.expect)(page0[0].id).not.toBe(page1[0].id);
159
+ });
160
+ stest('order() accepts field and direction without throwing', async () => {
161
+ const asc = await dapi.users.by(5).order('login').list();
162
+ const desc = await dapi.users.by(5).order('login', true).list();
163
+ (0, _vitest.expect)(Array.isArray(asc)).toBe(true);
164
+ (0, _vitest.expect)(Array.isArray(desc)).toBe(true);
165
+ });
166
+ stest('find() retrieves a user by ID', async () => {
167
+ if (!seed.userId) return;
168
+ const user = await dapi.users.find(seed.userId);
169
+ (0, _vitest.expect)(user.id).toBe(seed.userId);
170
+ (0, _vitest.expect)(user).toHaveProperty('login');
171
+ });
172
+ stest('delete() throws for a non-existent ID', async () => {
173
+ await (0, _vitest.expect)(dapi.users.delete('non-existent-id-xyz')).rejects.toThrow();
174
+ });
175
+ });
176
+
177
+ // ─── groups ───────────────────────────────────────────────────────────────────
178
+
179
+ (0, _vitest.describe)('groups', () => {
180
+ stest('list() returns an array with id and name', async () => {
181
+ const groups = await dapi.groups.by(5).list();
182
+ (0, _vitest.expect)(Array.isArray(groups)).toBe(true);
183
+ for (const g of groups) (0, _vitest.expect)(g).toMatchObject({
184
+ id: _vitest.expect.any(String),
185
+ name: _vitest.expect.any(String)
186
+ });
187
+ });
188
+ stest('count() returns a non-negative integer', async () => {
189
+ const n = await dapi.groups.count();
190
+ (0, _vitest.expect)(n).toBeGreaterThanOrEqual(0);
191
+ });
192
+ stest('find() retrieves a group by ID', async () => {
193
+ if (!seed.groupId) return;
194
+ const group = await dapi.groups.find(seed.groupId);
195
+ (0, _vitest.expect)(group.id).toBe(seed.groupId);
196
+ });
197
+ });
198
+
199
+ // ─── functions ────────────────────────────────────────────────────────────────
200
+
201
+ (0, _vitest.describe)('functions', () => {
202
+ stest('list() returns an array with id and name', async () => {
203
+ const fns = await dapi.functions.by(5).list();
204
+ (0, _vitest.expect)(Array.isArray(fns)).toBe(true);
205
+ (0, _vitest.expect)(fns.length).toBeGreaterThan(0);
206
+ for (const f of fns) (0, _vitest.expect)(f).toMatchObject({
207
+ id: _vitest.expect.any(String),
208
+ name: _vitest.expect.any(String)
209
+ });
210
+ });
211
+ stest('filter() narrows by name prefix', async () => {
212
+ const all = await dapi.functions.count();
213
+ const filtered = await dapi.functions.filter('System:').count();
214
+ (0, _vitest.expect)(filtered).toBeLessThanOrEqual(all);
215
+ });
216
+ stest('find() retrieves a function by ID', async () => {
217
+ if (!seed.functionId) return;
218
+ const fn = await dapi.functions.find(seed.functionId);
219
+ (0, _vitest.expect)(fn.id).toBe(seed.functionId);
220
+ (0, _vitest.expect)(fn).toHaveProperty('name');
221
+ });
222
+ stest('run() invokes a discovered function and returns a result or param error', async () => {
223
+ if (!seed.testFuncName) return;
224
+ // Call with no params — may fail with a params error, but must NOT return "Not Found"
225
+ try {
226
+ const result = await dapi.functions.run(seed.testFuncName, {});
227
+ (0, _vitest.expect)(result).toBeDefined();
228
+ } catch (e) {
229
+ // Params required / type error is acceptable — it means the function exists and was invoked
230
+ (0, _vitest.expect)(e.message).not.toMatch(/not found/i);
231
+ }
232
+ });
233
+ stest('run() throws a structured error for a non-existent function', async () => {
234
+ await (0, _vitest.expect)(dapi.functions.run('NonExistentPkg:nonExistentFunc', {})).rejects.toThrow();
235
+ });
236
+ });
237
+
238
+ // ─── connections ─────────────────────────────────────────────────────────────
239
+
240
+ (0, _vitest.describe)('connections', () => {
241
+ stest('list() returns an array', async () => {
242
+ const conns = await dapi.connections.by(5).list();
243
+ (0, _vitest.expect)(Array.isArray(conns)).toBe(true);
244
+ });
245
+ stest('count() returns a non-negative integer', async () => {
246
+ const n = await dapi.connections.count();
247
+ (0, _vitest.expect)(n).toBeGreaterThanOrEqual(0);
248
+ });
249
+ stest('find() retrieves a connection by ID', async () => {
250
+ if (!seed.connectionId) return;
251
+ const conn = await dapi.connections.find(seed.connectionId);
252
+ (0, _vitest.expect)(conn.id).toBe(seed.connectionId);
253
+ });
254
+ });
255
+
256
+ // ─── queries ──────────────────────────────────────────────────────────────────
257
+
258
+ (0, _vitest.describe)('queries', () => {
259
+ stestEndpoint('list() returns an array', async () => {
260
+ const queries = await dapi.queries.by(5).list();
261
+ (0, _vitest.expect)(Array.isArray(queries)).toBe(true);
262
+ });
263
+ stest('find() retrieves a query by ID', async () => {
264
+ if (!seed.queryId) return;
265
+ const query = await dapi.queries.find(seed.queryId);
266
+ (0, _vitest.expect)(query.id).toBe(seed.queryId);
267
+ });
268
+ });
269
+
270
+ // ─── scripts ──────────────────────────────────────────────────────────────────
271
+
272
+ (0, _vitest.describe)('scripts', () => {
273
+ stestEndpoint('list() returns an array', async () => {
274
+ const scripts = await dapi.scripts.by(5).list();
275
+ (0, _vitest.expect)(Array.isArray(scripts)).toBe(true);
276
+ });
277
+ stest('find() retrieves a script by ID', async () => {
278
+ if (!seed.scriptId) return;
279
+ const script = await dapi.scripts.find(seed.scriptId);
280
+ (0, _vitest.expect)(script.id).toBe(seed.scriptId);
281
+ });
282
+ });
283
+
284
+ // ─── packages ─────────────────────────────────────────────────────────────────
285
+
286
+ (0, _vitest.describe)('packages', () => {
287
+ stest('list() returns an array with id and name', async () => {
288
+ const pkgs = await dapi.packages.by(5).list();
289
+ (0, _vitest.expect)(Array.isArray(pkgs)).toBe(true);
290
+ (0, _vitest.expect)(pkgs.length).toBeGreaterThan(0);
291
+ for (const p of pkgs) (0, _vitest.expect)(p).toMatchObject({
292
+ id: _vitest.expect.any(String),
293
+ name: _vitest.expect.any(String)
294
+ });
295
+ });
296
+ stest('count() returns a non-negative integer', async () => {
297
+ const n = await dapi.packages.count();
298
+ (0, _vitest.expect)(n).toBeGreaterThanOrEqual(0);
299
+ });
300
+ stest('filter() narrows by name', async () => {
301
+ const all = await dapi.packages.count();
302
+ // Empty filter returns all; non-matching filter returns fewer
303
+ const filtered = await dapi.packages.filter('zzz-no-such-package-xyz').count();
304
+ (0, _vitest.expect)(filtered).toBeLessThanOrEqual(all);
305
+ });
306
+ stest('find() retrieves a package by ID', async () => {
307
+ if (!seed.packageId) return;
308
+ const pkg = await dapi.packages.find(seed.packageId);
309
+ (0, _vitest.expect)(pkg.id).toBe(seed.packageId);
310
+ (0, _vitest.expect)(pkg).toHaveProperty('name');
311
+ });
312
+ });
313
+
314
+ // ─── reports ──────────────────────────────────────────────────────────────────
315
+
316
+ (0, _vitest.describe)('reports', () => {
317
+ stestEndpoint('list() returns an array', async () => {
318
+ const reports = await dapi.reports.by(5).list();
319
+ (0, _vitest.expect)(Array.isArray(reports)).toBe(true);
320
+ });
321
+ stest('find() retrieves a report by ID', async () => {
322
+ if (!seed.reportId) return;
323
+ const report = await dapi.reports.find(seed.reportId);
324
+ (0, _vitest.expect)(report.id).toBe(seed.reportId);
325
+ });
326
+ });
327
+
328
+ // ─── files ────────────────────────────────────────────────────────────────────
329
+
330
+ (0, _vitest.describe)('files', () => {
331
+ /** Normalises the files.list() response to an array regardless of server shape. */
332
+ function toFileArray(result) {
333
+ if (Array.isArray(result)) return result;
334
+ if (result && typeof result === 'object') {
335
+ // Some servers return {items:[...]} or {files:[...]} or {value:[...]}
336
+ const nested = result.items ?? result.files ?? result.value ?? result.data;
337
+ if (Array.isArray(nested)) return nested;
338
+ }
339
+ return [];
340
+ }
341
+ stest(`list() returns a defined result for ${TEST_FILES_PATH}`, async () => {
342
+ const result = await dapi.files.list(TEST_FILES_PATH);
343
+ (0, _vitest.expect)(result).toBeDefined();
344
+ // Log actual shape to aid diagnosis
345
+ const shape = Array.isArray(result) ? `array(${result.length})` : typeof result;
346
+ console.log(`files.list shape: ${shape}`);
347
+ });
348
+ stest('list() with recursive=true returns a defined result', async () => {
349
+ const result = await dapi.files.list(TEST_FILES_PATH, true);
350
+ (0, _vitest.expect)(result).toBeDefined();
351
+ });
352
+ stest('list() non-recursive returns fewer or equal files than recursive', async () => {
353
+ const flat = toFileArray(await dapi.files.list(TEST_FILES_PATH, false));
354
+ const recursive = toFileArray(await dapi.files.list(TEST_FILES_PATH, true));
355
+ (0, _vitest.expect)(flat.length).toBeLessThanOrEqual(recursive.length);
356
+ });
357
+ stest('get() returns content for an existing file', async () => {
358
+ const files = toFileArray(await dapi.files.list(TEST_FILES_PATH, true));
359
+ const textFile = files.find(f => typeof f === 'string' ? /\.(json|txt|yaml|md)$/.test(f) : /\.(json|txt|yaml|md)$/.test(f.path ?? f.name ?? ''));
360
+ if (!textFile) return; // No text files found — skip gracefully
361
+ const filePath = typeof textFile === 'string' ? textFile : textFile.path ?? textFile.name;
362
+ const content = await dapi.files.get(`${TEST_FILES_PATH}/${filePath}`);
363
+ (0, _vitest.expect)(content).toBeDefined();
364
+ });
365
+ });
366
+
367
+ // ─── NodeDapi.raw ─────────────────────────────────────────────────────────────
368
+
369
+ (0, _vitest.describe)('NodeDapi.raw', () => {
370
+ stest('GET /api/users/current returns the current user', async () => {
371
+ const result = await dapi.raw('GET', '/api/users/current');
372
+ (0, _vitest.expect)(result).toBeDefined();
373
+ // The response may be an object or JSON text — just verify it contains "login" or "admin"
374
+ const text = typeof result === 'string' ? result : JSON.stringify(result);
375
+ (0, _vitest.expect)(text.toLowerCase()).toMatch(/login|admin|user/);
376
+ });
377
+ stest('GET /api/info returns server info', async () => {
378
+ const result = await dapi.raw('GET', '/api/info');
379
+ (0, _vitest.expect)(result).toBeDefined();
380
+ });
381
+ stest('accepts lowercase method names', async () => {
382
+ const result = await dapi.raw('get', '/api/users/current');
383
+ (0, _vitest.expect)(result).toBeDefined();
384
+ });
385
+ });
386
+
387
+ // ─── NodeDapi.describe ────────────────────────────────────────────────────────
388
+
389
+ (0, _vitest.describe)('NodeDapi.describe', () => {
390
+ stest('describe("connections") returns a schema object', async () => {
391
+ const schema = await dapi.describe('connections');
392
+ (0, _vitest.expect)(schema).toBeDefined();
393
+ });
394
+ stest('describe("users") returns a schema object', async () => {
395
+ const schema = await dapi.describe('users');
396
+ (0, _vitest.expect)(schema).toBeDefined();
397
+ });
398
+ stest('describe("nonexistentEntity") throws or returns null', async () => {
399
+ // Either behavior is acceptable — the important thing is it does not hang
400
+ try {
401
+ await dapi.describe('nonexistentEntity-xyz');
402
+ } catch {
403
+ // Expected
404
+ }
405
+ });
406
+ });