@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.
@@ -0,0 +1,347 @@
1
+ import { 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
+ localCli,
10
+ localCliRawWithArgs,
11
+ readManifest,
12
+ SETUP_TIMEOUT,
13
+ } from "../../../tests/e2e-shared/helpers";
14
+
15
+ const manifest = readManifest<E2EManifest>(MANIFEST_PATH);
16
+ const { tempDir } = manifest.sharedInstall;
17
+
18
+ // Owning e2e suite for scenarios/data-fabric-tool/access-management.md.
19
+ //
20
+ // Every S<n> in that scenario file maps one-to-one to an it()/it.skip() here
21
+ // (see scenarios/README.md — "Scenario ↔ e2e mapping"). Guard scenarios
22
+ // (S4, S7, S8, S10) fail before connectOrFail and run against the shared
23
+ // install without tenant permissions. Read-only live scenarios (S1, S2, S3)
24
+ // use a beforeAll probe + ctx.skip() when the token lacks DF manage-access.
25
+ // Mutating scenarios (S5, S6, S9) stay as it.skip — the CLI can't provision
26
+ // a scratch principal (identity-side, no OAuth from a test), and mutating a
27
+ // real user's role set isn't safe.
28
+
29
+ const PRINCIPAL_ID = "00000000-0000-0000-0000-000000000001";
30
+
31
+ interface FailureOutput {
32
+ Result: string;
33
+ Message: string;
34
+ Instructions?: string;
35
+ }
36
+
37
+ // noExplicitAny is off for test files; e2e parses dynamic CLI JSON.
38
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic CLI JSON
39
+ type Json = any;
40
+
41
+ const AUTH_MARKERS = [
42
+ "forbidden",
43
+ "403",
44
+ "401",
45
+ "unauthorized",
46
+ "not logged in",
47
+ "auth_token",
48
+ "authentication",
49
+ "insufficient_scope",
50
+ ];
51
+
52
+ function tryParse(stdout: string): Json {
53
+ try {
54
+ return extractJson(stdout) as Json;
55
+ } catch {
56
+ return {};
57
+ }
58
+ }
59
+
60
+ function isAuthDenial(res: { stdout: string; exitCode: number }): boolean {
61
+ if (res.exitCode === 0) return false;
62
+ const parsed = tryParse(res.stdout);
63
+ const hay =
64
+ `${parsed.Message ?? ""} ${parsed.Instructions ?? ""}`.toLowerCase();
65
+ return AUTH_MARKERS.some((m) => hay.includes(m));
66
+ }
67
+
68
+ let dfAccessDenied = false;
69
+
70
+ describe("df access management scenario", () => {
71
+ beforeAll(() => {
72
+ const probe = localCliRawWithArgs(tempDir, [
73
+ "df",
74
+ "roles",
75
+ "list",
76
+ "--output",
77
+ "json",
78
+ ]);
79
+ if (probe.exitCode !== 0) {
80
+ dfAccessDenied = isAuthDenial(probe);
81
+ if (!dfAccessDenied) {
82
+ console.warn(
83
+ `[df access-management e2e] Probe failed for a non-auth reason; live scenarios will run and assert.\n${probe.stdout}`,
84
+ );
85
+ }
86
+ }
87
+ }, SETUP_TIMEOUT);
88
+
89
+ // ── S1 ──────────────────────────────────────────────────────────────────
90
+ it(
91
+ "S1: list access roles with statistics",
92
+ (ctx) => {
93
+ if (dfAccessDenied) return ctx.skip();
94
+ const { stdout, exitCode } = localCliRawWithArgs(tempDir, [
95
+ "df",
96
+ "roles",
97
+ "list",
98
+ "--output",
99
+ "json",
100
+ ]);
101
+ expect(exitCode).toBe(0);
102
+ const out = extractJson(stdout) as Json;
103
+ expect(out.Result).toBe("Success");
104
+ expect(out.Code).toBe("RoleList");
105
+ expect(Array.isArray(out.Data)).toBe(true);
106
+ for (const role of out.Data as Json[]) {
107
+ expect(role).toHaveProperty("Id");
108
+ expect(role).toHaveProperty("Name");
109
+ expect(role).toHaveProperty("Type");
110
+ // Stats-on response carries the count.
111
+ expect(role).toHaveProperty("DirectoryEntityCount");
112
+ }
113
+ },
114
+ EXEC_TIMEOUT,
115
+ );
116
+
117
+ // ── S2 ──────────────────────────────────────────────────────────────────
118
+ it(
119
+ "S2: list access roles without statistics",
120
+ (ctx) => {
121
+ if (dfAccessDenied) return ctx.skip();
122
+ const { stdout, exitCode } = localCliRawWithArgs(tempDir, [
123
+ "df",
124
+ "roles",
125
+ "list",
126
+ "--no-stats",
127
+ "--output",
128
+ "json",
129
+ ]);
130
+ expect(exitCode).toBe(0);
131
+ const out = extractJson(stdout) as Json;
132
+ expect(out.Result).toBe("Success");
133
+ expect(out.Code).toBe("RoleList");
134
+ expect(Array.isArray(out.Data)).toBe(true);
135
+ for (const role of out.Data as Json[]) {
136
+ expect(role.DirectoryEntityCount).toBeUndefined();
137
+ }
138
+ },
139
+ EXEC_TIMEOUT,
140
+ );
141
+
142
+ // ── S3 ──────────────────────────────────────────────────────────────────
143
+ it(
144
+ "S3: list directory principals with their roles",
145
+ (ctx) => {
146
+ if (dfAccessDenied) return ctx.skip();
147
+ const { stdout, exitCode } = localCliRawWithArgs(tempDir, [
148
+ "df",
149
+ "directory",
150
+ "list",
151
+ "--limit",
152
+ "50",
153
+ "--offset",
154
+ "0",
155
+ "--output",
156
+ "json",
157
+ ]);
158
+ expect(exitCode).toBe(0);
159
+ const out = extractJson(stdout) as Json;
160
+ expect(out.Result).toBe("Success");
161
+ expect(out.Code).toBe("DirectoryList");
162
+ expect(typeof out.Data.TotalCount).toBe("number");
163
+ expect(Array.isArray(out.Data.Results)).toBe(true);
164
+ for (const p of out.Data.Results as Json[]) {
165
+ expect(p).toHaveProperty("ExternalId");
166
+ expect(p).toHaveProperty("Name");
167
+ expect(p).toHaveProperty("Type");
168
+ expect(Array.isArray(p.Roles)).toBe(true);
169
+ expect(typeof p.IsUIEnabled).toBe("boolean");
170
+ }
171
+ },
172
+ EXEC_TIMEOUT,
173
+ );
174
+
175
+ // ── S4 ──────────────────────────────────────────────────────────────────
176
+ it(
177
+ "S4: invalid paging values fail before any network call",
178
+ () => {
179
+ const rows: Array<{
180
+ limit: string;
181
+ offset: string;
182
+ message: string;
183
+ }> = [
184
+ {
185
+ limit: "0",
186
+ offset: "0",
187
+ message: "Invalid --limit value '0'",
188
+ },
189
+ {
190
+ limit: "abc",
191
+ offset: "0",
192
+ message: "Invalid --limit value 'abc'",
193
+ },
194
+ {
195
+ limit: "50",
196
+ offset: "-5",
197
+ message: "Invalid --offset value '-5'",
198
+ },
199
+ ];
200
+ for (const { limit, offset, message } of rows) {
201
+ const { stdout, exitCode } = localCliRawWithArgs(tempDir, [
202
+ "df",
203
+ "directory",
204
+ "list",
205
+ "--limit",
206
+ limit,
207
+ `--offset=${offset}`,
208
+ "--output",
209
+ "json",
210
+ ]);
211
+ expect(exitCode, `--limit ${limit} --offset ${offset}`).toBe(1);
212
+ const result = extractJson(stdout) as FailureOutput;
213
+ expect(result.Result).toBe("Failure");
214
+ expect(result.Message).toBe(message);
215
+ }
216
+ },
217
+ EXEC_TIMEOUT,
218
+ );
219
+
220
+ // ── S5 ──────────────────────────────────────────────────────────────────
221
+ it.skip("S5: assign a role to a principal preserving existing roles", () => {});
222
+
223
+ // ── S6 ──────────────────────────────────────────────────────────────────
224
+ it.skip("S6: replace a principal's role set", () => {});
225
+
226
+ // ── S7 ──────────────────────────────────────────────────────────────────
227
+ it(
228
+ "S7: assign with invalid input fails before any network call",
229
+ () => {
230
+ const rows: Array<{ type: string; ids: string; message: string }> =
231
+ [
232
+ {
233
+ type: "Robot",
234
+ ids: "r-1",
235
+ message: "Invalid --type value 'Robot'",
236
+ },
237
+ {
238
+ type: "User",
239
+ ids: " , ",
240
+ message: "No role IDs provided",
241
+ },
242
+ ];
243
+ for (const { type, ids, message } of rows) {
244
+ const { stdout, exitCode } = localCliRawWithArgs(tempDir, [
245
+ "df",
246
+ "directory",
247
+ "assign-roles",
248
+ PRINCIPAL_ID,
249
+ "--type",
250
+ type,
251
+ "--role-ids",
252
+ ids,
253
+ "--output",
254
+ "json",
255
+ ]);
256
+ expect(exitCode, `--type ${type}`).toBe(1);
257
+ const result = extractJson(stdout) as FailureOutput;
258
+ expect(result.Result).toBe("Failure");
259
+ expect(result.Message).toBe(message);
260
+ }
261
+ },
262
+ EXEC_TIMEOUT,
263
+ );
264
+
265
+ // ── S8 ──────────────────────────────────────────────────────────────────
266
+ it(
267
+ "S8: revoke requires confirmation and a reason",
268
+ () => {
269
+ // --reason without --yes → confirmation required
270
+ const withoutYes = localCliRawWithArgs(tempDir, [
271
+ "df",
272
+ "directory",
273
+ "revoke-roles",
274
+ PRINCIPAL_ID,
275
+ "--reason",
276
+ "x",
277
+ "--output",
278
+ "json",
279
+ ]);
280
+ expect(withoutYes.exitCode).toBe(1);
281
+ const withoutYesResult = extractJson(
282
+ withoutYes.stdout,
283
+ ) as FailureOutput;
284
+ expect(withoutYesResult.Result).toBe("Failure");
285
+ expect(withoutYesResult.Message).toContain("Confirmation required");
286
+
287
+ // --yes without --reason → reason required
288
+ const withoutReason = localCliRawWithArgs(tempDir, [
289
+ "df",
290
+ "directory",
291
+ "revoke-roles",
292
+ PRINCIPAL_ID,
293
+ "--yes",
294
+ "--output",
295
+ "json",
296
+ ]);
297
+ expect(withoutReason.exitCode).toBe(1);
298
+ const withoutReasonResult = extractJson(
299
+ withoutReason.stdout,
300
+ ) as FailureOutput;
301
+ expect(withoutReasonResult.Result).toBe("Failure");
302
+ expect(withoutReasonResult.Message).toBe(
303
+ "Reason required for destructive operation",
304
+ );
305
+ },
306
+ EXEC_TIMEOUT,
307
+ );
308
+
309
+ // ── S9 ──────────────────────────────────────────────────────────────────
310
+ it.skip("S9: revoke all direct roles from a principal", () => {});
311
+
312
+ // ── S10 ─────────────────────────────────────────────────────────────────
313
+ it(
314
+ "S10: roles and directory groups are hidden from help",
315
+ () => {
316
+ const help = localCli(tempDir, "df --help");
317
+ // Command rows render as "│ <name> <description>…" in the boxed
318
+ // help, so anchoring on "│ <name>" matches only the command column
319
+ // (continuation lines have extra spaces after the border).
320
+ // Documented command groups are listed…
321
+ expect(help).toMatch(/^│ entities\s/m);
322
+ expect(help).toMatch(/^│ records\s/m);
323
+ expect(help).toMatch(/^│ files\s/m);
324
+ expect(help).toMatch(/^│ choice-sets\s/m);
325
+ // …the internal manage-access groups are not.
326
+ expect(help).not.toMatch(/^│ roles\s/m);
327
+ expect(help).not.toMatch(/^│ directory\s/m);
328
+
329
+ // Hidden groups still execute when invoked directly: an invalid
330
+ // flag reaches the command's own validation, proving the command
331
+ // is registered and runs (full happy paths are S1/S3).
332
+ const { stdout, exitCode } = localCliRawWithArgs(tempDir, [
333
+ "df",
334
+ "directory",
335
+ "list",
336
+ "--limit",
337
+ "0",
338
+ "--output",
339
+ "json",
340
+ ]);
341
+ expect(exitCode).toBe(1);
342
+ const result = extractJson(stdout) as FailureOutput;
343
+ expect(result.Message).toBe("Invalid --limit value '0'");
344
+ },
345
+ EXEC_TIMEOUT,
346
+ );
347
+ });