From 3f8d3389d5b22a8e2ea9bc98acacde0b3a077fee Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Sun, 16 Mar 2025 22:56:58 -0400 Subject: [PATCH] feat: add error handling for key mismatch --- src/components/Dialog/DialogManager.tsx | 7 + .../RefreshKeysDialog.test.tsx | 55 ++++++++ .../RefreshKeysDialog/RefreshKeysDialog.tsx | 60 +++++++++ .../useRefreshKeysDialog.test.ts | 77 +++++++++++ .../RefreshKeysDialog/useRefreshKeysDialog.ts | 28 ++++ .../PageComponents/Connect/Serial.tsx | 6 +- .../PageComponents/Messages/ChannelChat.tsx | 5 +- .../PageComponents/Messages/Message.tsx | 122 +++++++++-------- .../PageComponents/Messages/MessageItem.tsx | 126 ++++++++++++++++++ src/components/UI/Avatar.tsx | 11 +- src/components/UI/Button.tsx | 2 +- src/core/stores/deviceStore.ts | 58 +++++++- src/core/subscriptions.ts | 29 ++-- src/pages/Messages.tsx | 6 +- src/pages/Nodes.tsx | 2 - vite.config.ts | 6 +- 16 files changed, 507 insertions(+), 93 deletions(-) create mode 100644 src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx create mode 100644 src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx create mode 100644 src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts create mode 100644 src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts create mode 100644 src/components/PageComponents/Messages/MessageItem.tsx diff --git a/src/components/Dialog/DialogManager.tsx b/src/components/Dialog/DialogManager.tsx index e8d597d4..bb5f660b 100644 --- a/src/components/Dialog/DialogManager.tsx +++ b/src/components/Dialog/DialogManager.tsx @@ -8,6 +8,7 @@ import { RebootDialog } from "@components/Dialog/RebootDialog.tsx"; import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx"; import { NodeDetailsDialog } from "@components/Dialog/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); }} /> + { + setDialogOpen("refreshKeys", open); + }} + /> ); }; diff --git a/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx new file mode 100644 index 00000000..e955b88f --- /dev/null +++ b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx @@ -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(); + 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(); + fireEvent.click(screen.getByText("Request New Keys")); + expect(handleNodeRemoveMock).toHaveBeenCalled(); + }); + + it("calls handleCloseDialog when 'Dismiss' button is clicked", () => { + render(); + fireEvent.click(screen.getByText("Dismiss")); + expect(handleCloseDialogMock).toHaveBeenCalled(); + }); + + it("calls onOpenChange when dialog close button is clicked", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: /close/i })); + expect(handleCloseDialogMock).toHaveBeenCalled(); + }); + + it("does not render when open is false", () => { + render(); + expect(screen.queryByText("Keys Mismatch")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx new file mode 100644 index 00000000..1d067458 --- /dev/null +++ b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx @@ -0,0 +1,60 @@ +import { + Dialog, + DialogClose, + DialogContent, + DialogHeader, + DialogTitle, +} from "@components/UI/Dialog.tsx"; +import { Button } from "@components/UI/Button.tsx"; +import { LockKeyholeOpenIcon } from "lucide-react"; +import { P } from "@components/UI/Typography/P.tsx"; +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 ( + + + + + Keys Mismatch + + Your node is unable to send a direct message to this node. This is due to public/private key mismatch. +
    +
  • +
    + +
    +
    +

    Refresh this node

    +

    + This will remove the node from the chat and request new keys. The process may take a few moments to complete. +

    + + +
    +
  • +
+ {/* */} +
+
+ ); +}; diff --git a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts new file mode 100644 index 00000000..b26d0165 --- /dev/null +++ b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts @@ -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); + }); +}); diff --git a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts new file mode 100644 index 00000000..821aade7 --- /dev/null +++ b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts @@ -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 + }; + +} \ No newline at end of file diff --git a/src/components/PageComponents/Connect/Serial.tsx b/src/components/PageComponents/Connect/Serial.tsx index b6cdf5f9..28085cc2 100644 --- a/src/components/PageComponents/Connect/Serial.tsx +++ b/src/components/PageComponents/Connect/Serial.tsx @@ -18,9 +18,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => { setSerialPorts(await navigator?.serial.getPorts()); }, []); - navigator?.serial?.addEventListener("connect", (event) => { - console.log(event); - + navigator?.serial?.addEventListener("connect", () => { updateSerialPortList(); }); navigator?.serial?.addEventListener("disconnect", () => { @@ -47,8 +45,6 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
{serialPorts.map((port, index) => { - console.log(port); - const { usbProductId, usbVendorId } = port.getInfo(); return (