Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,45 @@ const mockWalletForRecipient = (session = {
origUseWallet.mockReturnValue({ session, isHydrated: true } as ReturnType<typeof origUseWallet>);
};

describe("StreamDetailsContent action authorization", () => {
beforeEach(() => {
vi.clearAllMocks();
mockTracker.status = "idle";
global.fetch = createFetchMock();
});

it("shows only recipient actions to the stream recipient", async () => {
mockWalletForRecipient();
await renderLoaded();

expect(screen.getByRole("button", { name: /withdraw/i })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^top up$/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^pause$/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^cancel$/i })).not.toBeInTheDocument();
});

it("shows only sender actions to the stream sender", async () => {
origUseWallet.mockReturnValue({ session: mockSession, isHydrated: true } as ReturnType<typeof origUseWallet>);
await renderLoaded();

expect(screen.queryByRole("button", { name: /withdraw/i })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: /^top up$/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /^pause$/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /^cancel$/i })).toBeInTheDocument();
});

it("hides all stream actions from an unrelated wallet", async () => {
origUseWallet.mockReturnValue({
session: { ...mockSession, publicKey: "GUNRELATED" },
isHydrated: true,
} as ReturnType<typeof origUseWallet>);
await renderLoaded();

expect(screen.queryByRole("heading", { name: "Actions" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /withdraw|top up|pause|resume|cancel/i })).not.toBeInTheDocument();
});
});

describe("StreamDetailsContent handleWithdraw", () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -405,6 +444,20 @@ describe("StreamDetailsContent handleWithdraw", () => {
const withdrawBtn = screen.getByRole("button", { name: /withdraw/i });
expect(withdrawBtn).toBeDisabled();
});

it("renders zero rate and zero balance without a non-finite claimable value", async () => {
mockUseStreamingAmount.mockReturnValue(0);
await renderLoaded({
depositedAmount: "0",
withdrawnAmount: "0",
ratePerSecond: "0",
endTime: Math.floor(Date.now() / 1000),
});

expect(screen.getByText("Claimable")).toBeInTheDocument();
expect(screen.getAllByText(/0(?:\.0+)? XLM/).length).toBeGreaterThan(0);
expect(screen.queryByText(/NaN|Infinity/)).not.toBeInTheDocument();
});
});

// ─── handleTopUp ──────────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ describe("TopUpModal", () => {
expect(getAmountInput().value).toBe("1.1234567");
});

it("accepts the maximum supported precision at the amount boundary", async () => {
const user = userEvent.setup();
render(<TopUpModal {...baseProps} />);
await user.type(getAmountInput(), "1.1234567");

fireEvent.click(getConfirmButton());

await waitFor(() => {
expect(baseProps.onConfirm).toHaveBeenCalledWith("stream-42", "1.1234567");
});
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});

it("submits a valid amount and shows a success toast", async () => {
const user = userEvent.setup();
render(<TopUpModal {...baseProps} />);
Expand Down
65 changes: 64 additions & 1 deletion frontend/src/hooks/useIncomingStreams.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { renderHook, act } from "@testing-library/react";
import { renderHook, act, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import React from "react";
Expand Down Expand Up @@ -97,6 +97,31 @@ describe("useIncomingStreams hooks", () => {
expect(vi.mocked(fetchIncomingStreams).mock.calls.length).toBeGreaterThan(1);
vi.useRealTimers();
});

it("does not expose a stale response after switching wallets", async () => {
let resolveFirst!: (streams: IncomingStreamRecord[]) => void;
vi.mocked(fetchIncomingStreams)
.mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve; }))
.mockResolvedValueOnce([{ id: "new-wallet-stream" } as IncomingStreamRecord]);

const { result, rerender } = renderHook(
({ publicKey }: { publicKey: string }) => useIncomingStreams(publicKey),
{ wrapper, initialProps: { publicKey: "old-wallet" } },
);

await waitFor(() => expect(resolveFirst).toBeTypeOf("function"));
rerender({ publicKey: "new-wallet" });
await waitFor(() => {
expect(result.current.data?.[0]?.id).toBe("new-wallet-stream");
});

await act(async () => {
resolveFirst([{ id: "old-wallet-stream" } as IncomingStreamRecord]);
await Promise.resolve();
});

expect(result.current.data?.[0]?.id).toBe("new-wallet-stream");
});
});

describe("useWithdrawIncomingStream", () => {
Expand Down Expand Up @@ -191,5 +216,43 @@ describe("useIncomingStreams hooks", () => {
callsBeforeUnmount,
);
});

it("aborts polling when the wallet changes mid-backoff", async () => {
vi.useFakeTimers();
vi.mocked(withdrawFromStream).mockResolvedValue({ success: true, txHash: "tx-hash" });
vi.mocked(fetchIncomingStreams).mockResolvedValue([]);

const { result, rerender } = renderHook(
({ publicKey }: { publicKey: string }) =>
useWithdrawIncomingStream({} as unknown as WalletSession, publicKey),
{ wrapper, initialProps: { publicKey: "old-wallet" } },
);

await act(async () => {
await result.current.mutateAsync({
id: "1",
streamId: 1,
withdrawn: 0,
deposited: 100,
ratePerSecond: 1,
isPaused: false,
lastUpdateTime: Date.now() / 1000,
} as unknown as IncomingStreamRecord);
});

await act(async () => {
await vi.advanceTimersByTimeAsync(1_000);
});
const callsBeforeWalletChange = vi.mocked(fetchIncomingStreams).mock.calls.length;

rerender({ publicKey: "new-wallet" });
await act(async () => {
await vi.advanceTimersByTimeAsync(63_000);
});

expect(vi.mocked(fetchIncomingStreams).mock.calls.length).toBe(callsBeforeWalletChange);
expect(queryClient.getQueryData(incomingStreamsQueryKey("new-wallet"))).toBeUndefined();
vi.useRealTimers();
});
});
});
Loading