Files
kandev/apps/web/components/settings/secrets-settings.test.ts
T
2026-08-22 18:49:34 +00:00

170 lines
5.8 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createElement, type ReactNode } from "react";
import type { SecretListItem } from "@/lib/types/http-secrets";
const mocks = vi.hoisted(() => ({
deleteSecret: vi.fn(),
addSecret: vi.fn(),
updateSecret: vi.fn(),
removeSecret: vi.fn(),
toast: vi.fn(),
items: [] as SecretListItem[],
responsive: { isFinePointer: true },
}));
vi.mock("@/lib/api/domains/secrets-api", () => ({
createSecret: vi.fn(),
updateSecret: mocks.updateSecret,
deleteSecret: mocks.deleteSecret,
}));
vi.mock("@/hooks/domains/settings/use-secrets", () => ({
useSecrets: () => ({
loaded: true,
items: mocks.items,
addSecret: mocks.addSecret,
updateSecret: mocks.updateSecret,
removeSecret: mocks.removeSecret,
}),
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: unknown) => unknown) =>
selector({ addSecret: mocks.addSecret, workspaces: { items: [] } }),
}));
vi.mock("@/components/toast-provider", () => ({ useToast: () => ({ toast: mocks.toast }) }));
vi.mock("@/hooks/use-responsive-breakpoint", () => ({
useResponsiveBreakpoint: () => mocks.responsive,
}));
vi.mock("@/components/settings/settings-page-template", () => ({
SettingsPageTemplate: ({ children }: { children: ReactNode }) =>
createElement("div", null, children),
}));
vi.mock("@/components/settings/workspaces/workspace-section-header", () => ({
WorkspaceSectionHeader: () => null,
}));
vi.mock("./copy-move-secret-dialog", () => ({ CopyMoveSecretDialog: () => null }));
import { getSecretDraftMeta, SecretsSettings } from "./secrets-settings";
const secret: SecretListItem = {
id: "secret-1",
name: "Saved secret",
has_value: true,
created_at: "",
updated_at: "",
};
const DELETE_BUTTON = "Delete secret Saved secret";
const CONFIRM_POPOVER_TEST_ID = "secret-delete-confirm-popover";
const CONFIRM_TEST_ID = "secret-delete-confirm";
function renderSettings() {
render(createElement(SecretsSettings, { initialItems: mocks.items }));
}
beforeEach(() => {
mocks.items = [secret];
mocks.responsive.isFinePointer = true;
mocks.deleteSecret.mockReset();
mocks.deleteSecret.mockResolvedValue(undefined);
mocks.removeSecret.mockReset();
mocks.toast.mockReset();
});
afterEach(() => cleanup());
describe("getSecretDraftMeta", () => {
it("treats a newly opened secret form as dirty before persistence", () => {
const state = getSecretDraftMeta([], null, true, { name: "", value: "" });
expect(state.isDirty).toBe(true);
expect(state.revision.startsWith("new:")).toBe(true);
});
it("tracks existing secret edits against the saved name", () => {
expect(
getSecretDraftMeta([secret], secret.id, false, { name: secret.name, value: "" }).isDirty,
).toBe(false);
expect(
getSecretDraftMeta([secret], secret.id, false, { name: "Renamed", value: "" }).isDirty,
).toBe(true);
});
});
describe("SecretsSettings deletion lifecycle", () => {
it("cancels locally without dispatching deletion", () => {
renderSettings();
fireEvent.click(screen.getByRole("button", { name: DELETE_BUTTON }));
const popover = screen.getByTestId(CONFIRM_POPOVER_TEST_ID);
fireEvent.click(within(popover).getByRole("button", { name: "Cancel" }));
expect(mocks.deleteSecret).not.toHaveBeenCalled();
expect(screen.queryByTestId(CONFIRM_POPOVER_TEST_ID)).toBeNull();
});
it("closes before dispatching exactly one delete and removes on success", async () => {
let confirmationClosed = false;
mocks.deleteSecret.mockImplementation(async () => {
confirmationClosed = screen.queryByTestId(CONFIRM_POPOVER_TEST_ID) === null;
});
renderSettings();
fireEvent.click(screen.getByRole("button", { name: DELETE_BUTTON }));
fireEvent.click(
within(screen.getByTestId(CONFIRM_POPOVER_TEST_ID)).getByTestId(CONFIRM_TEST_ID),
);
await waitFor(() => expect(mocks.deleteSecret).toHaveBeenCalledTimes(1));
expect(confirmationClosed).toBe(true);
await waitFor(() => expect(mocks.removeSecret).toHaveBeenCalledWith(secret.id));
});
it("keeps row actions busy until deletion settles", async () => {
let resolveDelete!: () => void;
mocks.deleteSecret.mockReturnValue(
new Promise<void>((resolve) => {
resolveDelete = resolve;
}),
);
renderSettings();
fireEvent.click(screen.getByRole("button", { name: DELETE_BUTTON }));
fireEvent.click(
within(screen.getByTestId(CONFIRM_POPOVER_TEST_ID)).getByTestId(CONFIRM_TEST_ID),
);
await waitFor(() => expect(mocks.deleteSecret).toHaveBeenCalledTimes(1));
expect(
(screen.getByRole("button", { name: DELETE_BUTTON }) as HTMLButtonElement).disabled,
).toBe(true);
resolveDelete();
await waitFor(() => expect(mocks.removeSecret).toHaveBeenCalledWith(secret.id));
});
it("keeps the secret and reports a localized failure when deletion fails", async () => {
mocks.deleteSecret.mockRejectedValue(new Error("server details must stay out of copy"));
renderSettings();
fireEvent.click(screen.getByRole("button", { name: DELETE_BUTTON }));
fireEvent.click(
within(screen.getByTestId(CONFIRM_POPOVER_TEST_ID)).getByTestId(CONFIRM_TEST_ID),
);
await waitFor(() =>
expect(mocks.toast).toHaveBeenCalledWith({
description: "Couldn't delete secret",
variant: "error",
}),
);
expect(screen.getByText(secret.name)).not.toBeNull();
expect(
(screen.getByRole("button", { name: DELETE_BUTTON }) as HTMLButtonElement).disabled,
).toBe(false);
expect(mocks.removeSecret).not.toHaveBeenCalled();
expect(JSON.stringify(mocks.toast.mock.calls)).not.toContain(
"server details must stay out of copy",
);
});
});