@credda/cli 0.1.1 → 0.1.2
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 +136 -1
- package/dist/cli.d.ts +50 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +701 -5
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +626 -1
- package/dist/cli.test.js.map +1 -1
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/cli.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { classifyCredentialInput, runCli } from './cli.js';
|
|
2
|
+
import { classifyCredentialInput, errorHints, parseFlags, parseIdList, runCli } from './cli.js';
|
|
3
3
|
function makeCtx(overrides = {}) {
|
|
4
4
|
const lines = [];
|
|
5
5
|
const errors = [];
|
|
@@ -20,6 +20,51 @@ function makeCtx(overrides = {}) {
|
|
|
20
20
|
deleteWebhook: vi.fn().mockResolvedValue(undefined),
|
|
21
21
|
testWebhook: vi.fn().mockResolvedValue({ success: true }),
|
|
22
22
|
getWebhookDeliveries: vi.fn().mockResolvedValue({ data: [], nextCursor: null }),
|
|
23
|
+
getRecentWebhookEvents: vi.fn().mockResolvedValue({ data: [], nextCursor: null, source: 'examples' }),
|
|
24
|
+
getActivity: vi.fn().mockResolvedValue({ data: [], nextCursor: null }),
|
|
25
|
+
listMonitors: vi.fn().mockResolvedValue({ data: [], nextCursor: null }),
|
|
26
|
+
getMonitor: vi.fn().mockResolvedValue({ monitor: { id: 'mon_1' } }),
|
|
27
|
+
createMonitor: vi.fn().mockResolvedValue({ monitor: { id: 'mon_1', belowScore: 40 } }),
|
|
28
|
+
deleteMonitor: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
createScreening: vi.fn().mockResolvedValue({
|
|
30
|
+
screening: { id: 'scr_1', status: 'COMPLETED', totalCount: 2, foundCount: 2 },
|
|
31
|
+
}),
|
|
32
|
+
listScreenings: vi.fn().mockResolvedValue({ data: [], nextCursor: null }),
|
|
33
|
+
getScreening: vi.fn().mockResolvedValue({ screening: { id: 'scr_1', status: 'COMPLETED' } }),
|
|
34
|
+
getScreeningResults: vi.fn().mockResolvedValue({ screening: { id: 'scr_1' }, results: [], count: 0 }),
|
|
35
|
+
getBenchmarks: vi.fn().mockResolvedValue({ benchmarkVersion: '1', kAnonymity: { minimumCohortSize: 20 } }),
|
|
36
|
+
getReasonCodes: vi.fn().mockResolvedValue({ reasonCodesVersion: '1', codes: [] }),
|
|
37
|
+
getBenchmarkDistribution: vi.fn().mockResolvedValue({ dimension: 'all', populationSize: 3, cohorts: [] }),
|
|
38
|
+
getUserBenchmark: vi.fn().mockResolvedValue({ userId: 'u1', available: true, percentile: 72 }),
|
|
39
|
+
listUsers: vi.fn().mockResolvedValue({ data: [], count: 0, nextCursor: null }),
|
|
40
|
+
getTrustSummary: vi.fn().mockResolvedValue({ userId: 'u1', available: true, summary: '...' }),
|
|
41
|
+
getOpenBadgeAchievements: vi.fn().mockResolvedValue({ achievementIds: ['first-delivery'], achievements: [] }),
|
|
42
|
+
getOpenBadgeAchievement: vi.fn().mockResolvedValue({ id: 'first-delivery', achievementType: 'Badge' }),
|
|
43
|
+
createConfirmationRequest: vi.fn().mockResolvedValue({
|
|
44
|
+
confirmation: { id: 'cnf_1', status: 'PENDING', resultingEventId: null },
|
|
45
|
+
confirmationToken: 'raw_token',
|
|
46
|
+
confirmUrl: 'https://api.credda.io/confirm/cnf_1?token=raw_token',
|
|
47
|
+
previewUrl: 'https://api.credda.io/api/v1/confirmations/cnf_1/preview?token=raw_token',
|
|
48
|
+
respondUrl: 'https://api.credda.io/api/v1/confirmations/cnf_1/respond',
|
|
49
|
+
}),
|
|
50
|
+
listConfirmations: vi.fn().mockResolvedValue({ data: [], nextCursor: null }),
|
|
51
|
+
getConfirmation: vi.fn().mockResolvedValue({ confirmation: { id: 'cnf_1' } }),
|
|
52
|
+
cancelConfirmation: vi.fn().mockResolvedValue({ confirmation: { id: 'cnf_1', status: 'CANCELLED' } }),
|
|
53
|
+
previewConfirmation: vi.fn().mockResolvedValue({ confirmation: { id: 'cnf_1', platform: 'Acme' } }),
|
|
54
|
+
respondToConfirmation: vi.fn().mockResolvedValue({ status: 'CONFIRMED', confirmation: { id: 'cnf_1' }, eventId: 'ev_1' }),
|
|
55
|
+
createPolicy: vi.fn().mockResolvedValue({ policy: { id: 'pol_1' } }),
|
|
56
|
+
listPolicies: vi.fn().mockResolvedValue({ data: [], nextCursor: null }),
|
|
57
|
+
getPolicy: vi.fn().mockResolvedValue({ policy: { id: 'pol_1' } }),
|
|
58
|
+
updatePolicy: vi.fn().mockResolvedValue({ policy: { id: 'pol_1', threshold: 55 } }),
|
|
59
|
+
deletePolicy: vi.fn().mockResolvedValue(undefined),
|
|
60
|
+
getVerifiedProfile: vi.fn().mockResolvedValue({ userId: 'u1', verificationDepth: 0.75 }),
|
|
61
|
+
recordQualification: vi.fn().mockResolvedValue({ eventId: 'ev_2', isVerified: true, verificationNote: null }),
|
|
62
|
+
getProfessionalRecord: vi.fn().mockResolvedValue({ userId: 'u1', professionalRecordVersion: '1.0' }),
|
|
63
|
+
mintProfessionalRecordCredential: vi.fn().mockResolvedValue({
|
|
64
|
+
credentialType: 'CreddaProfessionalRecordCredential',
|
|
65
|
+
linkedin: { addToProfileUrl: 'https://www.linkedin.com/profile/add' },
|
|
66
|
+
}),
|
|
67
|
+
getPublicProfessionalRecord: vi.fn().mockResolvedValue({ token: 'tok', professionalRecord: null }),
|
|
23
68
|
};
|
|
24
69
|
return {
|
|
25
70
|
client,
|
|
@@ -136,6 +181,11 @@ describe('runCli', () => {
|
|
|
136
181
|
expect(await runCli(['webhooks', 'create', 'https://h.example/w'], ctx)).toBe(1); // no events
|
|
137
182
|
expect(await runCli(['webhooks', 'test', 'wh_1'], ctx)).toBe(0);
|
|
138
183
|
expect(await runCli(['webhooks', 'deliveries', 'wh_1'], ctx)).toBe(0);
|
|
184
|
+
// Sample data across every endpoint, optionally filtered by event type.
|
|
185
|
+
expect(await runCli(['webhooks', 'recent'], ctx)).toBe(0);
|
|
186
|
+
expect(ctx.client.getRecentWebhookEvents).toHaveBeenCalledWith('k', { limit: 25, eventType: undefined });
|
|
187
|
+
expect(await runCli(['webhooks', 'recent', 'score.updated'], ctx)).toBe(0);
|
|
188
|
+
expect(ctx.client.getRecentWebhookEvents).toHaveBeenCalledWith('k', { limit: 25, eventType: ['score.updated'] });
|
|
139
189
|
expect(await runCli(['webhooks', 'nope'], ctx)).toBe(1);
|
|
140
190
|
});
|
|
141
191
|
it('listen validates the port and delegates to the injected listener', async () => {
|
|
@@ -156,4 +206,579 @@ describe('runCli', () => {
|
|
|
156
206
|
expect(JSON.parse(ctx.lines[0])).toEqual({ revoked: true });
|
|
157
207
|
});
|
|
158
208
|
});
|
|
209
|
+
describe('parseFlags', () => {
|
|
210
|
+
it('splits valued flags, boolean flags, and positionals', () => {
|
|
211
|
+
const r = parseFlags(['a', '--limit', '5', '--band-change', 'b'], {
|
|
212
|
+
valued: ['limit'],
|
|
213
|
+
boolean: ['band-change'],
|
|
214
|
+
});
|
|
215
|
+
expect(r).toEqual({ positional: ['a', 'b'], flags: { limit: '5', 'band-change': true } });
|
|
216
|
+
});
|
|
217
|
+
it('rejects unknown flags and valued flags missing a value', () => {
|
|
218
|
+
expect(() => parseFlags(['--nope'], {})).toThrow(/unknown flag --nope/);
|
|
219
|
+
expect(() => parseFlags(['--limit'], { valued: ['limit'] })).toThrow(/--limit needs a value/);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe('parseIdList', () => {
|
|
223
|
+
it('splits inline args on commas and whitespace, deduped', () => {
|
|
224
|
+
expect(parseIdList({ inline: ['u1,u2', 'u3', 'u2'] })).toEqual(['u1', 'u2', 'u3']);
|
|
225
|
+
});
|
|
226
|
+
it('reads one id per line from a file, skipping blanks', () => {
|
|
227
|
+
expect(parseIdList({ fileText: 'u1\n\nu2\r\nu3\n' })).toEqual(['u1', 'u2', 'u3']);
|
|
228
|
+
});
|
|
229
|
+
it('takes the first CSV column and skips a header row', () => {
|
|
230
|
+
expect(parseIdList({ fileText: 'externalId,name\nu1,Ann\nu2,Bo\n' })).toEqual(['u1', 'u2']);
|
|
231
|
+
// no header — first line is a real id
|
|
232
|
+
expect(parseIdList({ fileText: 'u9,Zed\nu1,Ann\n' })).toEqual(['u9', 'u1']);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
describe('monitors', () => {
|
|
236
|
+
it('requires CREDDA_API_KEY', async () => {
|
|
237
|
+
const ctx = makeCtx();
|
|
238
|
+
expect(await runCli(['monitors', 'list'], ctx)).toBe(1);
|
|
239
|
+
expect(ctx.errors[0]).toContain('CREDDA_API_KEY');
|
|
240
|
+
});
|
|
241
|
+
it('list passes cursor/limit through', async () => {
|
|
242
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
243
|
+
expect(await runCli(['monitors', 'list', '--limit', '10', '--cursor', 'mon0'], ctx)).toBe(0);
|
|
244
|
+
expect(ctx.client.listMonitors).toHaveBeenCalledWith('k', { limit: 10, cursor: 'mon0' });
|
|
245
|
+
});
|
|
246
|
+
it('get and delete route to the client', async () => {
|
|
247
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
248
|
+
expect(await runCli(['monitors', 'get', 'mon_1'], ctx)).toBe(0);
|
|
249
|
+
expect(ctx.client.getMonitor).toHaveBeenCalledWith('mon_1', 'k');
|
|
250
|
+
expect(await runCli(['monitors', 'delete', 'mon_1'], ctx)).toBe(0);
|
|
251
|
+
expect(ctx.client.deleteMonitor).toHaveBeenCalledWith('mon_1', 'k');
|
|
252
|
+
expect(JSON.parse(ctx.lines.at(-1))).toEqual({ deleted: true });
|
|
253
|
+
});
|
|
254
|
+
it('create requires --user and at least one condition', async () => {
|
|
255
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
256
|
+
expect(await runCli(['monitors', 'create', '--below', '40'], ctx)).toBe(1);
|
|
257
|
+
expect(ctx.errors.at(-1)).toContain('--user');
|
|
258
|
+
expect(await runCli(['monitors', 'create', '--user', 'u1'], ctx)).toBe(1);
|
|
259
|
+
expect(ctx.errors.at(-1)).toContain('at least one condition');
|
|
260
|
+
expect(ctx.client.createMonitor).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
it('create maps flags to CreateMonitorInput', async () => {
|
|
263
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
264
|
+
expect(await runCli(['monitors', 'create', '--user', 'u1', '--below', '40', '--band-change'], ctx)).toBe(0);
|
|
265
|
+
expect(ctx.client.createMonitor).toHaveBeenCalledWith({ userId: 'u1', belowScore: 40, onBandChange: true }, 'k');
|
|
266
|
+
});
|
|
267
|
+
it('create rejects a non-numeric threshold', async () => {
|
|
268
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
269
|
+
expect(await runCli(['monitors', 'create', '--user', 'u1', '--below', 'nope'], ctx)).toBe(1);
|
|
270
|
+
expect(ctx.errors.at(-1)).toContain('--below must be a number');
|
|
271
|
+
});
|
|
272
|
+
it('unknown subcommand exits 1', async () => {
|
|
273
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
274
|
+
expect(await runCli(['monitors', 'frob'], ctx)).toBe(1);
|
|
275
|
+
expect(ctx.errors.at(-1)).toContain('unknown monitors subcommand');
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
describe('screen', () => {
|
|
279
|
+
it('submits inline ids and prints the job', async () => {
|
|
280
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
281
|
+
expect(await runCli(['screen', 'u1,u2', 'u3'], ctx)).toBe(0);
|
|
282
|
+
expect(ctx.client.createScreening).toHaveBeenCalledWith(['u1', 'u2', 'u3'], 'k');
|
|
283
|
+
expect(JSON.parse(ctx.lines[0])).toMatchObject({ screening: { id: 'scr_1', status: 'COMPLETED' } });
|
|
284
|
+
});
|
|
285
|
+
it('reads ids from --file via readInput', async () => {
|
|
286
|
+
const ctx = makeCtx({
|
|
287
|
+
apiKey: 'k',
|
|
288
|
+
readInput: vi.fn().mockResolvedValue('id,name\nu1,Ann\nu2,Bo\n'),
|
|
289
|
+
});
|
|
290
|
+
expect(await runCli(['screen', '--file', 'roster.csv'], ctx)).toBe(0);
|
|
291
|
+
expect(ctx.readInput).toHaveBeenCalledWith('roster.csv');
|
|
292
|
+
expect(ctx.client.createScreening).toHaveBeenCalledWith(['u1', 'u2'], 'k');
|
|
293
|
+
});
|
|
294
|
+
it('rejects ids AND --file together, and an empty id set', async () => {
|
|
295
|
+
const ctx = makeCtx({ apiKey: 'k', readInput: vi.fn().mockResolvedValue('') });
|
|
296
|
+
expect(await runCli(['screen', 'u1', '--file', 'f.csv'], ctx)).toBe(1);
|
|
297
|
+
expect(ctx.errors.at(-1)).toContain('not both');
|
|
298
|
+
expect(await runCli(['screen', '--file', 'f.csv'], ctx)).toBe(1);
|
|
299
|
+
expect(ctx.errors.at(-1)).toContain('no ids found');
|
|
300
|
+
expect(ctx.client.createScreening).not.toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
it('requires CREDDA_API_KEY', async () => {
|
|
303
|
+
const ctx = makeCtx();
|
|
304
|
+
expect(await runCli(['screen', 'u1'], ctx)).toBe(1);
|
|
305
|
+
expect(ctx.errors[0]).toContain('CREDDA_API_KEY');
|
|
306
|
+
});
|
|
307
|
+
it('--wait polls until terminal state then prints the summary', async () => {
|
|
308
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
309
|
+
const ctx = makeCtx({ apiKey: 'k', sleep, pollIntervalMs: 1 });
|
|
310
|
+
ctx.client.createScreening.mockResolvedValue({
|
|
311
|
+
screening: { id: 'scr_9', status: 'QUEUED', totalCount: 2, foundCount: null },
|
|
312
|
+
});
|
|
313
|
+
ctx.client.getScreening
|
|
314
|
+
.mockResolvedValueOnce({ screening: { id: 'scr_9', status: 'RUNNING' } })
|
|
315
|
+
.mockResolvedValueOnce({ screening: { id: 'scr_9', status: 'COMPLETED', foundCount: 2 } });
|
|
316
|
+
expect(await runCli(['screen', 'u1,u2', '--wait'], ctx)).toBe(0);
|
|
317
|
+
expect(sleep).toHaveBeenCalledTimes(2);
|
|
318
|
+
expect(ctx.client.getScreening).toHaveBeenCalledTimes(2);
|
|
319
|
+
expect(JSON.parse(ctx.lines[0])).toMatchObject({ screening: { status: 'COMPLETED', foundCount: 2 } });
|
|
320
|
+
});
|
|
321
|
+
it('--wait exits 1 when the job FAILED', async () => {
|
|
322
|
+
const ctx = makeCtx({ apiKey: 'k', sleep: vi.fn().mockResolvedValue(undefined), pollIntervalMs: 1 });
|
|
323
|
+
ctx.client.createScreening.mockResolvedValue({
|
|
324
|
+
screening: { id: 'scr_9', status: 'QUEUED' },
|
|
325
|
+
});
|
|
326
|
+
ctx.client.getScreening.mockResolvedValue({
|
|
327
|
+
screening: { id: 'scr_9', status: 'FAILED', error: 'boom' },
|
|
328
|
+
});
|
|
329
|
+
expect(await runCli(['screen', 'u1', '--wait'], ctx)).toBe(1);
|
|
330
|
+
expect(JSON.parse(ctx.lines[0])).toMatchObject({ screening: { status: 'FAILED' } });
|
|
331
|
+
});
|
|
332
|
+
it('without --wait, a queued job prints a polling hint to stderr', async () => {
|
|
333
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
334
|
+
ctx.client.createScreening.mockResolvedValue({
|
|
335
|
+
screening: { id: 'scr_9', status: 'QUEUED' },
|
|
336
|
+
});
|
|
337
|
+
expect(await runCli(['screen', 'u1'], ctx)).toBe(0);
|
|
338
|
+
expect(ctx.errors.join(' ')).toContain('credda screenings get scr_9');
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
describe('screenings', () => {
|
|
342
|
+
it('list/get route to the client with pagination', async () => {
|
|
343
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
344
|
+
expect(await runCli(['screenings', 'list', '--limit', '5', '--cursor', 'scr0'], ctx)).toBe(0);
|
|
345
|
+
expect(ctx.client.listScreenings).toHaveBeenCalledWith('k', { limit: 5, cursor: 'scr0' });
|
|
346
|
+
expect(await runCli(['screenings', 'get', 'scr_1'], ctx)).toBe(0);
|
|
347
|
+
expect(ctx.client.getScreening).toHaveBeenCalledWith('scr_1', 'k');
|
|
348
|
+
});
|
|
349
|
+
it('results prints JSON by default', async () => {
|
|
350
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
351
|
+
expect(await runCli(['screenings', 'results', 'scr_1'], ctx)).toBe(0);
|
|
352
|
+
expect(ctx.client.getScreeningResults).toHaveBeenCalledWith('scr_1', 'k');
|
|
353
|
+
});
|
|
354
|
+
it('results --csv fetches the CSV raw and writes the file', async () => {
|
|
355
|
+
const fetchCsv = vi.fn().mockResolvedValue('externalId,found\nu1,true\n');
|
|
356
|
+
const writeFile = vi.fn().mockResolvedValue(undefined);
|
|
357
|
+
const ctx = makeCtx({ apiKey: 'k', fetchCsv, writeFile });
|
|
358
|
+
expect(await runCli(['screenings', 'results', 'scr_1', '--csv', 'out.csv'], ctx)).toBe(0);
|
|
359
|
+
expect(fetchCsv).toHaveBeenCalledWith('/screenings/scr_1/results?format=csv', 'k');
|
|
360
|
+
expect(writeFile).toHaveBeenCalledWith('out.csv', 'externalId,found\nu1,true\n');
|
|
361
|
+
expect(ctx.client.getScreeningResults).not.toHaveBeenCalled();
|
|
362
|
+
expect(JSON.parse(ctx.lines[0])).toEqual({ written: 'out.csv' });
|
|
363
|
+
});
|
|
364
|
+
it('unknown subcommand exits 1', async () => {
|
|
365
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
366
|
+
expect(await runCli(['screenings', 'frob'], ctx)).toBe(1);
|
|
367
|
+
expect(ctx.errors.at(-1)).toContain('unknown screenings subcommand');
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
describe('activity', () => {
|
|
371
|
+
it('requires a key and passes filters through', async () => {
|
|
372
|
+
const noKey = makeCtx();
|
|
373
|
+
expect(await runCli(['activity'], noKey)).toBe(1);
|
|
374
|
+
expect(noKey.errors[0]).toContain('CREDDA_API_KEY');
|
|
375
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
376
|
+
expect(await runCli(['activity', '--action', 'EVENT_CREATED', '--from', '2026-07-01', '--to', '2026-07-22', '--limit', '5', '--cursor', 'a0'], ctx)).toBe(0);
|
|
377
|
+
expect(ctx.client.getActivity).toHaveBeenCalledWith('k', {
|
|
378
|
+
limit: 5,
|
|
379
|
+
cursor: 'a0',
|
|
380
|
+
action: 'EVENT_CREATED',
|
|
381
|
+
from: '2026-07-01',
|
|
382
|
+
to: '2026-07-22',
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
it('rejects stray positional arguments', async () => {
|
|
386
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
387
|
+
expect(await runCli(['activity', 'oops'], ctx)).toBe(1);
|
|
388
|
+
expect(ctx.errors.at(-1)).toContain('no positional arguments');
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
describe('usage ranges + csv', () => {
|
|
392
|
+
it('keeps the original [days] behavior (still called with a bare number)', async () => {
|
|
393
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
394
|
+
expect(await runCli(['usage', '30'], ctx)).toBe(0);
|
|
395
|
+
expect(ctx.client.getUsage).toHaveBeenCalledWith('k', 30);
|
|
396
|
+
expect(await runCli(['usage'], ctx)).toBe(0);
|
|
397
|
+
expect(ctx.client.getUsage).toHaveBeenLastCalledWith('k', undefined);
|
|
398
|
+
});
|
|
399
|
+
it('--from/--to pass a range object', async () => {
|
|
400
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
401
|
+
expect(await runCli(['usage', '--from', '2026-06-01', '--to', '2026-06-30'], ctx)).toBe(0);
|
|
402
|
+
expect(ctx.client.getUsage).toHaveBeenCalledWith('k', { from: '2026-06-01', to: '2026-06-30' });
|
|
403
|
+
});
|
|
404
|
+
it('rejects [days] combined with --from/--to', async () => {
|
|
405
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
406
|
+
expect(await runCli(['usage', '30', '--from', '2026-06-01'], ctx)).toBe(1);
|
|
407
|
+
expect(ctx.errors.at(-1)).toContain('not both');
|
|
408
|
+
expect(ctx.client.getUsage).not.toHaveBeenCalled();
|
|
409
|
+
});
|
|
410
|
+
it('--csv raw-fetches the statement and writes the file', async () => {
|
|
411
|
+
const fetchCsv = vi.fn().mockResolvedValue('row,date\nday,2026-07-01\n');
|
|
412
|
+
const writeFile = vi.fn().mockResolvedValue(undefined);
|
|
413
|
+
const ctx = makeCtx({ apiKey: 'k', fetchCsv, writeFile });
|
|
414
|
+
expect(await runCli(['usage', '--from', '2026-06-01', '--to', '2026-06-30', '--csv', 'usage.csv'], ctx)).toBe(0);
|
|
415
|
+
expect(fetchCsv).toHaveBeenCalledWith('/usage?format=csv&from=2026-06-01&to=2026-06-30', 'k');
|
|
416
|
+
expect(writeFile).toHaveBeenCalledWith('usage.csv', 'row,date\nday,2026-07-01\n');
|
|
417
|
+
expect(ctx.client.getUsage).not.toHaveBeenCalled();
|
|
418
|
+
expect(JSON.parse(ctx.lines[0])).toEqual({ written: 'usage.csv' });
|
|
419
|
+
});
|
|
420
|
+
it('--csv with a [days] window keeps days in the query', async () => {
|
|
421
|
+
const fetchCsv = vi.fn().mockResolvedValue('csv');
|
|
422
|
+
const writeFile = vi.fn().mockResolvedValue(undefined);
|
|
423
|
+
const ctx = makeCtx({ apiKey: 'k', fetchCsv, writeFile });
|
|
424
|
+
expect(await runCli(['usage', '14', '--csv', 'u.csv'], ctx)).toBe(0);
|
|
425
|
+
expect(fetchCsv).toHaveBeenCalledWith('/usage?format=csv&days=14', 'k');
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
describe('failure output', () => {
|
|
429
|
+
/** A CreddaError-shaped rejection, without importing the class. */
|
|
430
|
+
function creddaError(extra) {
|
|
431
|
+
return Object.assign(new Error('credda: Not found'), extra);
|
|
432
|
+
}
|
|
433
|
+
it('prints the request id so a CLI user has something to quote', async () => {
|
|
434
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
435
|
+
ctx.client.getScore
|
|
436
|
+
.mockRejectedValue(creddaError({ status: 404, code: 'USER_NOT_FOUND', requestId: 'rq-1' }));
|
|
437
|
+
expect(await runCli(['score', 'u1'], ctx)).toBe(1);
|
|
438
|
+
const out = ctx.errors.join('\n');
|
|
439
|
+
expect(out).toContain('credda: Not found');
|
|
440
|
+
expect(out).toContain('rq-1');
|
|
441
|
+
expect(out).toContain('USER_NOT_FOUND');
|
|
442
|
+
// Never doubles the SDK's own prefix.
|
|
443
|
+
expect(out).not.toContain('credda: credda:');
|
|
444
|
+
});
|
|
445
|
+
it('prints the back-off the server asked for on a 429', async () => {
|
|
446
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
447
|
+
ctx.client.getScore
|
|
448
|
+
.mockRejectedValue(creddaError({ status: 429, code: 'QUOTA_EXCEEDED', requestId: 'rq-2', retryAfterMs: 45000 }));
|
|
449
|
+
expect(await runCli(['score', 'u1'], ctx)).toBe(1);
|
|
450
|
+
expect(ctx.errors.join('\n')).toContain('45s');
|
|
451
|
+
});
|
|
452
|
+
it('adds no noise for a plain local error (bad usage)', async () => {
|
|
453
|
+
const ctx = makeCtx();
|
|
454
|
+
expect(await runCli(['nonsense'], ctx)).toBe(1);
|
|
455
|
+
expect(ctx.errors).toHaveLength(1);
|
|
456
|
+
expect(ctx.errors[0]).toContain('unknown command');
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
describe('errorHints', () => {
|
|
460
|
+
it('returns nothing for non-object throws', () => {
|
|
461
|
+
expect(errorHints('boom')).toEqual([]);
|
|
462
|
+
expect(errorHints(undefined)).toEqual([]);
|
|
463
|
+
expect(errorHints(new Error('plain'))).toEqual([]);
|
|
464
|
+
});
|
|
465
|
+
it('omits a zero/absent Retry-After rather than printing "retry in 0s"', () => {
|
|
466
|
+
expect(errorHints({ code: 'NOT_FOUND', retryAfterMs: 0 }).join('\n')).not.toContain('retry in');
|
|
467
|
+
expect(errorHints({ code: 'NOT_FOUND', retryAfterMs: null }).join('\n')).not.toContain('retry in');
|
|
468
|
+
});
|
|
469
|
+
it('rounds a sub-second Retry-After up to 1s', () => {
|
|
470
|
+
expect(errorHints({ retryAfterMs: 200 }).join('\n')).toContain('1s');
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
describe('benchmarks, reason codes, trust summary and the book query', () => {
|
|
474
|
+
it('benchmarks is public — no key needed', async () => {
|
|
475
|
+
const ctx = makeCtx();
|
|
476
|
+
expect(await runCli(['benchmarks'], ctx)).toBe(0);
|
|
477
|
+
expect(ctx.client.getBenchmarks).toHaveBeenCalledWith();
|
|
478
|
+
expect(JSON.parse(ctx.lines[0]).kAnonymity.minimumCohortSize).toBe(20);
|
|
479
|
+
});
|
|
480
|
+
it('reason-codes is public — no key needed', async () => {
|
|
481
|
+
const ctx = makeCtx();
|
|
482
|
+
expect(await runCli(['reason-codes'], ctx)).toBe(0);
|
|
483
|
+
expect(ctx.client.getReasonCodes).toHaveBeenCalledWith();
|
|
484
|
+
});
|
|
485
|
+
it('trust-summary passes the key and defaults narrative off', async () => {
|
|
486
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
487
|
+
expect(await runCli(['trust-summary', 'u1'], ctx)).toBe(0);
|
|
488
|
+
expect(ctx.client.getTrustSummary).toHaveBeenCalledWith('u1', 'k', { narrative: false });
|
|
489
|
+
});
|
|
490
|
+
it('trust-summary --narrative sets the flag', async () => {
|
|
491
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
492
|
+
expect(await runCli(['trust-summary', 'u1', '--narrative'], ctx)).toBe(0);
|
|
493
|
+
expect(ctx.client.getTrustSummary).toHaveBeenCalledWith('u1', 'k', { narrative: true });
|
|
494
|
+
});
|
|
495
|
+
it('trust-summary needs a key', async () => {
|
|
496
|
+
const ctx = makeCtx();
|
|
497
|
+
expect(await runCli(['trust-summary', 'u1'], ctx)).toBe(1);
|
|
498
|
+
expect(ctx.errors[0]).toContain('CREDDA_API_KEY');
|
|
499
|
+
expect(ctx.client.getTrustSummary).not.toHaveBeenCalled();
|
|
500
|
+
});
|
|
501
|
+
it('benchmark passes the optional dimension through', async () => {
|
|
502
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
503
|
+
expect(await runCli(['benchmark', 'u1', '--dimension', 'tenureBand'], ctx)).toBe(0);
|
|
504
|
+
expect(ctx.client.getUserBenchmark).toHaveBeenCalledWith('u1', 'k', { dimension: 'tenureBand' });
|
|
505
|
+
});
|
|
506
|
+
it('benchmark without a dimension leaves it undefined', async () => {
|
|
507
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
508
|
+
expect(await runCli(['benchmark', 'u1'], ctx)).toBe(0);
|
|
509
|
+
expect(ctx.client.getUserBenchmark).toHaveBeenCalledWith('u1', 'k', { dimension: undefined });
|
|
510
|
+
});
|
|
511
|
+
it('benchmark requires a userId', async () => {
|
|
512
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
513
|
+
expect(await runCli(['benchmark'], ctx)).toBe(1);
|
|
514
|
+
expect(ctx.errors[0]).toContain('userId');
|
|
515
|
+
});
|
|
516
|
+
it('distribution passes dimension + cohort', async () => {
|
|
517
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
518
|
+
expect(await runCli(['distribution', '--dimension', 'subjectType', '--cohort', 'AGENT'], ctx)).toBe(0);
|
|
519
|
+
expect(ctx.client.getBenchmarkDistribution).toHaveBeenCalledWith('k', {
|
|
520
|
+
dimension: 'subjectType',
|
|
521
|
+
cohort: 'AGENT',
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
it('distribution rejects positional arguments', async () => {
|
|
525
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
526
|
+
expect(await runCli(['distribution', 'oops'], ctx)).toBe(1);
|
|
527
|
+
expect(ctx.errors[0]).toContain('no positional arguments');
|
|
528
|
+
expect(ctx.client.getBenchmarkDistribution).not.toHaveBeenCalled();
|
|
529
|
+
});
|
|
530
|
+
it('users maps every filter flag onto the query', async () => {
|
|
531
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
532
|
+
expect(await runCli([
|
|
533
|
+
'users',
|
|
534
|
+
'--score-min', '40', '--score-max', '90',
|
|
535
|
+
'--band', 'Good',
|
|
536
|
+
'--subject-type', 'PERSON',
|
|
537
|
+
'--active-since', '2026-01-01T00:00:00Z',
|
|
538
|
+
'--verified',
|
|
539
|
+
'--min-verified', '2',
|
|
540
|
+
'--sort', 'score', '--order', 'desc',
|
|
541
|
+
'--limit', '50', '--cursor', 'cur_1',
|
|
542
|
+
], ctx)).toBe(0);
|
|
543
|
+
expect(ctx.client.listUsers).toHaveBeenCalledWith('k', {
|
|
544
|
+
scoreMin: 40,
|
|
545
|
+
scoreMax: 90,
|
|
546
|
+
band: 'Good',
|
|
547
|
+
subjectType: 'PERSON',
|
|
548
|
+
activeSince: '2026-01-01T00:00:00Z',
|
|
549
|
+
minVerifiedEvents: 2,
|
|
550
|
+
sort: 'score',
|
|
551
|
+
order: 'desc',
|
|
552
|
+
limit: 50,
|
|
553
|
+
cursor: 'cur_1',
|
|
554
|
+
hasVerifiedEvents: true,
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
it('users with no flags sends an all-undefined query (no filters)', async () => {
|
|
558
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
559
|
+
expect(await runCli(['users'], ctx)).toBe(0);
|
|
560
|
+
const query = ctx.client.listUsers.mock.calls[0][1];
|
|
561
|
+
expect(query).not.toHaveProperty('hasVerifiedEvents');
|
|
562
|
+
expect(Object.values(query).every((v) => v === undefined)).toBe(true);
|
|
563
|
+
});
|
|
564
|
+
it('users rejects positional arguments and needs a key', async () => {
|
|
565
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
566
|
+
expect(await runCli(['users', 'oops'], ctx)).toBe(1);
|
|
567
|
+
expect(ctx.errors[0]).toContain('no positional arguments');
|
|
568
|
+
const noKey = makeCtx();
|
|
569
|
+
expect(await runCli(['users'], noKey)).toBe(1);
|
|
570
|
+
expect(noKey.errors[0]).toContain('CREDDA_API_KEY');
|
|
571
|
+
});
|
|
572
|
+
it('help documents the new commands', async () => {
|
|
573
|
+
const ctx = makeCtx();
|
|
574
|
+
await runCli(['help'], ctx);
|
|
575
|
+
const help = ctx.lines.join('\n');
|
|
576
|
+
for (const cmd of ['credda benchmarks', 'credda reason-codes', 'credda trust-summary', 'credda benchmark', 'credda distribution', 'credda users']) {
|
|
577
|
+
expect(help).toContain(cmd);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
describe('confirmation requests', () => {
|
|
582
|
+
it('create maps every flag onto the request and warns the token is shown once', async () => {
|
|
583
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
584
|
+
expect(await runCli([
|
|
585
|
+
'confirmations', 'create',
|
|
586
|
+
'--user', 'worker_7',
|
|
587
|
+
'--type', 'CONTRACT_FULFILLED',
|
|
588
|
+
'--counterparty', 'client_42',
|
|
589
|
+
'--counterparty-name', 'Acme Ltd',
|
|
590
|
+
'--description', 'Kitchen refit',
|
|
591
|
+
'--stake', 'HIGH',
|
|
592
|
+
'--value', '1200',
|
|
593
|
+
'--due', '2026-07-20T00:00:00Z',
|
|
594
|
+
'--completed', '2026-07-19T00:00:00Z',
|
|
595
|
+
'--return-url', 'https://acme.example/thanks',
|
|
596
|
+
'--expires-in', '14',
|
|
597
|
+
'--idempotency-key', 'job-991',
|
|
598
|
+
], ctx)).toBe(0);
|
|
599
|
+
expect(ctx.client.createConfirmationRequest).toHaveBeenCalledWith({
|
|
600
|
+
userId: 'worker_7',
|
|
601
|
+
eventType: 'CONTRACT_FULFILLED',
|
|
602
|
+
counterpartyRef: 'client_42',
|
|
603
|
+
counterpartyName: 'Acme Ltd',
|
|
604
|
+
description: 'Kitchen refit',
|
|
605
|
+
stakeLevel: 'HIGH',
|
|
606
|
+
transactionValue: 1200,
|
|
607
|
+
dueDate: '2026-07-20T00:00:00Z',
|
|
608
|
+
completedAt: '2026-07-19T00:00:00Z',
|
|
609
|
+
returnUrl: 'https://acme.example/thanks',
|
|
610
|
+
expiresInDays: 14,
|
|
611
|
+
}, 'k', { idempotencyKey: 'job-991' });
|
|
612
|
+
expect(ctx.errors.join('\n')).toContain('shown ONCE');
|
|
613
|
+
expect(JSON.parse(ctx.lines[0]).confirmUrl).toContain('/confirm/cnf_1');
|
|
614
|
+
});
|
|
615
|
+
it('create refuses without --user / --type / --counterparty', async () => {
|
|
616
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
617
|
+
expect(await runCli(['confirmations', 'create', '--user', 'w1'], ctx)).toBe(1);
|
|
618
|
+
expect(ctx.errors.at(-1)).toContain('--counterparty');
|
|
619
|
+
expect(ctx.client.createConfirmationRequest).not.toHaveBeenCalled();
|
|
620
|
+
});
|
|
621
|
+
it('list/get/cancel are keyed and route through', async () => {
|
|
622
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
623
|
+
expect(await runCli(['confirmations', 'list', '--status', 'pending', '--limit', '5', '--cursor', 'c0'], ctx)).toBe(0);
|
|
624
|
+
// The status is normalised — the API stores it upper-case.
|
|
625
|
+
expect(ctx.client.listConfirmations).toHaveBeenCalledWith('k', { limit: 5, cursor: 'c0', status: 'PENDING' });
|
|
626
|
+
expect(await runCli(['confirmations', 'get', 'cnf_1'], ctx)).toBe(0);
|
|
627
|
+
expect(ctx.client.getConfirmation).toHaveBeenCalledWith('cnf_1', 'k');
|
|
628
|
+
expect(await runCli(['confirmations', 'cancel', 'cnf_1'], ctx)).toBe(0);
|
|
629
|
+
expect(ctx.client.cancelConfirmation).toHaveBeenCalledWith('cnf_1', 'k');
|
|
630
|
+
});
|
|
631
|
+
it('preview works with NO API key — the token is the capability', async () => {
|
|
632
|
+
const ctx = makeCtx(); // deliberately keyless
|
|
633
|
+
expect(await runCli(['confirmations', 'preview', 'cnf_1', '--token', 'raw_token'], ctx)).toBe(0);
|
|
634
|
+
expect(ctx.client.previewConfirmation).toHaveBeenCalledWith('cnf_1', 'raw_token');
|
|
635
|
+
expect(ctx.errors.join('\n')).not.toContain('CREDDA_API_KEY');
|
|
636
|
+
});
|
|
637
|
+
it('respond --confirm works with NO API key and returns the written event', async () => {
|
|
638
|
+
const ctx = makeCtx(); // deliberately keyless
|
|
639
|
+
expect(await runCli(['confirmations', 'respond', 'cnf_1', '--token', 'raw_token', '--confirm'], ctx)).toBe(0);
|
|
640
|
+
expect(ctx.client.respondToConfirmation).toHaveBeenCalledWith('cnf_1', 'raw_token', 'confirm');
|
|
641
|
+
expect(ctx.errors.join('\n')).not.toContain('CREDDA_API_KEY');
|
|
642
|
+
expect(JSON.parse(ctx.lines[0]).eventId).toBe('ev_1');
|
|
643
|
+
});
|
|
644
|
+
it('respond --decline sends the decline decision', async () => {
|
|
645
|
+
const ctx = makeCtx();
|
|
646
|
+
expect(await runCli(['confirmations', 'respond', 'cnf_1', '--token', 't', '--decline'], ctx)).toBe(0);
|
|
647
|
+
expect(ctx.client.respondToConfirmation).toHaveBeenCalledWith('cnf_1', 't', 'decline');
|
|
648
|
+
});
|
|
649
|
+
it('respond refuses an ambiguous or missing decision — confirming is never the default', async () => {
|
|
650
|
+
const both = makeCtx();
|
|
651
|
+
expect(await runCli(['confirmations', 'respond', 'cnf_1', '--token', 't', '--confirm', '--decline'], both)).toBe(1);
|
|
652
|
+
expect(both.errors.at(-1)).toContain('exactly one of --confirm or --decline');
|
|
653
|
+
const neither = makeCtx();
|
|
654
|
+
expect(await runCli(['confirmations', 'respond', 'cnf_1', '--token', 't'], neither)).toBe(1);
|
|
655
|
+
expect(neither.errors.at(-1)).toContain('exactly one of --confirm or --decline');
|
|
656
|
+
expect(neither.client.respondToConfirmation).not.toHaveBeenCalled();
|
|
657
|
+
});
|
|
658
|
+
it('preview/respond still need the token', async () => {
|
|
659
|
+
const ctx = makeCtx();
|
|
660
|
+
expect(await runCli(['confirmations', 'preview', 'cnf_1'], ctx)).toBe(1);
|
|
661
|
+
expect(ctx.errors.at(-1)).toContain('--token');
|
|
662
|
+
});
|
|
663
|
+
it('unknown subcommand exits 1', async () => {
|
|
664
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
665
|
+
expect(await runCli(['confirmations', 'frob'], ctx)).toBe(1);
|
|
666
|
+
expect(ctx.errors.at(-1)).toContain('unknown confirmations subcommand');
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
describe('threshold policies', () => {
|
|
670
|
+
it('create builds a subject-scoped score policy', async () => {
|
|
671
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
672
|
+
expect(await runCli(['policies', 'create', '--name', 'Watch 60', '--user', 'w1', '--metric', 'score', '--direction', 'down', '--threshold', '60'], ctx)).toBe(0);
|
|
673
|
+
expect(ctx.client.createPolicy).toHaveBeenCalledWith({ name: 'Watch 60', metric: 'score', userId: 'w1', direction: 'down', threshold: 60 }, 'k');
|
|
674
|
+
});
|
|
675
|
+
it('create builds an --all band policy', async () => {
|
|
676
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
677
|
+
expect(await runCli(['policies', 'create', '--name', 'Any High Risk', '--all', '--metric', 'band', '--direction', 'enter', '--band', 'High Risk'], ctx)).toBe(0);
|
|
678
|
+
expect(ctx.client.createPolicy).toHaveBeenCalledWith({ name: 'Any High Risk', metric: 'band', appliesToAll: true, direction: 'enter', band: 'High Risk' }, 'k');
|
|
679
|
+
});
|
|
680
|
+
it('create refuses both or neither of --user / --all', async () => {
|
|
681
|
+
const both = makeCtx({ apiKey: 'k' });
|
|
682
|
+
expect(await runCli(['policies', 'create', '--name', 'n', '--metric', 'score', '--user', 'w1', '--all'], both)).toBe(1);
|
|
683
|
+
expect(both.errors.at(-1)).toContain('exactly one of --user');
|
|
684
|
+
const neither = makeCtx({ apiKey: 'k' });
|
|
685
|
+
expect(await runCli(['policies', 'create', '--name', 'n', '--metric', 'score'], neither)).toBe(1);
|
|
686
|
+
expect(neither.errors.at(-1)).toContain('exactly one of --user');
|
|
687
|
+
expect(neither.client.createPolicy).not.toHaveBeenCalled();
|
|
688
|
+
});
|
|
689
|
+
it('list/get/update/delete route through', async () => {
|
|
690
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
691
|
+
expect(await runCli(['policies', 'list', '--limit', '5', '--cursor', 'p0'], ctx)).toBe(0);
|
|
692
|
+
expect(ctx.client.listPolicies).toHaveBeenCalledWith('k', { limit: 5, cursor: 'p0' });
|
|
693
|
+
expect(await runCli(['policies', 'get', 'pol_1'], ctx)).toBe(0);
|
|
694
|
+
expect(ctx.client.getPolicy).toHaveBeenCalledWith('pol_1', 'k');
|
|
695
|
+
expect(await runCli(['policies', 'update', 'pol_1', '--threshold', '55', '--deactivate'], ctx)).toBe(0);
|
|
696
|
+
expect(ctx.client.updatePolicy).toHaveBeenCalledWith('pol_1', { threshold: 55, isActive: false }, 'k');
|
|
697
|
+
expect(await runCli(['policies', 'delete', 'pol_1'], ctx)).toBe(0);
|
|
698
|
+
expect(ctx.client.deletePolicy).toHaveBeenCalledWith('pol_1', 'k');
|
|
699
|
+
expect(JSON.parse(ctx.lines.at(-1))).toEqual({ deleted: true });
|
|
700
|
+
});
|
|
701
|
+
it('update needs at least one field, and refuses contradictory activation flags', async () => {
|
|
702
|
+
const empty = makeCtx({ apiKey: 'k' });
|
|
703
|
+
expect(await runCli(['policies', 'update', 'pol_1'], empty)).toBe(1);
|
|
704
|
+
expect(empty.errors.at(-1)).toContain('at least one field');
|
|
705
|
+
const contradictory = makeCtx({ apiKey: 'k' });
|
|
706
|
+
expect(await runCli(['policies', 'update', 'pol_1', '--activate', '--deactivate'], contradictory)).toBe(1);
|
|
707
|
+
expect(contradictory.errors.at(-1)).toContain('at most one of --activate');
|
|
708
|
+
});
|
|
709
|
+
it('needs a key', async () => {
|
|
710
|
+
const ctx = makeCtx();
|
|
711
|
+
expect(await runCli(['policies', 'list'], ctx)).toBe(1);
|
|
712
|
+
expect(ctx.errors[0]).toContain('CREDDA_API_KEY');
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
describe('Open Badges, verified profile and the professional record', () => {
|
|
716
|
+
it('badges list/get are public — no key needed', async () => {
|
|
717
|
+
const ctx = makeCtx();
|
|
718
|
+
expect(await runCli(['badges', 'list'], ctx)).toBe(0);
|
|
719
|
+
expect(ctx.client.getOpenBadgeAchievements).toHaveBeenCalledWith();
|
|
720
|
+
expect(await runCli(['badges', 'get', 'first-delivery'], ctx)).toBe(0);
|
|
721
|
+
expect(ctx.client.getOpenBadgeAchievement).toHaveBeenCalledWith('first-delivery');
|
|
722
|
+
});
|
|
723
|
+
it('verified-profile reads the measure with the key', async () => {
|
|
724
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
725
|
+
expect(await runCli(['verified-profile', 'u1'], ctx)).toBe(0);
|
|
726
|
+
expect(ctx.client.getVerifiedProfile).toHaveBeenCalledWith('u1', 'k');
|
|
727
|
+
});
|
|
728
|
+
it('qualify records a claim and passes the witness through', async () => {
|
|
729
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
730
|
+
expect(await runCli(['qualify', 'u1', '--category', 'certification', '--label', 'AWS SA', '--issuer', 'AWS', '--verified-by', 'aws-training'], ctx)).toBe(0);
|
|
731
|
+
expect(ctx.client.recordQualification).toHaveBeenCalledWith('u1', { category: 'certification', label: 'AWS SA', issuer: 'AWS', verifiedBy: 'aws-training' }, 'k');
|
|
732
|
+
});
|
|
733
|
+
it('qualify with no witness still records the claim (the API downgrades it, the CLI does not block)', async () => {
|
|
734
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
735
|
+
expect(await runCli(['qualify', 'u1', '--category', 'skill', '--label', 'TypeScript'], ctx)).toBe(0);
|
|
736
|
+
expect(ctx.client.recordQualification).toHaveBeenCalledWith('u1', { category: 'skill', label: 'TypeScript', issuer: undefined, verifiedBy: undefined }, 'k');
|
|
737
|
+
});
|
|
738
|
+
it('qualify requires --category', async () => {
|
|
739
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
740
|
+
expect(await runCli(['qualify', 'u1'], ctx)).toBe(1);
|
|
741
|
+
expect(ctx.errors.at(-1)).toContain('--category');
|
|
742
|
+
expect(ctx.client.recordQualification).not.toHaveBeenCalled();
|
|
743
|
+
});
|
|
744
|
+
it('professional-record get/credential are keyed; public is not', async () => {
|
|
745
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
746
|
+
expect(await runCli(['professional-record', 'get', 'u1'], ctx)).toBe(0);
|
|
747
|
+
expect(ctx.client.getProfessionalRecord).toHaveBeenCalledWith('u1', 'k');
|
|
748
|
+
expect(await runCli(['professional-record', 'credential', 'u1', '--ttl', '3600'], ctx)).toBe(0);
|
|
749
|
+
expect(ctx.client.mintProfessionalRecordCredential).toHaveBeenCalledWith('u1', 'k', { ttlSeconds: 3600 });
|
|
750
|
+
const keyless = makeCtx();
|
|
751
|
+
expect(await runCli(['professional-record', 'public', 'tok_1'], keyless)).toBe(0);
|
|
752
|
+
expect(keyless.client.getPublicProfessionalRecord).toHaveBeenCalledWith('tok_1');
|
|
753
|
+
expect(keyless.errors.join('\n')).not.toContain('CREDDA_API_KEY');
|
|
754
|
+
});
|
|
755
|
+
it('professional-record credential without --ttl sends no options', async () => {
|
|
756
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
757
|
+
expect(await runCli(['professional-record', 'credential', 'u1'], ctx)).toBe(0);
|
|
758
|
+
expect(ctx.client.mintProfessionalRecordCredential).toHaveBeenCalledWith('u1', 'k', {});
|
|
759
|
+
});
|
|
760
|
+
it('unknown subcommands exit 1', async () => {
|
|
761
|
+
const ctx = makeCtx({ apiKey: 'k' });
|
|
762
|
+
expect(await runCli(['badges', 'frob'], ctx)).toBe(1);
|
|
763
|
+
expect(ctx.errors.at(-1)).toContain('unknown badges subcommand');
|
|
764
|
+
expect(await runCli(['professional-record', 'frob'], ctx)).toBe(1);
|
|
765
|
+
expect(ctx.errors.at(-1)).toContain('unknown professional-record subcommand');
|
|
766
|
+
});
|
|
767
|
+
it('help documents the confirmation, policy and record commands', async () => {
|
|
768
|
+
const ctx = makeCtx();
|
|
769
|
+
await runCli(['help'], ctx);
|
|
770
|
+
const help = ctx.lines.join('\n');
|
|
771
|
+
for (const cmd of [
|
|
772
|
+
'credda confirmations create',
|
|
773
|
+
'credda confirmations respond',
|
|
774
|
+
'credda policies create',
|
|
775
|
+
'credda badges list',
|
|
776
|
+
'credda verified-profile',
|
|
777
|
+
'credda qualify',
|
|
778
|
+
'credda professional-record get',
|
|
779
|
+
]) {
|
|
780
|
+
expect(help).toContain(cmd);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
});
|
|
159
784
|
//# sourceMappingURL=cli.test.js.map
|