Merge pull request #516 from danditomaso/feat/add-key-mismatch-error-handling

feat: add error handling for key mismatch
This commit is contained in:
Dan Ditomaso
2025-03-19 12:42:38 -04:00
committed by GitHub
16 changed files with 513 additions and 93 deletions
+7
View File
@@ -8,6 +8,7 @@ import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
export const DialogManager = () => {
const { channels, config, dialog, setDialogOpen } = useDevice();
@@ -70,6 +71,12 @@ export const DialogManager = () => {
setDialogOpen("unsafeRoles", open);
}}
/>
<RefreshKeysDialog
open={dialog.refreshKeys}
onOpenChange={(open) => {
setDialogOpen("refreshKeys", open);
}}
/>
</>
);
};
@@ -0,0 +1,55 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { beforeEach, describe, expect, it, Mock, vi } from "vitest";
import { RefreshKeysDialog } from "./RefreshKeysDialog";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
vi.mock("./useRefreshKeysDialog.ts", () => ({
useRefreshKeysDialog: vi.fn(),
}));
describe("RefreshKeysDialog Component", () => {
let handleCloseDialogMock: Mock;
let handleNodeRemoveMock: Mock;
let onOpenChangeMock: Mock;
beforeEach(() => {
handleCloseDialogMock = vi.fn();
handleNodeRemoveMock = vi.fn();
onOpenChangeMock = vi.fn();
(useRefreshKeysDialog as Mock).mockReturnValue({
handleCloseDialog: handleCloseDialogMock,
handleNodeRemove: handleNodeRemoveMock,
});
});
it("renders the dialog with correct content", () => {
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
expect(screen.getByText("Keys Mismatch")).toBeInTheDocument();
expect(screen.getByText("Request New Keys")).toBeInTheDocument();
expect(screen.getByText("Dismiss")).toBeInTheDocument();
});
it("calls handleNodeRemove when 'Request New Keys' button is clicked", () => {
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
fireEvent.click(screen.getByText("Request New Keys"));
expect(handleNodeRemoveMock).toHaveBeenCalled();
});
it("calls handleCloseDialog when 'Dismiss' button is clicked", () => {
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
fireEvent.click(screen.getByText("Dismiss"));
expect(handleCloseDialogMock).toHaveBeenCalled();
});
it("calls onOpenChange when dialog close button is clicked", () => {
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
fireEvent.click(screen.getByRole("button", { name: /close/i }));
expect(handleCloseDialogMock).toHaveBeenCalled();
});
it("does not render when open is false", () => {
render(<RefreshKeysDialog open={false} onOpenChange={onOpenChangeMock} />);
expect(screen.queryByText("Keys Mismatch")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,61 @@
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { LockKeyholeOpenIcon } from "lucide-react";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
export interface RefreshKeysDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-8 flex flex-col gap-2">
<DialogClose onClick={handleCloseDialog} />
<DialogHeader>
<DialogTitle>Keys Mismatch</DialogTitle>
</DialogHeader>
Your node is unable to send a direct message to this node. This is due to the remote node's current public key not matching the previously stored key for this node.
<ul className="mt-2">
<li className="flex place-items-center gap-2 items-start">
<div className="p-2 bg-slate-500 rounded-lg mt-1">
<LockKeyholeOpenIcon size={30} className="text-white justify-center" />
</div>
<div className="flex flex-col gap-2">
<div>
<p className="font-bold mb-0.5">Accept New Keys</p>
<p>
This will remove the node from device and request new keys.
</p>
</div>
<Button
variant="default"
onClick={handleNodeRemove}
className=""
>
Request New Keys
</Button>
<Button
variant="outline"
onClick={handleCloseDialog}
className=""
>
Dismiss
</Button>
</div>
</li>
</ul>
{/* </DialogDescription> */}
</DialogContent>
</Dialog >
);
};
@@ -0,0 +1,77 @@
import { renderHook, act } from "@testing-library/react";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
import { beforeEach, describe, expect, it, Mock, vi } from "vitest";
import { useDevice } from "@core/stores/deviceStore.ts";
vi.mock("@core/stores/appStore.ts", () => ({
useAppStore: vi.fn(() => ({ activeChat: "chat-123" })),
}));
vi.mock("@core/stores/deviceStore.ts", () => ({
useDevice: vi.fn(() => ({
removeNode: vi.fn(),
setDialogOpen: vi.fn(),
getNodeError: vi.fn(),
clearNodeError: vi.fn(),
})),
}));
describe("useRefreshKeysDialog Hook", () => {
let removeNodeMock: Mock;
let setDialogOpenMock: Mock;
let getNodeErrorMock: Mock;
let clearNodeErrorMock: Mock;
beforeEach(() => {
removeNodeMock = vi.fn();
setDialogOpenMock = vi.fn();
getNodeErrorMock = vi.fn();
clearNodeErrorMock = vi.fn();
(useDevice as Mock).mockReturnValue({
removeNode: removeNodeMock,
setDialogOpen: setDialogOpenMock,
getNodeError: getNodeErrorMock,
clearNodeError: clearNodeErrorMock,
});
});
it("handleNodeRemove should remove the node and update dialog if there is an error", () => {
getNodeErrorMock.mockReturnValue({ node: "node-abc" });
const { result } = renderHook(() => useRefreshKeysDialog());
act(() => {
result.current.handleNodeRemove();
});
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123");
expect(clearNodeErrorMock).toHaveBeenCalledWith("chat-123");
expect(removeNodeMock).toHaveBeenCalledWith("node-abc");
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
});
it("handleNodeRemove should do nothing if there is no error", () => {
getNodeErrorMock.mockReturnValue(undefined);
const { result } = renderHook(() => useRefreshKeysDialog());
act(() => {
result.current.handleNodeRemove();
});
expect(removeNodeMock).not.toHaveBeenCalled();
expect(setDialogOpenMock).not.toHaveBeenCalled();
expect(clearNodeErrorMock).not.toHaveBeenCalled();
});
it("handleCloseDialog should close the dialog", () => {
const { result } = renderHook(() => useRefreshKeysDialog());
act(() => {
result.current.handleCloseDialog();
});
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
});
});
@@ -0,0 +1,28 @@
import { useCallback } from "react";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
export function useRefreshKeysDialog() {
const { removeNode, setDialogOpen, clearNodeError, getNodeError } = useDevice();
const { activeChat } = useAppStore();
const handleNodeRemove = useCallback(() => {
const nodeWithError = getNodeError(activeChat);
if (!nodeWithError) {
return;
}
clearNodeError(activeChat);
handleCloseDialog();;
return removeNode(nodeWithError?.node);
}, [activeChat, clearNodeError, setDialogOpen, removeNode]);
const handleCloseDialog = useCallback(() => {
setDialogOpen('refreshKeys', false);
}, [setDialogOpen])
return {
handleCloseDialog,
handleNodeRemove
};
}