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