@clef-sh/ui 0.1.13-beta.88

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.
Files changed (70) hide show
  1. package/README.md +38 -0
  2. package/dist/client/assets/index-CVpAmirt.js +26 -0
  3. package/dist/client/favicon-96x96.png +0 -0
  4. package/dist/client/favicon.ico +0 -0
  5. package/dist/client/favicon.svg +16 -0
  6. package/dist/client/index.html +50 -0
  7. package/dist/client-lib/api.d.ts +3 -0
  8. package/dist/client-lib/api.d.ts.map +1 -0
  9. package/dist/client-lib/components/Button.d.ts +10 -0
  10. package/dist/client-lib/components/Button.d.ts.map +1 -0
  11. package/dist/client-lib/components/CopyButton.d.ts +6 -0
  12. package/dist/client-lib/components/CopyButton.d.ts.map +1 -0
  13. package/dist/client-lib/components/EnvBadge.d.ts +7 -0
  14. package/dist/client-lib/components/EnvBadge.d.ts.map +1 -0
  15. package/dist/client-lib/components/MatrixGrid.d.ts +13 -0
  16. package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -0
  17. package/dist/client-lib/components/Sidebar.d.ts +16 -0
  18. package/dist/client-lib/components/Sidebar.d.ts.map +1 -0
  19. package/dist/client-lib/components/StatusDot.d.ts +6 -0
  20. package/dist/client-lib/components/StatusDot.d.ts.map +1 -0
  21. package/dist/client-lib/components/TopBar.d.ts +9 -0
  22. package/dist/client-lib/components/TopBar.d.ts.map +1 -0
  23. package/dist/client-lib/index.d.ts +12 -0
  24. package/dist/client-lib/index.d.ts.map +1 -0
  25. package/dist/client-lib/theme.d.ts +42 -0
  26. package/dist/client-lib/theme.d.ts.map +1 -0
  27. package/dist/server/api.d.ts +11 -0
  28. package/dist/server/api.d.ts.map +1 -0
  29. package/dist/server/api.js +1020 -0
  30. package/dist/server/api.js.map +1 -0
  31. package/dist/server/index.d.ts +12 -0
  32. package/dist/server/index.d.ts.map +1 -0
  33. package/dist/server/index.js +231 -0
  34. package/dist/server/index.js.map +1 -0
  35. package/package.json +74 -0
  36. package/src/client/App.tsx +205 -0
  37. package/src/client/api.test.tsx +94 -0
  38. package/src/client/api.ts +30 -0
  39. package/src/client/components/Button.tsx +52 -0
  40. package/src/client/components/CopyButton.test.tsx +43 -0
  41. package/src/client/components/CopyButton.tsx +36 -0
  42. package/src/client/components/EnvBadge.tsx +32 -0
  43. package/src/client/components/MatrixGrid.tsx +265 -0
  44. package/src/client/components/Sidebar.tsx +337 -0
  45. package/src/client/components/StatusDot.tsx +30 -0
  46. package/src/client/components/TopBar.tsx +50 -0
  47. package/src/client/index.html +50 -0
  48. package/src/client/index.ts +18 -0
  49. package/src/client/main.tsx +15 -0
  50. package/src/client/public/favicon-96x96.png +0 -0
  51. package/src/client/public/favicon.ico +0 -0
  52. package/src/client/public/favicon.svg +16 -0
  53. package/src/client/screens/BackendScreen.test.tsx +611 -0
  54. package/src/client/screens/BackendScreen.tsx +836 -0
  55. package/src/client/screens/DiffView.test.tsx +130 -0
  56. package/src/client/screens/DiffView.tsx +547 -0
  57. package/src/client/screens/GitLogView.test.tsx +113 -0
  58. package/src/client/screens/GitLogView.tsx +192 -0
  59. package/src/client/screens/ImportScreen.tsx +710 -0
  60. package/src/client/screens/LintView.test.tsx +143 -0
  61. package/src/client/screens/LintView.tsx +589 -0
  62. package/src/client/screens/MatrixView.test.tsx +138 -0
  63. package/src/client/screens/MatrixView.tsx +143 -0
  64. package/src/client/screens/NamespaceEditor.test.tsx +694 -0
  65. package/src/client/screens/NamespaceEditor.tsx +1122 -0
  66. package/src/client/screens/RecipientsScreen.tsx +696 -0
  67. package/src/client/screens/ScanScreen.test.tsx +323 -0
  68. package/src/client/screens/ScanScreen.tsx +523 -0
  69. package/src/client/screens/ServiceIdentitiesScreen.tsx +1398 -0
  70. package/src/client/theme.ts +48 -0
@@ -0,0 +1,611 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
3
+ import "@testing-library/jest-dom";
4
+ import { BackendScreen } from "./BackendScreen";
5
+ import type { ClefManifest } from "@clef-sh/core";
6
+
7
+ jest.mock("../api", () => ({
8
+ apiFetch: jest.fn(),
9
+ }));
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const { apiFetch } = require("../api") as { apiFetch: jest.Mock };
13
+
14
+ const manifest: ClefManifest = {
15
+ version: 1,
16
+ environments: [
17
+ { name: "staging", description: "Staging" },
18
+ { name: "production", description: "Production", protected: true },
19
+ ],
20
+ namespaces: [{ name: "database", description: "DB" }],
21
+ sops: { default_backend: "age" },
22
+ file_pattern: "{namespace}/{environment}.enc.yaml",
23
+ };
24
+
25
+ const backendConfigResponse = {
26
+ global: { default_backend: "age" },
27
+ environments: [
28
+ { name: "staging", protected: false, effective: { backend: "age" }, hasOverride: false },
29
+ { name: "production", protected: true, effective: { backend: "age" }, hasOverride: false },
30
+ ],
31
+ };
32
+
33
+ function mockConfigEndpoint() {
34
+ apiFetch.mockImplementation((url: string) => {
35
+ if (url === "/api/backend-config") {
36
+ return Promise.resolve({
37
+ ok: true,
38
+ json: () => Promise.resolve(backendConfigResponse),
39
+ });
40
+ }
41
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
42
+ });
43
+ }
44
+
45
+ const setView = jest.fn();
46
+ const reloadManifest = jest.fn();
47
+
48
+ beforeEach(() => {
49
+ jest.clearAllMocks();
50
+ });
51
+
52
+ describe("BackendScreen — step 1", () => {
53
+ it("renders current backend configuration on mount", async () => {
54
+ mockConfigEndpoint();
55
+ await act(async () => {
56
+ render(
57
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
58
+ );
59
+ });
60
+
61
+ expect(await screen.findByText(/Default backend/)).toBeTruthy();
62
+ expect(screen.getAllByText("age").length).toBeGreaterThan(0);
63
+ });
64
+
65
+ it("shows all 5 backend radio buttons", async () => {
66
+ mockConfigEndpoint();
67
+ await act(async () => {
68
+ render(
69
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
70
+ );
71
+ });
72
+
73
+ expect(screen.getByTestId("backend-radio-age")).toBeTruthy();
74
+ expect(screen.getByTestId("backend-radio-awskms")).toBeTruthy();
75
+ expect(screen.getByTestId("backend-radio-gcpkms")).toBeTruthy();
76
+ expect(screen.getByTestId("backend-radio-azurekv")).toBeTruthy();
77
+ expect(screen.getByTestId("backend-radio-pgp")).toBeTruthy();
78
+ });
79
+
80
+ it("shows key input when non-age backend is selected", async () => {
81
+ mockConfigEndpoint();
82
+ await act(async () => {
83
+ render(
84
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
85
+ );
86
+ });
87
+
88
+ // Initially age is selected — no key input
89
+ expect(screen.queryByTestId("backend-key-input")).toBeNull();
90
+
91
+ // Select AWS KMS
92
+ fireEvent.click(screen.getByTestId("backend-radio-awskms"));
93
+ expect(screen.getByTestId("backend-key-input")).toBeTruthy();
94
+ });
95
+
96
+ it("hides key input when age is re-selected", async () => {
97
+ mockConfigEndpoint();
98
+ await act(async () => {
99
+ render(
100
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
101
+ );
102
+ });
103
+
104
+ fireEvent.click(screen.getByTestId("backend-radio-awskms"));
105
+ expect(screen.getByTestId("backend-key-input")).toBeTruthy();
106
+
107
+ fireEvent.click(screen.getByTestId("backend-radio-age"));
108
+ expect(screen.queryByTestId("backend-key-input")).toBeNull();
109
+ });
110
+ });
111
+
112
+ describe("BackendScreen — preview and apply", () => {
113
+ it("calls preview endpoint on Preview button click", async () => {
114
+ apiFetch.mockImplementation((url: string) => {
115
+ if (url === "/api/backend-config") {
116
+ return Promise.resolve({
117
+ ok: true,
118
+ json: () => Promise.resolve(backendConfigResponse),
119
+ });
120
+ }
121
+ if (url === "/api/migrate-backend/preview") {
122
+ return Promise.resolve({
123
+ ok: true,
124
+ json: () =>
125
+ Promise.resolve({
126
+ success: true,
127
+ result: {
128
+ migratedFiles: [],
129
+ skippedFiles: [],
130
+ rolledBack: false,
131
+ verifiedFiles: [],
132
+ warnings: ["Would update global default_backend \u2192 awskms"],
133
+ },
134
+ events: [
135
+ { type: "info", message: "Would migrate database/staging to awskms" },
136
+ { type: "info", message: "Would migrate database/production to awskms" },
137
+ ],
138
+ }),
139
+ });
140
+ }
141
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
142
+ });
143
+
144
+ await act(async () => {
145
+ render(
146
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
147
+ );
148
+ });
149
+
150
+ // Select AWS KMS and fill key
151
+ fireEvent.click(screen.getByTestId("backend-radio-awskms"));
152
+ fireEvent.change(screen.getByTestId("backend-key-input"), {
153
+ target: { value: "arn:aws:kms:us-east-1:123:key/abc" },
154
+ });
155
+
156
+ // Click Preview
157
+ await act(async () => {
158
+ fireEvent.click(screen.getByRole("button", { name: "Preview" }));
159
+ });
160
+
161
+ // Should show preview step
162
+ await waitFor(() => {
163
+ expect(screen.getByText(/Files to migrate/)).toBeTruthy();
164
+ });
165
+ });
166
+
167
+ it("shows protected env warning on 409", async () => {
168
+ apiFetch.mockImplementation((url: string) => {
169
+ if (url === "/api/backend-config") {
170
+ return Promise.resolve({
171
+ ok: true,
172
+ json: () => Promise.resolve(backendConfigResponse),
173
+ });
174
+ }
175
+ if (url === "/api/migrate-backend/preview") {
176
+ return Promise.resolve({
177
+ ok: false,
178
+ status: 409,
179
+ json: () =>
180
+ Promise.resolve({
181
+ error: "Protected environment requires confirmation",
182
+ code: "PROTECTED_ENV",
183
+ protected: true,
184
+ }),
185
+ });
186
+ }
187
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
188
+ });
189
+
190
+ await act(async () => {
191
+ render(
192
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
193
+ );
194
+ });
195
+
196
+ // Click Preview (age is default, no key needed)
197
+ await act(async () => {
198
+ fireEvent.click(screen.getByRole("button", { name: "Preview" }));
199
+ });
200
+
201
+ // Should show protected env confirmation
202
+ await waitFor(() => {
203
+ expect(screen.getByTestId("protected-confirm")).toBeTruthy();
204
+ });
205
+ });
206
+
207
+ it("shows success result after apply", async () => {
208
+ let previewCalled = false;
209
+ apiFetch.mockImplementation((url: string) => {
210
+ if (url === "/api/backend-config") {
211
+ return Promise.resolve({
212
+ ok: true,
213
+ json: () => Promise.resolve(backendConfigResponse),
214
+ });
215
+ }
216
+ if (url === "/api/migrate-backend/preview") {
217
+ previewCalled = true;
218
+ return Promise.resolve({
219
+ ok: true,
220
+ json: () =>
221
+ Promise.resolve({
222
+ success: true,
223
+ result: {
224
+ migratedFiles: [],
225
+ skippedFiles: [],
226
+ rolledBack: false,
227
+ verifiedFiles: [],
228
+ warnings: [],
229
+ },
230
+ events: [{ type: "info", message: "Would migrate database/staging to awskms" }],
231
+ }),
232
+ });
233
+ }
234
+ if (url === "/api/migrate-backend/apply") {
235
+ return Promise.resolve({
236
+ ok: true,
237
+ json: () =>
238
+ Promise.resolve({
239
+ success: true,
240
+ result: {
241
+ migratedFiles: ["/repo/database/staging.enc.yaml"],
242
+ skippedFiles: [],
243
+ rolledBack: false,
244
+ verifiedFiles: ["/repo/database/staging.enc.yaml"],
245
+ warnings: [],
246
+ },
247
+ events: [],
248
+ }),
249
+ });
250
+ }
251
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
252
+ });
253
+
254
+ await act(async () => {
255
+ render(
256
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
257
+ );
258
+ });
259
+
260
+ // Select AWS KMS
261
+ fireEvent.click(screen.getByTestId("backend-radio-awskms"));
262
+ fireEvent.change(screen.getByTestId("backend-key-input"), {
263
+ target: { value: "arn:..." },
264
+ });
265
+
266
+ // Preview
267
+ await act(async () => {
268
+ fireEvent.click(screen.getByRole("button", { name: "Preview" }));
269
+ });
270
+ await waitFor(() => expect(previewCalled).toBe(true));
271
+
272
+ // Apply
273
+ await act(async () => {
274
+ fireEvent.click(screen.getByTestId("apply-button"));
275
+ });
276
+
277
+ // Should show success
278
+ await waitFor(() => {
279
+ expect(screen.getByText("Migration complete")).toBeTruthy();
280
+ });
281
+ expect(screen.getByText(/1 migrated/)).toBeTruthy();
282
+ expect(reloadManifest).toHaveBeenCalled();
283
+ });
284
+
285
+ it("shows rollback state on failure", async () => {
286
+ let previewCalled = false;
287
+ apiFetch.mockImplementation((url: string) => {
288
+ if (url === "/api/backend-config") {
289
+ return Promise.resolve({
290
+ ok: true,
291
+ json: () => Promise.resolve(backendConfigResponse),
292
+ });
293
+ }
294
+ if (url === "/api/migrate-backend/preview") {
295
+ previewCalled = true;
296
+ return Promise.resolve({
297
+ ok: true,
298
+ json: () =>
299
+ Promise.resolve({
300
+ success: true,
301
+ result: {
302
+ migratedFiles: [],
303
+ skippedFiles: [],
304
+ rolledBack: false,
305
+ verifiedFiles: [],
306
+ warnings: [],
307
+ },
308
+ events: [{ type: "info", message: "Would migrate database/staging to awskms" }],
309
+ }),
310
+ });
311
+ }
312
+ if (url === "/api/migrate-backend/apply") {
313
+ return Promise.resolve({
314
+ ok: true,
315
+ json: () =>
316
+ Promise.resolve({
317
+ success: false,
318
+ result: {
319
+ migratedFiles: [],
320
+ skippedFiles: [],
321
+ rolledBack: true,
322
+ error: "KMS access denied",
323
+ verifiedFiles: [],
324
+ warnings: ["All changes have been rolled back."],
325
+ },
326
+ events: [],
327
+ }),
328
+ });
329
+ }
330
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
331
+ });
332
+
333
+ await act(async () => {
334
+ render(
335
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
336
+ );
337
+ });
338
+
339
+ fireEvent.click(screen.getByTestId("backend-radio-awskms"));
340
+ fireEvent.change(screen.getByTestId("backend-key-input"), {
341
+ target: { value: "arn:..." },
342
+ });
343
+
344
+ await act(async () => {
345
+ fireEvent.click(screen.getByRole("button", { name: "Preview" }));
346
+ });
347
+ await waitFor(() => expect(previewCalled).toBe(true));
348
+
349
+ await act(async () => {
350
+ fireEvent.click(screen.getByTestId("apply-button"));
351
+ });
352
+
353
+ await waitFor(() => {
354
+ expect(screen.getByText("Migration failed")).toBeTruthy();
355
+ });
356
+ expect(screen.getByText(/KMS access denied/)).toBeTruthy();
357
+ expect(screen.getAllByText(/rolled back/).length).toBeGreaterThan(0);
358
+ });
359
+ });
360
+
361
+ describe("BackendScreen — negative cases", () => {
362
+ it("Preview button is disabled when non-age backend has no key", async () => {
363
+ mockConfigEndpoint();
364
+ await act(async () => {
365
+ render(
366
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
367
+ );
368
+ });
369
+
370
+ fireEvent.click(screen.getByTestId("backend-radio-awskms"));
371
+ // Key input is empty — Preview should be disabled
372
+ const button = screen.getByRole("button", { name: "Preview" });
373
+ expect(button).toBeDisabled();
374
+ });
375
+
376
+ it("Preview button is enabled when age is selected (no key needed)", async () => {
377
+ mockConfigEndpoint();
378
+ await act(async () => {
379
+ render(
380
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
381
+ );
382
+ });
383
+
384
+ // age is default — Preview should be enabled
385
+ const button = screen.getByRole("button", { name: "Preview" });
386
+ expect(button).not.toBeDisabled();
387
+ });
388
+
389
+ it("displays error when preview returns 500", async () => {
390
+ apiFetch.mockImplementation((url: string) => {
391
+ if (url === "/api/backend-config") {
392
+ return Promise.resolve({
393
+ ok: true,
394
+ json: () => Promise.resolve(backendConfigResponse),
395
+ });
396
+ }
397
+ if (url === "/api/migrate-backend/preview") {
398
+ return Promise.resolve({
399
+ ok: false,
400
+ status: 500,
401
+ json: () =>
402
+ Promise.resolve({
403
+ error: "Unexpected sops failure",
404
+ code: "MIGRATION_ERROR",
405
+ }),
406
+ });
407
+ }
408
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
409
+ });
410
+
411
+ await act(async () => {
412
+ render(
413
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
414
+ );
415
+ });
416
+
417
+ await act(async () => {
418
+ fireEvent.click(screen.getByRole("button", { name: "Preview" }));
419
+ });
420
+
421
+ await waitFor(() => {
422
+ expect(screen.getByText(/Unexpected sops failure/)).toBeTruthy();
423
+ });
424
+ // Should remain on step 1
425
+ expect(screen.getByTestId("backend-radio-age")).toBeTruthy();
426
+ });
427
+
428
+ it("displays error when network request fails", async () => {
429
+ apiFetch.mockImplementation((url: string) => {
430
+ if (url === "/api/backend-config") {
431
+ return Promise.resolve({
432
+ ok: true,
433
+ json: () => Promise.resolve(backendConfigResponse),
434
+ });
435
+ }
436
+ if (url === "/api/migrate-backend/preview") {
437
+ return Promise.reject(new Error("Network error"));
438
+ }
439
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
440
+ });
441
+
442
+ await act(async () => {
443
+ render(
444
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
445
+ );
446
+ });
447
+
448
+ await act(async () => {
449
+ fireEvent.click(screen.getByRole("button", { name: "Preview" }));
450
+ });
451
+
452
+ await waitFor(() => {
453
+ expect(screen.getByText(/Network error/)).toBeTruthy();
454
+ });
455
+ });
456
+
457
+ it("View in Matrix button navigates away after success", async () => {
458
+ let previewCalled = false;
459
+ apiFetch.mockImplementation((url: string) => {
460
+ if (url === "/api/backend-config") {
461
+ return Promise.resolve({
462
+ ok: true,
463
+ json: () => Promise.resolve(backendConfigResponse),
464
+ });
465
+ }
466
+ if (url === "/api/migrate-backend/preview") {
467
+ previewCalled = true;
468
+ return Promise.resolve({
469
+ ok: true,
470
+ json: () =>
471
+ Promise.resolve({
472
+ success: true,
473
+ result: {
474
+ migratedFiles: [],
475
+ skippedFiles: [],
476
+ rolledBack: false,
477
+ verifiedFiles: [],
478
+ warnings: [],
479
+ },
480
+ events: [{ type: "info", message: "Would migrate database/staging to awskms" }],
481
+ }),
482
+ });
483
+ }
484
+ if (url === "/api/migrate-backend/apply") {
485
+ return Promise.resolve({
486
+ ok: true,
487
+ json: () =>
488
+ Promise.resolve({
489
+ success: true,
490
+ result: {
491
+ migratedFiles: ["/repo/database/staging.enc.yaml"],
492
+ skippedFiles: [],
493
+ rolledBack: false,
494
+ verifiedFiles: ["/repo/database/staging.enc.yaml"],
495
+ warnings: [],
496
+ },
497
+ events: [],
498
+ }),
499
+ });
500
+ }
501
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
502
+ });
503
+
504
+ await act(async () => {
505
+ render(
506
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
507
+ );
508
+ });
509
+
510
+ fireEvent.click(screen.getByTestId("backend-radio-awskms"));
511
+ fireEvent.change(screen.getByTestId("backend-key-input"), {
512
+ target: { value: "arn:..." },
513
+ });
514
+
515
+ await act(async () => {
516
+ fireEvent.click(screen.getByRole("button", { name: "Preview" }));
517
+ });
518
+ await waitFor(() => expect(previewCalled).toBe(true));
519
+
520
+ await act(async () => {
521
+ fireEvent.click(screen.getByTestId("apply-button"));
522
+ });
523
+
524
+ await waitFor(() => {
525
+ expect(screen.getByText("Migration complete")).toBeTruthy();
526
+ });
527
+
528
+ fireEvent.click(screen.getByRole("button", { name: "View in Matrix" }));
529
+ expect(setView).toHaveBeenCalledWith("matrix");
530
+ });
531
+
532
+ it("Migrate again resets to step 1", async () => {
533
+ let previewCalled = false;
534
+ apiFetch.mockImplementation((url: string) => {
535
+ if (url === "/api/backend-config") {
536
+ return Promise.resolve({
537
+ ok: true,
538
+ json: () => Promise.resolve(backendConfigResponse),
539
+ });
540
+ }
541
+ if (url === "/api/migrate-backend/preview") {
542
+ previewCalled = true;
543
+ return Promise.resolve({
544
+ ok: true,
545
+ json: () =>
546
+ Promise.resolve({
547
+ success: true,
548
+ result: {
549
+ migratedFiles: [],
550
+ skippedFiles: [],
551
+ rolledBack: false,
552
+ verifiedFiles: [],
553
+ warnings: [],
554
+ },
555
+ events: [{ type: "info", message: "Would migrate database/staging to awskms" }],
556
+ }),
557
+ });
558
+ }
559
+ if (url === "/api/migrate-backend/apply") {
560
+ return Promise.resolve({
561
+ ok: true,
562
+ json: () =>
563
+ Promise.resolve({
564
+ success: true,
565
+ result: {
566
+ migratedFiles: ["/repo/database/staging.enc.yaml"],
567
+ skippedFiles: [],
568
+ rolledBack: false,
569
+ verifiedFiles: ["/repo/database/staging.enc.yaml"],
570
+ warnings: [],
571
+ },
572
+ events: [],
573
+ }),
574
+ });
575
+ }
576
+ return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
577
+ });
578
+
579
+ await act(async () => {
580
+ render(
581
+ <BackendScreen manifest={manifest} setView={setView} reloadManifest={reloadManifest} />,
582
+ );
583
+ });
584
+
585
+ fireEvent.click(screen.getByTestId("backend-radio-awskms"));
586
+ fireEvent.change(screen.getByTestId("backend-key-input"), {
587
+ target: { value: "arn:..." },
588
+ });
589
+
590
+ await act(async () => {
591
+ fireEvent.click(screen.getByRole("button", { name: "Preview" }));
592
+ });
593
+ await waitFor(() => expect(previewCalled).toBe(true));
594
+
595
+ await act(async () => {
596
+ fireEvent.click(screen.getByTestId("apply-button"));
597
+ });
598
+
599
+ await waitFor(() => {
600
+ expect(screen.getByText("Migration complete")).toBeTruthy();
601
+ });
602
+
603
+ await act(async () => {
604
+ fireEvent.click(screen.getByRole("button", { name: "Migrate again" }));
605
+ });
606
+
607
+ // Should be back on step 1 with config visible
608
+ expect(screen.getByTestId("backend-radio-age")).toBeTruthy();
609
+ expect(screen.getByText(/Current Configuration/)).toBeTruthy();
610
+ });
611
+ });