@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.
- package/README.md +38 -0
- package/dist/client/assets/index-CVpAmirt.js +26 -0
- package/dist/client/favicon-96x96.png +0 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.svg +16 -0
- package/dist/client/index.html +50 -0
- package/dist/client-lib/api.d.ts +3 -0
- package/dist/client-lib/api.d.ts.map +1 -0
- package/dist/client-lib/components/Button.d.ts +10 -0
- package/dist/client-lib/components/Button.d.ts.map +1 -0
- package/dist/client-lib/components/CopyButton.d.ts +6 -0
- package/dist/client-lib/components/CopyButton.d.ts.map +1 -0
- package/dist/client-lib/components/EnvBadge.d.ts +7 -0
- package/dist/client-lib/components/EnvBadge.d.ts.map +1 -0
- package/dist/client-lib/components/MatrixGrid.d.ts +13 -0
- package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -0
- package/dist/client-lib/components/Sidebar.d.ts +16 -0
- package/dist/client-lib/components/Sidebar.d.ts.map +1 -0
- package/dist/client-lib/components/StatusDot.d.ts +6 -0
- package/dist/client-lib/components/StatusDot.d.ts.map +1 -0
- package/dist/client-lib/components/TopBar.d.ts +9 -0
- package/dist/client-lib/components/TopBar.d.ts.map +1 -0
- package/dist/client-lib/index.d.ts +12 -0
- package/dist/client-lib/index.d.ts.map +1 -0
- package/dist/client-lib/theme.d.ts +42 -0
- package/dist/client-lib/theme.d.ts.map +1 -0
- package/dist/server/api.d.ts +11 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +1020 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +231 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +74 -0
- package/src/client/App.tsx +205 -0
- package/src/client/api.test.tsx +94 -0
- package/src/client/api.ts +30 -0
- package/src/client/components/Button.tsx +52 -0
- package/src/client/components/CopyButton.test.tsx +43 -0
- package/src/client/components/CopyButton.tsx +36 -0
- package/src/client/components/EnvBadge.tsx +32 -0
- package/src/client/components/MatrixGrid.tsx +265 -0
- package/src/client/components/Sidebar.tsx +337 -0
- package/src/client/components/StatusDot.tsx +30 -0
- package/src/client/components/TopBar.tsx +50 -0
- package/src/client/index.html +50 -0
- package/src/client/index.ts +18 -0
- package/src/client/main.tsx +15 -0
- package/src/client/public/favicon-96x96.png +0 -0
- package/src/client/public/favicon.ico +0 -0
- package/src/client/public/favicon.svg +16 -0
- package/src/client/screens/BackendScreen.test.tsx +611 -0
- package/src/client/screens/BackendScreen.tsx +836 -0
- package/src/client/screens/DiffView.test.tsx +130 -0
- package/src/client/screens/DiffView.tsx +547 -0
- package/src/client/screens/GitLogView.test.tsx +113 -0
- package/src/client/screens/GitLogView.tsx +192 -0
- package/src/client/screens/ImportScreen.tsx +710 -0
- package/src/client/screens/LintView.test.tsx +143 -0
- package/src/client/screens/LintView.tsx +589 -0
- package/src/client/screens/MatrixView.test.tsx +138 -0
- package/src/client/screens/MatrixView.tsx +143 -0
- package/src/client/screens/NamespaceEditor.test.tsx +694 -0
- package/src/client/screens/NamespaceEditor.tsx +1122 -0
- package/src/client/screens/RecipientsScreen.tsx +696 -0
- package/src/client/screens/ScanScreen.test.tsx +323 -0
- package/src/client/screens/ScanScreen.tsx +523 -0
- package/src/client/screens/ServiceIdentitiesScreen.tsx +1398 -0
- package/src/client/theme.ts +48 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent, act } from "@testing-library/react";
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
import { NamespaceEditor } from "./NamespaceEditor";
|
|
5
|
+
import type { ClefManifest } from "@clef-sh/core";
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
declare let global: any;
|
|
9
|
+
|
|
10
|
+
const manifest: ClefManifest = {
|
|
11
|
+
version: 1,
|
|
12
|
+
environments: [
|
|
13
|
+
{ name: "dev", description: "Dev" },
|
|
14
|
+
{ name: "production", description: "Prod", protected: true },
|
|
15
|
+
],
|
|
16
|
+
namespaces: [{ name: "database", description: "DB" }],
|
|
17
|
+
sops: { default_backend: "age" },
|
|
18
|
+
file_pattern: "{namespace}/{environment}.enc.yaml",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mockDecrypted = {
|
|
22
|
+
values: { DB_HOST: "localhost", DB_PORT: "5432" },
|
|
23
|
+
metadata: {
|
|
24
|
+
backend: "age",
|
|
25
|
+
recipients: ["age1abc", "age1def"],
|
|
26
|
+
lastModified: "2024-01-15",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
jest.restoreAllMocks();
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
delete (global as any).fetch;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("NamespaceEditor", () => {
|
|
38
|
+
it("renders with fetched data", async () => {
|
|
39
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
40
|
+
ok: true,
|
|
41
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
42
|
+
} as Response);
|
|
43
|
+
|
|
44
|
+
await act(async () => {
|
|
45
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(screen.getByText("DB_HOST")).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByText("DB_PORT")).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("shows production warning on production tab", async () => {
|
|
53
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
54
|
+
ok: true,
|
|
55
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
56
|
+
} as Response);
|
|
57
|
+
|
|
58
|
+
await act(async () => {
|
|
59
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Click production tab
|
|
63
|
+
const tabs = screen.getAllByRole("tab");
|
|
64
|
+
const prodTab = tabs.find((t) => t.textContent?.includes("production"));
|
|
65
|
+
|
|
66
|
+
await act(async () => {
|
|
67
|
+
if (prodTab) fireEvent.click(prodTab);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(screen.getByTestId("production-warning")).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("reveals value when eye icon is clicked", async () => {
|
|
74
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
75
|
+
ok: true,
|
|
76
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
77
|
+
} as Response);
|
|
78
|
+
|
|
79
|
+
await act(async () => {
|
|
80
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await act(async () => {
|
|
84
|
+
fireEvent.click(screen.getByTestId("eye-DB_HOST"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(screen.getByTestId("value-input-DB_HOST")).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("shows error state when API fails", async () => {
|
|
91
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
92
|
+
ok: false,
|
|
93
|
+
json: () => Promise.resolve({ error: "Decrypt failed" }),
|
|
94
|
+
} as Response);
|
|
95
|
+
|
|
96
|
+
await act(async () => {
|
|
97
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText("Decrypt failed")).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("shows mode toggle when adding a key", async () => {
|
|
104
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
107
|
+
} as Response);
|
|
108
|
+
|
|
109
|
+
await act(async () => {
|
|
110
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await act(async () => {
|
|
114
|
+
fireEvent.click(screen.getByTestId("add-key-btn"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(screen.getByTestId("mode-set-value")).toBeInTheDocument();
|
|
118
|
+
expect(screen.getByTestId("mode-random")).toBeInTheDocument();
|
|
119
|
+
expect(screen.getByTestId("new-value-input")).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("hides value input in random mode and shows Generate button", async () => {
|
|
123
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
124
|
+
ok: true,
|
|
125
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
126
|
+
} as Response);
|
|
127
|
+
|
|
128
|
+
await act(async () => {
|
|
129
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await act(async () => {
|
|
133
|
+
fireEvent.click(screen.getByTestId("add-key-btn"));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await act(async () => {
|
|
137
|
+
fireEvent.click(screen.getByTestId("mode-random"));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(screen.queryByTestId("new-value-input")).not.toBeInTheDocument();
|
|
141
|
+
expect(screen.getByTestId("add-key-submit")).toHaveTextContent("Generate random value");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("sends random: true when adding in random mode", async () => {
|
|
145
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
146
|
+
ok: true,
|
|
147
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
148
|
+
} as Response);
|
|
149
|
+
global.fetch = fetchMock;
|
|
150
|
+
|
|
151
|
+
await act(async () => {
|
|
152
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await act(async () => {
|
|
156
|
+
fireEvent.click(screen.getByTestId("add-key-btn"));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await act(async () => {
|
|
160
|
+
fireEvent.click(screen.getByTestId("mode-random"));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await act(async () => {
|
|
164
|
+
fireEvent.change(screen.getByTestId("new-key-input"), {
|
|
165
|
+
target: { value: "NEW_SECRET" },
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await act(async () => {
|
|
170
|
+
fireEvent.click(screen.getByTestId("add-key-submit"));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const putCall = fetchMock.mock.calls.find(
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
175
|
+
(c: any[]) =>
|
|
176
|
+
typeof c[0] === "string" && c[0].includes("/NEW_SECRET") && c[1]?.method === "PUT",
|
|
177
|
+
);
|
|
178
|
+
expect(putCall).toBeDefined();
|
|
179
|
+
const body = JSON.parse(putCall![1].body);
|
|
180
|
+
expect(body.random).toBe(true);
|
|
181
|
+
expect(body.value).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("shows overflow menu with Reset to random option", async () => {
|
|
185
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
186
|
+
ok: true,
|
|
187
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
188
|
+
} as Response);
|
|
189
|
+
|
|
190
|
+
await act(async () => {
|
|
191
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await act(async () => {
|
|
195
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(screen.getByTestId("overflow-menu-DB_HOST")).toBeInTheDocument();
|
|
199
|
+
expect(screen.getByTestId("reset-random-DB_HOST")).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("shows confirmation dialog before resetting to random", async () => {
|
|
203
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
204
|
+
ok: true,
|
|
205
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
206
|
+
} as Response);
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Open overflow menu
|
|
213
|
+
await act(async () => {
|
|
214
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Click reset to random
|
|
218
|
+
await act(async () => {
|
|
219
|
+
fireEvent.click(screen.getByTestId("reset-random-DB_HOST"));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(screen.getByTestId("confirm-reset-dialog")).toBeInTheDocument();
|
|
223
|
+
expect(screen.getByText(/current value will be overwritten/)).toBeInTheDocument();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("cancels reset when Cancel is clicked in confirmation", async () => {
|
|
227
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
228
|
+
ok: true,
|
|
229
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
230
|
+
} as Response);
|
|
231
|
+
|
|
232
|
+
await act(async () => {
|
|
233
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await act(async () => {
|
|
237
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await act(async () => {
|
|
241
|
+
fireEvent.click(screen.getByTestId("reset-random-DB_HOST"));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// The confirmation dialog is visible
|
|
245
|
+
expect(screen.getByTestId("confirm-reset-dialog")).toBeInTheDocument();
|
|
246
|
+
|
|
247
|
+
// Click Cancel in the confirmation dialog
|
|
248
|
+
await act(async () => {
|
|
249
|
+
fireEvent.click(screen.getByTestId("confirm-reset-no"));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(screen.queryByTestId("confirm-reset-dialog")).not.toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("sends random: true when confirming reset to random", async () => {
|
|
256
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
257
|
+
ok: true,
|
|
258
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
259
|
+
} as Response);
|
|
260
|
+
global.fetch = fetchMock;
|
|
261
|
+
|
|
262
|
+
await act(async () => {
|
|
263
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await act(async () => {
|
|
267
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await act(async () => {
|
|
271
|
+
fireEvent.click(screen.getByTestId("reset-random-DB_HOST"));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Click "Reset to random" button in the confirmation dialog
|
|
275
|
+
await act(async () => {
|
|
276
|
+
fireEvent.click(screen.getByTestId("confirm-reset-yes"));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const putCall = fetchMock.mock.calls.find(
|
|
280
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
281
|
+
(c: any[]) => typeof c[0] === "string" && c[0].includes("/DB_HOST") && c[1]?.method === "PUT",
|
|
282
|
+
);
|
|
283
|
+
expect(putCall).toBeDefined();
|
|
284
|
+
const body = JSON.parse(putCall![1].body);
|
|
285
|
+
expect(body.random).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("marks row dirty when value is edited", async () => {
|
|
289
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
290
|
+
ok: true,
|
|
291
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
292
|
+
} as Response);
|
|
293
|
+
|
|
294
|
+
await act(async () => {
|
|
295
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Reveal the value
|
|
299
|
+
await act(async () => {
|
|
300
|
+
fireEvent.click(screen.getByTestId("eye-DB_HOST"));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Edit the value
|
|
304
|
+
await act(async () => {
|
|
305
|
+
fireEvent.change(screen.getByTestId("value-input-DB_HOST"), {
|
|
306
|
+
target: { value: "newhost" },
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(screen.getByTestId("dirty-dot")).toBeInTheDocument();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("clears decrypted values from state on idle timeout", async () => {
|
|
314
|
+
jest.useFakeTimers();
|
|
315
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
316
|
+
ok: true,
|
|
317
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
318
|
+
} as Response);
|
|
319
|
+
|
|
320
|
+
await act(async () => {
|
|
321
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Reveal a value to start the timer
|
|
325
|
+
await act(async () => {
|
|
326
|
+
fireEvent.click(screen.getByTestId("eye-DB_HOST"));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Value should be visible
|
|
330
|
+
expect(screen.getByTestId("value-input-DB_HOST")).toBeInTheDocument();
|
|
331
|
+
|
|
332
|
+
// Advance time past the 5-minute idle timeout
|
|
333
|
+
await act(async () => {
|
|
334
|
+
jest.advanceTimersByTime(5 * 60 * 1000 + 100);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Value should no longer be visible (masked)
|
|
338
|
+
expect(screen.queryByTestId("value-input-DB_HOST")).not.toBeInTheDocument();
|
|
339
|
+
|
|
340
|
+
jest.useRealTimers();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("shows protected confirmation dialog when adding a key in production", async () => {
|
|
344
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
345
|
+
ok: true,
|
|
346
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
347
|
+
} as Response);
|
|
348
|
+
|
|
349
|
+
await act(async () => {
|
|
350
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Switch to production tab
|
|
354
|
+
const prodTab = screen.getAllByRole("tab").find((t) => t.textContent?.includes("production"));
|
|
355
|
+
await act(async () => {
|
|
356
|
+
if (prodTab) fireEvent.click(prodTab);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Open add key form
|
|
360
|
+
await act(async () => {
|
|
361
|
+
fireEvent.click(screen.getByTestId("add-key-btn"));
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await act(async () => {
|
|
365
|
+
fireEvent.change(screen.getByTestId("new-key-input"), {
|
|
366
|
+
target: { value: "PROD_SECRET" },
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Click Add — should show confirmation instead of sending request
|
|
371
|
+
await act(async () => {
|
|
372
|
+
fireEvent.click(screen.getByTestId("add-key-submit"));
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(screen.getByTestId("confirm-protected-dialog")).toBeInTheDocument();
|
|
376
|
+
expect(screen.getByText(/Protected environment/)).toBeInTheDocument();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("sends confirmed: true after confirming protected add", async () => {
|
|
380
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
381
|
+
ok: true,
|
|
382
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
383
|
+
} as Response);
|
|
384
|
+
global.fetch = fetchMock;
|
|
385
|
+
|
|
386
|
+
await act(async () => {
|
|
387
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Switch to production tab
|
|
391
|
+
const prodTab = screen.getAllByRole("tab").find((t) => t.textContent?.includes("production"));
|
|
392
|
+
await act(async () => {
|
|
393
|
+
if (prodTab) fireEvent.click(prodTab);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
await act(async () => {
|
|
397
|
+
fireEvent.click(screen.getByTestId("add-key-btn"));
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await act(async () => {
|
|
401
|
+
fireEvent.change(screen.getByTestId("new-key-input"), {
|
|
402
|
+
target: { value: "PROD_SECRET" },
|
|
403
|
+
});
|
|
404
|
+
fireEvent.change(screen.getByTestId("new-value-input"), {
|
|
405
|
+
target: { value: "s3cret" },
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Click Add — triggers confirmation
|
|
410
|
+
await act(async () => {
|
|
411
|
+
fireEvent.click(screen.getByTestId("add-key-submit"));
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Confirm
|
|
415
|
+
await act(async () => {
|
|
416
|
+
fireEvent.click(screen.getByTestId("confirm-protected-yes"));
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const putCall = fetchMock.mock.calls.find(
|
|
420
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
421
|
+
(c: any[]) =>
|
|
422
|
+
typeof c[0] === "string" && c[0].includes("/PROD_SECRET") && c[1]?.method === "PUT",
|
|
423
|
+
);
|
|
424
|
+
expect(putCall).toBeDefined();
|
|
425
|
+
const body = JSON.parse(putCall![1].body);
|
|
426
|
+
expect(body.confirmed).toBe(true);
|
|
427
|
+
expect(body.value).toBe("s3cret");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("cancels protected add when Cancel is clicked", async () => {
|
|
431
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
432
|
+
ok: true,
|
|
433
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
434
|
+
} as Response);
|
|
435
|
+
global.fetch = fetchMock;
|
|
436
|
+
|
|
437
|
+
await act(async () => {
|
|
438
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Switch to production tab
|
|
442
|
+
const prodTab = screen.getAllByRole("tab").find((t) => t.textContent?.includes("production"));
|
|
443
|
+
await act(async () => {
|
|
444
|
+
if (prodTab) fireEvent.click(prodTab);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
await act(async () => {
|
|
448
|
+
fireEvent.click(screen.getByTestId("add-key-btn"));
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await act(async () => {
|
|
452
|
+
fireEvent.change(screen.getByTestId("new-key-input"), {
|
|
453
|
+
target: { value: "PROD_SECRET" },
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await act(async () => {
|
|
458
|
+
fireEvent.click(screen.getByTestId("add-key-submit"));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Cancel the confirmation
|
|
462
|
+
await act(async () => {
|
|
463
|
+
fireEvent.click(screen.getByTestId("confirm-protected-no"));
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(screen.queryByTestId("confirm-protected-dialog")).not.toBeInTheDocument();
|
|
467
|
+
// No PUT call to PROD_SECRET should have been made
|
|
468
|
+
const putCall = fetchMock.mock.calls.find(
|
|
469
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
470
|
+
(c: any[]) =>
|
|
471
|
+
typeof c[0] === "string" && c[0].includes("/PROD_SECRET") && c[1]?.method === "PUT",
|
|
472
|
+
);
|
|
473
|
+
expect(putCall).toBeUndefined();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("shows protected warning in reset confirmation for production env", async () => {
|
|
477
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
478
|
+
ok: true,
|
|
479
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
480
|
+
} as Response);
|
|
481
|
+
|
|
482
|
+
await act(async () => {
|
|
483
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Switch to production tab
|
|
487
|
+
const prodTab = screen.getAllByRole("tab").find((t) => t.textContent?.includes("production"));
|
|
488
|
+
await act(async () => {
|
|
489
|
+
if (prodTab) fireEvent.click(prodTab);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Open overflow menu and click reset
|
|
493
|
+
await act(async () => {
|
|
494
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
await act(async () => {
|
|
498
|
+
fireEvent.click(screen.getByTestId("reset-random-DB_HOST"));
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
expect(screen.getByTestId("confirm-reset-dialog")).toBeInTheDocument();
|
|
502
|
+
expect(screen.getByText(/This is a protected environment/)).toBeInTheDocument();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("does not show protected confirmation for non-production env", async () => {
|
|
506
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
507
|
+
ok: true,
|
|
508
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
509
|
+
} as Response);
|
|
510
|
+
|
|
511
|
+
await act(async () => {
|
|
512
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Stay on dev tab (default)
|
|
516
|
+
await act(async () => {
|
|
517
|
+
fireEvent.click(screen.getByTestId("add-key-btn"));
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await act(async () => {
|
|
521
|
+
fireEvent.change(screen.getByTestId("new-key-input"), {
|
|
522
|
+
target: { value: "DEV_SECRET" },
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Click Add — should NOT show confirmation dialog
|
|
527
|
+
await act(async () => {
|
|
528
|
+
fireEvent.click(screen.getByTestId("add-key-submit"));
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(screen.queryByTestId("confirm-protected-dialog")).not.toBeInTheDocument();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("shows accept button for pending keys", async () => {
|
|
535
|
+
const decryptedWithPending = {
|
|
536
|
+
...mockDecrypted,
|
|
537
|
+
pending: ["DB_HOST"],
|
|
538
|
+
};
|
|
539
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
540
|
+
ok: true,
|
|
541
|
+
json: () => Promise.resolve(decryptedWithPending),
|
|
542
|
+
} as Response);
|
|
543
|
+
|
|
544
|
+
await act(async () => {
|
|
545
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
expect(screen.getByTestId("accept-value-DB_HOST")).toBeInTheDocument();
|
|
549
|
+
expect(screen.getByTestId("set-value-DB_HOST")).toBeInTheDocument();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("calls accept endpoint and updates row with returned value", async () => {
|
|
553
|
+
const decryptedWithPending = {
|
|
554
|
+
...mockDecrypted,
|
|
555
|
+
pending: ["DB_HOST"],
|
|
556
|
+
};
|
|
557
|
+
const fetchMock = jest.fn().mockImplementation((url: string, opts?: RequestInit) => {
|
|
558
|
+
if (typeof url === "string" && url.includes("/accept") && opts?.method === "POST") {
|
|
559
|
+
return Promise.resolve({
|
|
560
|
+
ok: true,
|
|
561
|
+
json: () => Promise.resolve({ success: true, key: "DB_HOST", value: "abc123" }),
|
|
562
|
+
} as Response);
|
|
563
|
+
}
|
|
564
|
+
return Promise.resolve({
|
|
565
|
+
ok: true,
|
|
566
|
+
json: () => Promise.resolve(decryptedWithPending),
|
|
567
|
+
} as Response);
|
|
568
|
+
});
|
|
569
|
+
global.fetch = fetchMock;
|
|
570
|
+
|
|
571
|
+
await act(async () => {
|
|
572
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Click accept
|
|
576
|
+
await act(async () => {
|
|
577
|
+
fireEvent.click(screen.getByTestId("accept-value-DB_HOST"));
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// Verify the accept endpoint was called
|
|
581
|
+
const acceptCall = fetchMock.mock.calls.find(
|
|
582
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
583
|
+
(c: any[]) =>
|
|
584
|
+
typeof c[0] === "string" && c[0].includes("/DB_HOST/accept") && c[1]?.method === "POST",
|
|
585
|
+
);
|
|
586
|
+
expect(acceptCall).toBeDefined();
|
|
587
|
+
|
|
588
|
+
// Accept button should be gone (no longer pending)
|
|
589
|
+
expect(screen.queryByTestId("accept-value-DB_HOST")).not.toBeInTheDocument();
|
|
590
|
+
// Eye icon should be present (can reveal the value)
|
|
591
|
+
expect(screen.getByTestId("eye-DB_HOST")).toBeInTheDocument();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("shows delete option in overflow menu", async () => {
|
|
595
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
596
|
+
ok: true,
|
|
597
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
598
|
+
} as Response);
|
|
599
|
+
|
|
600
|
+
await act(async () => {
|
|
601
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await act(async () => {
|
|
605
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
expect(screen.getByTestId("delete-key-DB_HOST")).toBeInTheDocument();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("shows confirmation dialog before deleting a key", async () => {
|
|
612
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
613
|
+
ok: true,
|
|
614
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
615
|
+
} as Response);
|
|
616
|
+
|
|
617
|
+
await act(async () => {
|
|
618
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
await act(async () => {
|
|
622
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
await act(async () => {
|
|
626
|
+
fireEvent.click(screen.getByTestId("delete-key-DB_HOST"));
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
expect(screen.getByTestId("confirm-delete-dialog")).toBeInTheDocument();
|
|
630
|
+
expect(screen.getByText(/Permanently delete/)).toBeInTheDocument();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("deletes key after confirming", async () => {
|
|
634
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
635
|
+
ok: true,
|
|
636
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
637
|
+
} as Response);
|
|
638
|
+
global.fetch = fetchMock;
|
|
639
|
+
|
|
640
|
+
await act(async () => {
|
|
641
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
await act(async () => {
|
|
645
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
await act(async () => {
|
|
649
|
+
fireEvent.click(screen.getByTestId("delete-key-DB_HOST"));
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
await act(async () => {
|
|
653
|
+
fireEvent.click(screen.getByTestId("confirm-delete-yes"));
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Verify DELETE was called
|
|
657
|
+
const deleteCall = fetchMock.mock.calls.find(
|
|
658
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
659
|
+
(c: any[]) =>
|
|
660
|
+
typeof c[0] === "string" && c[0].includes("/DB_HOST") && c[1]?.method === "DELETE",
|
|
661
|
+
);
|
|
662
|
+
expect(deleteCall).toBeDefined();
|
|
663
|
+
|
|
664
|
+
// Row should be removed
|
|
665
|
+
expect(screen.queryByText("DB_HOST")).not.toBeInTheDocument();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("cancels delete when Cancel is clicked", async () => {
|
|
669
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
670
|
+
ok: true,
|
|
671
|
+
json: () => Promise.resolve(mockDecrypted),
|
|
672
|
+
} as Response);
|
|
673
|
+
|
|
674
|
+
await act(async () => {
|
|
675
|
+
render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await act(async () => {
|
|
679
|
+
fireEvent.click(screen.getByTestId("overflow-DB_HOST"));
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
await act(async () => {
|
|
683
|
+
fireEvent.click(screen.getByTestId("delete-key-DB_HOST"));
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
await act(async () => {
|
|
687
|
+
fireEvent.click(screen.getByTestId("confirm-delete-no"));
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
expect(screen.queryByTestId("confirm-delete-dialog")).not.toBeInTheDocument();
|
|
691
|
+
// Key should still be there
|
|
692
|
+
expect(screen.getByText("DB_HOST")).toBeInTheDocument();
|
|
693
|
+
});
|
|
694
|
+
});
|