@uipath/data-fabric-tool 1.198.0 → 1.199.0-preview.104
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/dist/tool.js +774 -28
- package/package.json +2 -2
- package/src/commands/choice-sets.spec.ts +107 -4
- package/src/commands/choice-sets.ts +15 -3
- package/src/commands/directory.spec.ts +544 -0
- package/src/commands/directory.ts +325 -0
- package/src/commands/entities.spec.ts +239 -2
- package/src/commands/entities.ts +47 -3
- package/src/commands/records.spec.ts +119 -3
- package/src/commands/records.ts +83 -2
- package/src/commands/roles.spec.ts +153 -0
- package/src/commands/roles.ts +88 -0
- package/src/tool.ts +4 -0
- package/src/utils/validate.spec.ts +555 -0
- package/src/utils/validate.ts +611 -0
- package/tests/access-management.e2e.test.ts +347 -0
- package/tests/choice-sets-lifecycle.e2e.test.ts +741 -0
- package/tests/entities-lifecycle.e2e.test.ts +614 -0
- package/tests/files-management.e2e.test.ts +518 -0
- package/tests/records-management.e2e.test.ts +646 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
type E2EManifest,
|
|
4
|
+
MANIFEST_PATH,
|
|
5
|
+
} from "../../../tests/e2e-shared/global-setup";
|
|
6
|
+
import {
|
|
7
|
+
EXEC_TIMEOUT,
|
|
8
|
+
extractJson,
|
|
9
|
+
localCliRawWithArgs,
|
|
10
|
+
readManifest,
|
|
11
|
+
SETUP_TIMEOUT,
|
|
12
|
+
} from "../../../tests/e2e-shared/helpers";
|
|
13
|
+
|
|
14
|
+
const manifest = readManifest<E2EManifest>(MANIFEST_PATH);
|
|
15
|
+
const { tempDir } = manifest.sharedInstall;
|
|
16
|
+
|
|
17
|
+
// Owning e2e suite for scenarios/data-fabric-tool/choice-sets-lifecycle.md.
|
|
18
|
+
//
|
|
19
|
+
// Same runtime pattern as packages/integrationservice-tool/tests: `beforeAll`
|
|
20
|
+
// probes DF access and creates a shared scratch choice-set (used by S3 for
|
|
21
|
+
// list-values and by S11/S13/S15/S16 as the parent of scratch values). Each
|
|
22
|
+
// mutating scenario creates its own transient scratch and cleans it up inline
|
|
23
|
+
// so it doesn't rely on ordering. `afterAll` deletes the shared scratch.
|
|
24
|
+
//
|
|
25
|
+
// The command is `df choice-set-values ...`, NOT `df choice-sets values ...`
|
|
26
|
+
// — the two are separate top-level commands under `df`. Value-delete uses
|
|
27
|
+
// `--ids <id,id,...>` (comma-separated), not positional varargs.
|
|
28
|
+
|
|
29
|
+
// noExplicitAny is off for test files (biome.json); e2e parses dynamic CLI JSON.
|
|
30
|
+
type Json = any;
|
|
31
|
+
|
|
32
|
+
const AUTH_MARKERS = [
|
|
33
|
+
"forbidden",
|
|
34
|
+
"403",
|
|
35
|
+
"401",
|
|
36
|
+
"unauthorized",
|
|
37
|
+
"not logged in",
|
|
38
|
+
"auth_token",
|
|
39
|
+
"authentication",
|
|
40
|
+
"insufficient_scope",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function isAuthDenial(res: { stdout: string; exitCode: number }): boolean {
|
|
44
|
+
if (res.exitCode === 0) return false;
|
|
45
|
+
const parsed = tryParse(res.stdout);
|
|
46
|
+
const hay =
|
|
47
|
+
`${parsed.Message ?? ""} ${parsed.Instructions ?? ""}`.toLowerCase();
|
|
48
|
+
return AUTH_MARKERS.some((m) => hay.includes(m));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runDf(argv: string[]): { stdout: string; exitCode: number } {
|
|
52
|
+
const res = localCliRawWithArgs(tempDir, [
|
|
53
|
+
"df",
|
|
54
|
+
...argv,
|
|
55
|
+
"--output",
|
|
56
|
+
"json",
|
|
57
|
+
]);
|
|
58
|
+
return { stdout: res.stdout, exitCode: res.exitCode };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function cli(argv: string[]): Json {
|
|
62
|
+
const res = runDf(argv);
|
|
63
|
+
if (res.exitCode !== 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[df ${argv.join(" ")}] exited ${res.exitCode}\n${res.stdout}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return extractJson(res.stdout) as Json;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tryParse(stdout: string): Json {
|
|
72
|
+
try {
|
|
73
|
+
return extractJson(stdout) as Json;
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function expectFailure(
|
|
80
|
+
result: { stdout: string; exitCode: number },
|
|
81
|
+
label: string,
|
|
82
|
+
): Json {
|
|
83
|
+
if (result.exitCode !== 1) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[${label}] expected exit 1, got ${result.exitCode}. stdout:\n${result.stdout}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const parsed = extractJson(result.stdout) as Json;
|
|
89
|
+
expect(parsed.Result, label).toBe("Failure");
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function runOrc(argv: string[]): { stdout: string; exitCode: number } {
|
|
94
|
+
const res = localCliRawWithArgs(tempDir, [
|
|
95
|
+
"or",
|
|
96
|
+
...argv,
|
|
97
|
+
"--output",
|
|
98
|
+
"json",
|
|
99
|
+
]);
|
|
100
|
+
return { stdout: res.stdout, exitCode: res.exitCode };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let dfAccessDenied = false;
|
|
104
|
+
let scratchCsId: string | undefined;
|
|
105
|
+
// Scratch folder for S17 — created via `uip or folders create`, matches the
|
|
106
|
+
// repo-wide pattern (see e.g. orchestrator-tool/tests/assets.e2e.test.ts).
|
|
107
|
+
let scratchFolderKey: string | undefined;
|
|
108
|
+
// Unique-per-run suffix. Repo convention (see orchestrator-tool/tests/*)
|
|
109
|
+
// uses Date.now(); process.pid alone can repeat across sequential runs and
|
|
110
|
+
// collide with orphans a prior failed run left behind.
|
|
111
|
+
const RUN_ID = `${process.pid}_${Date.now()}`;
|
|
112
|
+
const scratchBaseName = `E2eDfCs${RUN_ID}`;
|
|
113
|
+
const scratchFolderName = `e2e-df-choice-sets-folder-${RUN_ID}`;
|
|
114
|
+
|
|
115
|
+
describe("df choice-sets — choice sets lifecycle", () => {
|
|
116
|
+
beforeAll(() => {
|
|
117
|
+
const probe = runDf(["choice-sets", "list"]);
|
|
118
|
+
if (probe.exitCode !== 0) {
|
|
119
|
+
dfAccessDenied = isAuthDenial(probe);
|
|
120
|
+
if (!dfAccessDenied) {
|
|
121
|
+
console.warn(
|
|
122
|
+
`[df choice-sets e2e] Probe failed for a non-auth reason; live scenarios will run and assert.\n${probe.stdout}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Shared scratch choice-set — doubles as the write-scope probe. Read
|
|
128
|
+
// passed but the token lacks choice-set-write scope → mark
|
|
129
|
+
// dfAccessDenied so every live test soft-skips together.
|
|
130
|
+
const create = runDf([
|
|
131
|
+
"choice-sets",
|
|
132
|
+
"create",
|
|
133
|
+
`${scratchBaseName}Shared`,
|
|
134
|
+
"--display-name",
|
|
135
|
+
`E2E Shared ${RUN_ID}`,
|
|
136
|
+
"--description",
|
|
137
|
+
"shared scratch for value tests",
|
|
138
|
+
]);
|
|
139
|
+
if (isAuthDenial(create)) {
|
|
140
|
+
dfAccessDenied = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (create.exitCode === 0) {
|
|
144
|
+
scratchCsId = (extractJson(create.stdout) as Json).Data?.Id;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Scratch folder for S17. Failure here (Orchestrator not enabled or
|
|
148
|
+
// no folder-create permission) leaves S17 in ctx.skip() — the rest
|
|
149
|
+
// of the suite is unaffected.
|
|
150
|
+
const createFolder = runOrc(["folders", "create", scratchFolderName]);
|
|
151
|
+
if (createFolder.exitCode === 0) {
|
|
152
|
+
scratchFolderKey = (extractJson(createFolder.stdout) as Json).Data
|
|
153
|
+
?.Key;
|
|
154
|
+
}
|
|
155
|
+
}, SETUP_TIMEOUT);
|
|
156
|
+
|
|
157
|
+
afterAll(() => {
|
|
158
|
+
if (scratchCsId) {
|
|
159
|
+
runDf([
|
|
160
|
+
"choice-sets",
|
|
161
|
+
"delete",
|
|
162
|
+
scratchCsId,
|
|
163
|
+
"--yes",
|
|
164
|
+
"--reason",
|
|
165
|
+
"e2e cleanup",
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
if (scratchFolderKey) {
|
|
169
|
+
runOrc(["folders", "delete", scratchFolderKey, "--yes"]);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── S1 ── list ──────────────────────────────────────────────────────────
|
|
174
|
+
it(
|
|
175
|
+
"S1: list tenant-level choice sets",
|
|
176
|
+
(ctx) => {
|
|
177
|
+
if (dfAccessDenied) return ctx.skip();
|
|
178
|
+
const out = cli(["choice-sets", "list"]);
|
|
179
|
+
expect(out.Result).toBe("Success");
|
|
180
|
+
expect(out.Code).toBe("ChoiceSetList");
|
|
181
|
+
expect(Array.isArray(out.Data)).toBe(true);
|
|
182
|
+
},
|
|
183
|
+
EXEC_TIMEOUT,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// ── S2 ── folder-scope guard on list, pre-network ───────────────────────
|
|
187
|
+
it(
|
|
188
|
+
"S2: --folder-key and --include-folders are mutually exclusive on list",
|
|
189
|
+
() => {
|
|
190
|
+
const parsed = expectFailure(
|
|
191
|
+
runDf([
|
|
192
|
+
"choice-sets",
|
|
193
|
+
"list",
|
|
194
|
+
"--folder-key",
|
|
195
|
+
"pre-network",
|
|
196
|
+
"--include-folders",
|
|
197
|
+
]),
|
|
198
|
+
"list mutex",
|
|
199
|
+
);
|
|
200
|
+
expect(parsed.Message).toBe(
|
|
201
|
+
"--folder-key and --include-folders are mutually exclusive",
|
|
202
|
+
);
|
|
203
|
+
expect(parsed.Instructions.length).toBeGreaterThan(0);
|
|
204
|
+
},
|
|
205
|
+
EXEC_TIMEOUT,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// ── S3 ── list-values on the shared scratch (empty is fine) ─────────────
|
|
209
|
+
it(
|
|
210
|
+
"S3: list the values inside a choice set with pagination",
|
|
211
|
+
(ctx) => {
|
|
212
|
+
if (dfAccessDenied) return ctx.skip();
|
|
213
|
+
if (!scratchCsId) return ctx.skip();
|
|
214
|
+
const out = cli([
|
|
215
|
+
"choice-sets",
|
|
216
|
+
"list-values",
|
|
217
|
+
scratchCsId,
|
|
218
|
+
"--limit",
|
|
219
|
+
"50",
|
|
220
|
+
]);
|
|
221
|
+
expect(out.Result).toBe("Success");
|
|
222
|
+
expect(out.Code).toBe("ChoiceSetValues");
|
|
223
|
+
},
|
|
224
|
+
EXEC_TIMEOUT,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// ── S4 ── list-values paging guards, pre-network ────────────────────────
|
|
228
|
+
it(
|
|
229
|
+
"S4: list-values rejects invalid paging before the API call",
|
|
230
|
+
() => {
|
|
231
|
+
const rows: Array<{
|
|
232
|
+
label: string;
|
|
233
|
+
argv: string[];
|
|
234
|
+
message: string;
|
|
235
|
+
}> = [
|
|
236
|
+
{
|
|
237
|
+
label: "--limit 0",
|
|
238
|
+
argv: [
|
|
239
|
+
"choice-sets",
|
|
240
|
+
"list-values",
|
|
241
|
+
"pre-network",
|
|
242
|
+
"--limit",
|
|
243
|
+
"0",
|
|
244
|
+
],
|
|
245
|
+
message: "Invalid --limit value",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
label: "--limit abc",
|
|
249
|
+
argv: [
|
|
250
|
+
"choice-sets",
|
|
251
|
+
"list-values",
|
|
252
|
+
"pre-network",
|
|
253
|
+
"--limit",
|
|
254
|
+
"abc",
|
|
255
|
+
],
|
|
256
|
+
message: "Invalid --limit value",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
label: "--offset -5",
|
|
260
|
+
argv: [
|
|
261
|
+
"choice-sets",
|
|
262
|
+
"list-values",
|
|
263
|
+
"pre-network",
|
|
264
|
+
"--offset=-5",
|
|
265
|
+
],
|
|
266
|
+
message: "Invalid --offset value",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
label: "--offset + --cursor",
|
|
270
|
+
argv: [
|
|
271
|
+
"choice-sets",
|
|
272
|
+
"list-values",
|
|
273
|
+
"pre-network",
|
|
274
|
+
"--offset",
|
|
275
|
+
"5",
|
|
276
|
+
"--cursor",
|
|
277
|
+
"XYZ",
|
|
278
|
+
],
|
|
279
|
+
message: "--offset and --cursor are mutually exclusive",
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
for (const row of rows) {
|
|
284
|
+
const parsed = expectFailure(runDf(row.argv), row.label);
|
|
285
|
+
expect(parsed.Message, row.label).toBe(row.message);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
EXEC_TIMEOUT,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// ── S5 ── create a transient scratch choice-set ─────────────────────────
|
|
292
|
+
it(
|
|
293
|
+
"S5: create a tenant-level choice set",
|
|
294
|
+
(ctx) => {
|
|
295
|
+
if (dfAccessDenied) return ctx.skip();
|
|
296
|
+
const name = `${scratchBaseName}S5`;
|
|
297
|
+
const created = cli([
|
|
298
|
+
"choice-sets",
|
|
299
|
+
"create",
|
|
300
|
+
name,
|
|
301
|
+
"--display-name",
|
|
302
|
+
`Priority ${RUN_ID}`,
|
|
303
|
+
"--description",
|
|
304
|
+
"S5 scratch",
|
|
305
|
+
]);
|
|
306
|
+
try {
|
|
307
|
+
expect(created.Result).toBe("Success");
|
|
308
|
+
expect(created.Code).toBe("ChoiceSetCreated");
|
|
309
|
+
expect(typeof created.Data.Id).toBe("string");
|
|
310
|
+
} finally {
|
|
311
|
+
if (created.Data?.Id) {
|
|
312
|
+
runDf([
|
|
313
|
+
"choice-sets",
|
|
314
|
+
"delete",
|
|
315
|
+
created.Data.Id,
|
|
316
|
+
"--yes",
|
|
317
|
+
"--reason",
|
|
318
|
+
"S5 cleanup",
|
|
319
|
+
]);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
EXEC_TIMEOUT,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// ── S6 ── create name validator, pre-network ────────────────────────────
|
|
327
|
+
it(
|
|
328
|
+
"S6: create rejects invalid names before the API call",
|
|
329
|
+
() => {
|
|
330
|
+
const rows: Array<{ name: string; message: string }> = [
|
|
331
|
+
{
|
|
332
|
+
name: "1abc",
|
|
333
|
+
message: "Invalid choice set name '1abc'",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "class",
|
|
337
|
+
message:
|
|
338
|
+
"Choice set name 'class' is a reserved C# or VB keyword",
|
|
339
|
+
},
|
|
340
|
+
{ name: "Id", message: "Invalid choice set name 'Id'" },
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
const parsed = expectFailure(
|
|
345
|
+
runDf(["choice-sets", "create", row.name]),
|
|
346
|
+
row.name,
|
|
347
|
+
);
|
|
348
|
+
expect(parsed.Message, row.name).toBe(row.message);
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
EXEC_TIMEOUT,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// ── S7 ── update both-required guard, pre-network ───────────────────────
|
|
355
|
+
it(
|
|
356
|
+
"S7: update requires both --display-name and --description",
|
|
357
|
+
() => {
|
|
358
|
+
const message =
|
|
359
|
+
"choice-sets update requires both --display-name and --description";
|
|
360
|
+
|
|
361
|
+
for (const argv of [
|
|
362
|
+
["choice-sets", "update", "pre-network", "--display-name", "X"],
|
|
363
|
+
["choice-sets", "update", "pre-network", "--description", "Y"],
|
|
364
|
+
["choice-sets", "update", "pre-network"],
|
|
365
|
+
]) {
|
|
366
|
+
const parsed = expectFailure(runDf(argv), argv.join(" "));
|
|
367
|
+
expect(parsed.Message, argv.join(" ")).toBe(message);
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
EXEC_TIMEOUT,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// ── S8 ── update a transient scratch with both fields ───────────────────
|
|
374
|
+
it(
|
|
375
|
+
"S8: update a choice set with both fields",
|
|
376
|
+
(ctx) => {
|
|
377
|
+
if (dfAccessDenied) return ctx.skip();
|
|
378
|
+
const name = `${scratchBaseName}S8`;
|
|
379
|
+
const created = cli([
|
|
380
|
+
"choice-sets",
|
|
381
|
+
"create",
|
|
382
|
+
name,
|
|
383
|
+
"--display-name",
|
|
384
|
+
`Old ${RUN_ID}`,
|
|
385
|
+
"--description",
|
|
386
|
+
"Old desc",
|
|
387
|
+
]);
|
|
388
|
+
const id: string = created.Data.Id;
|
|
389
|
+
try {
|
|
390
|
+
const out = cli([
|
|
391
|
+
"choice-sets",
|
|
392
|
+
"update",
|
|
393
|
+
id,
|
|
394
|
+
"--display-name",
|
|
395
|
+
`New ${RUN_ID}`,
|
|
396
|
+
"--description",
|
|
397
|
+
"New desc",
|
|
398
|
+
]);
|
|
399
|
+
expect(out.Result).toBe("Success");
|
|
400
|
+
expect(out.Code).toBe("ChoiceSetUpdated");
|
|
401
|
+
expect(out.Data.Id).toBe(id);
|
|
402
|
+
} finally {
|
|
403
|
+
runDf([
|
|
404
|
+
"choice-sets",
|
|
405
|
+
"delete",
|
|
406
|
+
id,
|
|
407
|
+
"--yes",
|
|
408
|
+
"--reason",
|
|
409
|
+
"S8 cleanup",
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
EXEC_TIMEOUT,
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// ── S9 ── delete confirmation guard, pre-network ────────────────────────
|
|
417
|
+
it(
|
|
418
|
+
"S9: delete requires confirmation and a reason",
|
|
419
|
+
() => {
|
|
420
|
+
const noFlags = expectFailure(
|
|
421
|
+
runDf(["choice-sets", "delete", "pre-network"]),
|
|
422
|
+
"no flags",
|
|
423
|
+
);
|
|
424
|
+
expect(noFlags.Message).toContain("Confirmation required");
|
|
425
|
+
|
|
426
|
+
const yesNoReason = expectFailure(
|
|
427
|
+
runDf(["choice-sets", "delete", "pre-network", "--yes"]),
|
|
428
|
+
"--yes without --reason",
|
|
429
|
+
);
|
|
430
|
+
expect(yesNoReason.Message).toBe(
|
|
431
|
+
"Reason required for destructive operation",
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const reasonNoYes = expectFailure(
|
|
435
|
+
runDf([
|
|
436
|
+
"choice-sets",
|
|
437
|
+
"delete",
|
|
438
|
+
"pre-network",
|
|
439
|
+
"--reason",
|
|
440
|
+
"cleanup",
|
|
441
|
+
]),
|
|
442
|
+
"--reason without --yes",
|
|
443
|
+
);
|
|
444
|
+
expect(reasonNoYes.Message).toContain("Confirmation required");
|
|
445
|
+
},
|
|
446
|
+
EXEC_TIMEOUT,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// ── S10 ── delete a transient scratch ───────────────────────────────────
|
|
450
|
+
it(
|
|
451
|
+
"S10: delete a choice set",
|
|
452
|
+
(ctx) => {
|
|
453
|
+
if (dfAccessDenied) return ctx.skip();
|
|
454
|
+
const name = `${scratchBaseName}S10`;
|
|
455
|
+
const created = cli([
|
|
456
|
+
"choice-sets",
|
|
457
|
+
"create",
|
|
458
|
+
name,
|
|
459
|
+
"--display-name",
|
|
460
|
+
`For delete ${RUN_ID}`,
|
|
461
|
+
"--description",
|
|
462
|
+
"S10 scratch",
|
|
463
|
+
]);
|
|
464
|
+
const id: string = created.Data.Id;
|
|
465
|
+
const out = cli([
|
|
466
|
+
"choice-sets",
|
|
467
|
+
"delete",
|
|
468
|
+
id,
|
|
469
|
+
"--yes",
|
|
470
|
+
"--reason",
|
|
471
|
+
"S10 test",
|
|
472
|
+
]);
|
|
473
|
+
expect(out.Result).toBe("Success");
|
|
474
|
+
expect(out.Code).toBe("ChoiceSetDeleted");
|
|
475
|
+
expect(out.Data.Id).toBe(id);
|
|
476
|
+
expect(out.Data.Reason).toBe("S10 test");
|
|
477
|
+
},
|
|
478
|
+
EXEC_TIMEOUT,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// ── S11 ── add a value on the shared scratch ────────────────────────────
|
|
482
|
+
it(
|
|
483
|
+
"S11: add a value to a choice set",
|
|
484
|
+
(ctx) => {
|
|
485
|
+
if (dfAccessDenied) return ctx.skip();
|
|
486
|
+
if (!scratchCsId) return ctx.skip();
|
|
487
|
+
const created = cli([
|
|
488
|
+
"choice-set-values",
|
|
489
|
+
"create",
|
|
490
|
+
scratchCsId,
|
|
491
|
+
`s11_${process.pid}`,
|
|
492
|
+
"--display-name",
|
|
493
|
+
"S11",
|
|
494
|
+
]);
|
|
495
|
+
try {
|
|
496
|
+
expect(created.Result).toBe("Success");
|
|
497
|
+
expect(created.Code).toBe("ChoiceSetValueCreated");
|
|
498
|
+
expect(typeof created.Data.Id).toBe("string");
|
|
499
|
+
} finally {
|
|
500
|
+
if (created.Data?.Id) {
|
|
501
|
+
runDf([
|
|
502
|
+
"choice-set-values",
|
|
503
|
+
"delete",
|
|
504
|
+
scratchCsId,
|
|
505
|
+
"--ids",
|
|
506
|
+
created.Data.Id,
|
|
507
|
+
"--yes",
|
|
508
|
+
"--reason",
|
|
509
|
+
"S11 cleanup",
|
|
510
|
+
]);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
EXEC_TIMEOUT,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// ── S12 ── value-create name validator, pre-network ─────────────────────
|
|
518
|
+
it(
|
|
519
|
+
"S12: value-create rejects invalid names before the API call",
|
|
520
|
+
() => {
|
|
521
|
+
const rows: Array<{ name: string; message: string }> = [
|
|
522
|
+
{
|
|
523
|
+
name: "1abc",
|
|
524
|
+
message: "Invalid choice-set value name '1abc'",
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "internal",
|
|
528
|
+
message:
|
|
529
|
+
"Choice-set value name 'internal' is a reserved C# keyword",
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
for (const row of rows) {
|
|
534
|
+
const parsed = expectFailure(
|
|
535
|
+
runDf([
|
|
536
|
+
"choice-set-values",
|
|
537
|
+
"create",
|
|
538
|
+
"pre-network",
|
|
539
|
+
row.name,
|
|
540
|
+
]),
|
|
541
|
+
row.name,
|
|
542
|
+
);
|
|
543
|
+
expect(parsed.Message, row.name).toBe(row.message);
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
EXEC_TIMEOUT,
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// ── S13 ── update a value's display name on a transient value ───────────
|
|
550
|
+
it(
|
|
551
|
+
"S13: update a value's display name",
|
|
552
|
+
(ctx) => {
|
|
553
|
+
if (dfAccessDenied) return ctx.skip();
|
|
554
|
+
if (!scratchCsId) return ctx.skip();
|
|
555
|
+
const valueName = `s13_${process.pid}`;
|
|
556
|
+
const created = cli([
|
|
557
|
+
"choice-set-values",
|
|
558
|
+
"create",
|
|
559
|
+
scratchCsId,
|
|
560
|
+
valueName,
|
|
561
|
+
"--display-name",
|
|
562
|
+
"S13 Old",
|
|
563
|
+
]);
|
|
564
|
+
const valueId: string = created.Data.Id;
|
|
565
|
+
try {
|
|
566
|
+
const out = cli([
|
|
567
|
+
"choice-set-values",
|
|
568
|
+
"update",
|
|
569
|
+
scratchCsId,
|
|
570
|
+
valueId,
|
|
571
|
+
"S13 New",
|
|
572
|
+
]);
|
|
573
|
+
expect(out.Result).toBe("Success");
|
|
574
|
+
expect(out.Code).toBe("ChoiceSetValueUpdated");
|
|
575
|
+
expect(out.Data.DisplayName).toBe("S13 New");
|
|
576
|
+
} finally {
|
|
577
|
+
runDf([
|
|
578
|
+
"choice-set-values",
|
|
579
|
+
"delete",
|
|
580
|
+
scratchCsId,
|
|
581
|
+
"--ids",
|
|
582
|
+
valueId,
|
|
583
|
+
"--yes",
|
|
584
|
+
"--reason",
|
|
585
|
+
"S13 cleanup",
|
|
586
|
+
]);
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
EXEC_TIMEOUT,
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// ── S14 ── value-delete missing --ids, pre-network ──────────────────────
|
|
593
|
+
it(
|
|
594
|
+
"S14: value-delete rejects missing --ids before the API call",
|
|
595
|
+
() => {
|
|
596
|
+
const parsed = expectFailure(
|
|
597
|
+
runDf(["choice-set-values", "delete", "pre-network"]),
|
|
598
|
+
"missing --ids",
|
|
599
|
+
);
|
|
600
|
+
expect(parsed.Message).toBe("No value IDs provided");
|
|
601
|
+
},
|
|
602
|
+
EXEC_TIMEOUT,
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// ── S15 ── value-delete confirmation guard, pre-network ─────────────────
|
|
606
|
+
it(
|
|
607
|
+
"S15: value-delete requires confirmation and a reason",
|
|
608
|
+
() => {
|
|
609
|
+
const base = [
|
|
610
|
+
"choice-set-values",
|
|
611
|
+
"delete",
|
|
612
|
+
"pre-network",
|
|
613
|
+
"--ids",
|
|
614
|
+
"v-pre-network",
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
const noFlags = expectFailure(runDf(base), "no flags");
|
|
618
|
+
expect(noFlags.Message).toContain("Confirmation required");
|
|
619
|
+
|
|
620
|
+
const yesNoReason = expectFailure(
|
|
621
|
+
runDf([...base, "--yes"]),
|
|
622
|
+
"--yes without --reason",
|
|
623
|
+
);
|
|
624
|
+
expect(yesNoReason.Message).toBe(
|
|
625
|
+
"Reason required for destructive operation",
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
const reasonNoYes = expectFailure(
|
|
629
|
+
runDf([...base, "--reason", "renamed"]),
|
|
630
|
+
"--reason without --yes",
|
|
631
|
+
);
|
|
632
|
+
expect(reasonNoYes.Message).toContain("Confirmation required");
|
|
633
|
+
},
|
|
634
|
+
EXEC_TIMEOUT,
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
// ── S16 ── delete transient values on the shared scratch ────────────────
|
|
638
|
+
it(
|
|
639
|
+
"S16: delete one or more values with a shared confirmation",
|
|
640
|
+
(ctx) => {
|
|
641
|
+
if (dfAccessDenied) return ctx.skip();
|
|
642
|
+
if (!scratchCsId) return ctx.skip();
|
|
643
|
+
const a = cli([
|
|
644
|
+
"choice-set-values",
|
|
645
|
+
"create",
|
|
646
|
+
scratchCsId,
|
|
647
|
+
`s16a_${process.pid}`,
|
|
648
|
+
"--display-name",
|
|
649
|
+
"S16a",
|
|
650
|
+
]);
|
|
651
|
+
const b = cli([
|
|
652
|
+
"choice-set-values",
|
|
653
|
+
"create",
|
|
654
|
+
scratchCsId,
|
|
655
|
+
`s16b_${process.pid}`,
|
|
656
|
+
"--display-name",
|
|
657
|
+
"S16b",
|
|
658
|
+
]);
|
|
659
|
+
const ids = `${a.Data.Id},${b.Data.Id}`;
|
|
660
|
+
const out = cli([
|
|
661
|
+
"choice-set-values",
|
|
662
|
+
"delete",
|
|
663
|
+
scratchCsId,
|
|
664
|
+
"--ids",
|
|
665
|
+
ids,
|
|
666
|
+
"--yes",
|
|
667
|
+
"--reason",
|
|
668
|
+
"S16 test",
|
|
669
|
+
]);
|
|
670
|
+
expect(out.Result).toBe("Success");
|
|
671
|
+
expect(out.Code).toBe("ChoiceSetValuesDeleted");
|
|
672
|
+
expect(out.Data.ChoiceSetId).toBe(scratchCsId);
|
|
673
|
+
expect(out.Data.DeletedIds).toEqual([a.Data.Id, b.Data.Id]);
|
|
674
|
+
expect(out.Data.Reason).toBe("S16 test");
|
|
675
|
+
},
|
|
676
|
+
EXEC_TIMEOUT,
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
// ── S17 ── create + delete a folder-scoped choice set ───────────────────
|
|
680
|
+
it(
|
|
681
|
+
"S17: create and delete a folder-scoped choice set",
|
|
682
|
+
(ctx) => {
|
|
683
|
+
if (dfAccessDenied) return ctx.skip();
|
|
684
|
+
if (!scratchFolderKey) return ctx.skip();
|
|
685
|
+
|
|
686
|
+
const name = `${scratchBaseName}S17`;
|
|
687
|
+
const created = cli([
|
|
688
|
+
"choice-sets",
|
|
689
|
+
"create",
|
|
690
|
+
name,
|
|
691
|
+
"--display-name",
|
|
692
|
+
`Folder-scoped S17 ${RUN_ID}`,
|
|
693
|
+
"--description",
|
|
694
|
+
"S17 scratch",
|
|
695
|
+
"--folder-key",
|
|
696
|
+
scratchFolderKey,
|
|
697
|
+
]);
|
|
698
|
+
const id: string = created.Data.Id;
|
|
699
|
+
try {
|
|
700
|
+
expect(created.Result).toBe("Success");
|
|
701
|
+
expect(created.Code).toBe("ChoiceSetCreated");
|
|
702
|
+
|
|
703
|
+
const deleted = cli([
|
|
704
|
+
"choice-sets",
|
|
705
|
+
"delete",
|
|
706
|
+
id,
|
|
707
|
+
"--folder-key",
|
|
708
|
+
scratchFolderKey,
|
|
709
|
+
"--yes",
|
|
710
|
+
"--reason",
|
|
711
|
+
"S17 cleanup",
|
|
712
|
+
]);
|
|
713
|
+
expect(deleted.Result).toBe("Success");
|
|
714
|
+
expect(deleted.Code).toBe("ChoiceSetDeleted");
|
|
715
|
+
|
|
716
|
+
const list = cli([
|
|
717
|
+
"choice-sets",
|
|
718
|
+
"list",
|
|
719
|
+
"--folder-key",
|
|
720
|
+
scratchFolderKey,
|
|
721
|
+
]);
|
|
722
|
+
const ids = (list.Data as Json[]).map((c) => c.Id);
|
|
723
|
+
expect(ids).not.toContain(id);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
// Best-effort cleanup if an assertion failed after create.
|
|
726
|
+
runDf([
|
|
727
|
+
"choice-sets",
|
|
728
|
+
"delete",
|
|
729
|
+
id,
|
|
730
|
+
"--folder-key",
|
|
731
|
+
scratchFolderKey,
|
|
732
|
+
"--yes",
|
|
733
|
+
"--reason",
|
|
734
|
+
"S17 cleanup after failure",
|
|
735
|
+
]);
|
|
736
|
+
throw err;
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
EXEC_TIMEOUT,
|
|
740
|
+
);
|
|
741
|
+
});
|