Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
33 changes: 33 additions & 0 deletions __tests__/freighter_connector.component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@ import {
HIGH_FEE_THRESHOLD_STROOPS,
} from "@/app/lib/freighter_connector";

// WalletProvider pulls @creit.tech/stellar-wallets-kit at module scope, whose
// bundled UMD dependencies are not Node-ESM-importable. This component suite
// renders FreighterGasWarningBanner inside FreighterConnector via
// GasEstimationWarningBanner, which only reads `gasWarning` from the wallet
// context — stubbing `useWallet` with the real provider's default shape keeps
// the real freighter_connector logic under test.
const walletContextMock = vi.hoisted(() => ({
useWallet: () => ({
address: null,
assembleMultiSigTransaction: vi.fn(async () => ({
uniqueSigners: 0,
splitsValidated: 0,
})),
connect: vi.fn(async () => {}),
disconnect: vi.fn(),
isConnecting: false,
networkMismatchMessage: null,
selectedWalletId: "freighter",
setSelectedWalletId: vi.fn(),
signTransaction: vi.fn(async () => ""),
signatureTimeoutError: null,
signatureTimeoutXdr: null,
clearSignatureTimeout: vi.fn(),
simulationResult: null,
setSimulationResult: vi.fn(),
gasWarning: null,
}),
}));

vi.mock("@/app/context/WalletContext", () => ({
useWallet: walletContextMock.useWallet,
}));

// ---------------------------------------------------------------------------
// #112 — React Testing Library assertions for freighter_connector
// ---------------------------------------------------------------------------
Expand Down
27 changes: 23 additions & 4 deletions __tests__/freighter_multisig_hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { Account, Keypair, Networks, Operation, TransactionBuilder } from "@stellar/stellar-sdk";
import { useFreighterMultiSigAssembly } from "@/app/hooks/useFreighterMultiSigAssembly";
import { WalletMultiSigStructureError } from "@/app/lib/wallet_state_context";
import {
Expand All @@ -13,13 +14,31 @@ function toBase64(text: string): string {

const TEST_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";

// The hook validates XDR against a real @stellar/stellar-sdk parser, so the
// fixtures must be genuine signed transaction envelopes rather than arbitrary
// bytes. Building a real envelope (and signing it so `signatures` is
// populated) exercises the parser instead of failing it.
function buildSignedEnvelope(): string {
const keypair = Keypair.random();
const account = new Account(keypair.publicKey(), "0");
const transaction = new TransactionBuilder(account, {
fee: "100",
networkPassphrase: Networks.TESTNET,
})
.addOperation(Operation.bumpSequence({ bumpTo: "0" }))
.setTimeout(30)
.build();
transaction.sign(keypair);
return transaction.toEnvelope().toXDR("base64");
}

describe("useFreighterMultiSigAssembly hook (#109)", () => {
it("parseStructure parses a well-formed envelope without errors", () => {
const { result } = renderHook(() =>
useFreighterMultiSigAssembly(TEST_NETWORK_PASSPHRASE)
);

const xdr = toBase64("a".repeat(200));
const xdr = buildSignedEnvelope();
const shape = result.current.parseStructure(xdr);

expect(shape.baseXdr).toBe(xdr);
Expand All @@ -42,7 +61,7 @@ describe("useFreighterMultiSigAssembly hook (#109)", () => {
useFreighterMultiSigAssembly(TEST_NETWORK_PASSPHRASE)
);

const xdr = toBase64("a".repeat(200));
const xdr = buildSignedEnvelope();
const tx = result.current.prepareTransaction(xdr);
expect(tx).toBeDefined();
});
Expand All @@ -62,7 +81,7 @@ describe("useFreighterMultiSigAssembly hook (#109)", () => {
useFreighterMultiSigAssembly(TEST_NETWORK_PASSPHRASE)
);

const xdr = toBase64("b".repeat(200));
const xdr = buildSignedEnvelope();
const tx = result.current.prepareTransaction(xdr);
const serialized = result.current.serializeTransaction(tx);
expect(typeof serialized).toBe("string");
Expand All @@ -74,7 +93,7 @@ describe("useFreighterMultiSigAssembly hook (#109)", () => {
useFreighterMultiSigAssembly(TEST_NETWORK_PASSPHRASE)
);

const xdr = toBase64("c".repeat(200));
const xdr = buildSignedEnvelope();
const signFn = vi.fn(async () => "signed-xdr");

const signed = await result.current.signTransaction(xdr, signFn);
Expand Down
197 changes: 197 additions & 0 deletions __tests__/network_sync_checker_logging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
formatConsoleWarningBlock,
formatStackTrace,
logNetworkSyncWarning,
networkSyncTracker,
runNetworkSyncSign,
validateNetworkSyncWithSignature,
} from "@/app/lib/network_sync_checker";

const LOG_PREFIX = "[network_sync_checker]";

describe("network_sync_checker formatted console output", () => {
beforeEach(() => {
networkSyncTracker.clear();
});

describe("formatStackTrace", () => {
it("returns the real stack of an Error", () => {
const stack = formatStackTrace(new Error("boom"));
expect(stack).toContain("Error: boom");
expect(stack).toContain("\n");
});

it("keeps multi-line strings as-is", () => {
const lines = "line one\nline two";
expect(formatStackTrace(lines)).toBe(lines);
});

it("synthesizes a stack for primitives and single-line values", () => {
expect(formatStackTrace("just a message")).toContain(
"Error: just a message"
);
expect(formatStackTrace(undefined)).toContain("network_sync_checker trace");
});
});

describe("formatConsoleWarningBlock", () => {
it("renders a bordered block with title, body, txId, phase and stack frames", () => {
const block = formatConsoleWarningBlock({
title: "TX ERROR",
body: "signature rejected during network sync",
stack: "Error: nope\n at probe (file.ts:1:1)",
txId: "sync-123",
phase: "error",
});

expect(block).toContain(`${LOG_PREFIX} ╔══`);
expect(block).toContain(`${LOG_PREFIX} ╚══`);
expect(block).toContain("TX ERROR");
expect(block).toContain(`${LOG_PREFIX} signature rejected during network sync`);
expect(block).toContain(`${LOG_PREFIX} txId: sync-123`);
expect(block).toContain(`${LOG_PREFIX} phase: error`);
expect(block).toContain(`${LOG_PREFIX} --- stack trace ---`);
expect(block).toContain(`${LOG_PREFIX} at probe (file.ts:1:1)`);
expect(block).toContain(`${LOG_PREFIX} --- end stack ---`);
});
});

describe("logNetworkSyncWarning", () => {
it("logs a single formatted warning block to the console", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

const formatted = logNetworkSyncWarning(
"NETWORK MISMATCH",
"wallet availability check failed",
{ phase: "checking" }
);

expect(warnSpy).toHaveBeenCalledTimes(1);
expect(String(warnSpy.mock.calls[0][0])).toContain(LOG_PREFIX);
expect(String(warnSpy.mock.calls[0][0])).toContain("NETWORK MISMATCH");
expect(formatted).toContain(LOG_PREFIX);
warnSpy.mockRestore();
});
});
});

describe("network_sync_checker transaction tracking", () => {
let warnSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
networkSyncTracker.clear();
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
});

afterEach(() => {
warnSpy.mockRestore();
});

it("records successful probes without console noise", async () => {
const result = await runNetworkSyncSign(async () => "signed", vi.fn(), {
txId: "sync-ok",
});

expect(result).toBe("signed");
expect(warnSpy).not.toHaveBeenCalled();

const history = networkSyncTracker.getHistory("sync-ok");
expect(history).toHaveLength(1);
expect(history[0]).toMatchObject({
txId: "sync-ok",
phase: "success",
message: "network sync probe signed",
});
});

it("emits one formatted TX ERROR block when the user rejects the probe", async () => {
const showToast = vi.fn();

const result = await runNetworkSyncSign(async () => {
throw new Error("user rejected transaction");
}, showToast);

expect(result).toBeNull();
expect(showToast).toHaveBeenCalledWith(
"Network sync cancelled — you rejected the signature in your wallet.",
"warning"
);
expect(warnSpy).toHaveBeenCalledTimes(1);

const logged = String(warnSpy.mock.calls[0][0]);
expect(logged).toContain(LOG_PREFIX);
expect(logged).toContain("TX ERROR");
expect(logged).toContain("signature rejected during network sync");
expect(logged).toContain("phase: error");

const history = networkSyncTracker.getHistory("network-sync-probe");
expect(history).toHaveLength(1);
expect(history[0].phase).toBe("error");
});

it("emits no tracking block and no warn when a non-rejection error rethrows", async () => {
await expect(
runNetworkSyncSign(async () => {
throw new Error("horizon unreachable");
}, vi.fn())
).rejects.toThrow("horizon unreachable");

expect(warnSpy).not.toHaveBeenCalled();
expect(networkSyncTracker.getHistory()).toHaveLength(0);
});

it("tracks the out-of-sync failure in validateNetworkSyncWithSignature", async () => {
const showToast = vi.fn();
const signFn = vi.fn();

const result = await validateNetworkSyncWithSignature(
"mainnet",
"testnet",
signFn,
showToast,
{ txId: "sync-mismatch" }
);

expect(result).toBeNull();
expect(signFn).not.toHaveBeenCalled();
expect(showToast).toHaveBeenCalledWith(
expect.stringMatching(/Network out of sync/i),
"warning"
);
expect(warnSpy).toHaveBeenCalledTimes(1);

const logged = String(warnSpy.mock.calls[0][0]);
expect(logged).toContain(LOG_PREFIX);
expect(logged).toContain("network out of sync");

const history = networkSyncTracker.getHistory("sync-mismatch");
expect(history).toHaveLength(1);
expect(history[0].phase).toBe("error");
});

it("records success through validateNetworkSyncWithSignature when aligned", async () => {
const result = await validateNetworkSyncWithSignature(
"testnet",
"testnet",
async () => "signed",
vi.fn(),
{ txId: "sync-align" }
);

expect(result).toBe("signed");
expect(warnSpy).not.toHaveBeenCalled();
expect(networkSyncTracker.getHistory("sync-align")[0].phase).toBe("success");
});

it("filters history by txId and clears the tracker", () => {
networkSyncTracker.track("a", "signing", "start");
networkSyncTracker.track("b", "signing", "start");

expect(networkSyncTracker.getHistory("a")).toHaveLength(1);
expect(networkSyncTracker.getHistory()).toHaveLength(2);

networkSyncTracker.clear();
expect(networkSyncTracker.getHistory()).toHaveLength(0);
});
});
44 changes: 41 additions & 3 deletions __tests__/signature_timeout_alert.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
import { render, screen, waitFor } from "@testing-library/react";
import { act, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import SignatureTimeoutAlert from "@/app/components/SignatureTimeoutAlert";
import WalletLoaderOverlay from "@/app/components/WalletLoaderOverlay";
import { endWalletOperation, startWalletOperation } from "@/app/lib/wallet_state_context";

// WalletProvider pulls @creit.tech/stellar-wallets-kit at module scope, whose
// bundled UMD dependencies are not Node-ESM-importable. These suites never
// exercise provider-driven wallet state (they render the components with bare
// props), so `useWallet` is stubbed with the same defaults the real
// WalletContext provides via `createContext`. The real wallet library modules
// (wallet_state_context, freighter_connector, albedo_connector,
// ledger_usb_bridge) stay under test.
const walletContextMock = vi.hoisted(() => ({
useWallet: () => ({
address: null,
assembleMultiSigTransaction: vi.fn(async () => ({
uniqueSigners: 0,
splitsValidated: 0,
})),
connect: vi.fn(async () => {}),
disconnect: vi.fn(),
isConnecting: false,
networkMismatchMessage: null,
selectedWalletId: "albedo",
setSelectedWalletId: vi.fn(),
signTransaction: vi.fn(async () => ""),
signatureTimeoutError: null,
signatureTimeoutXdr: null,
clearSignatureTimeout: vi.fn(),
simulationResult: null,
setSimulationResult: vi.fn(),
gasWarning: null,
}),
}));

vi.mock("@/app/context/WalletContext", () => ({
useWallet: walletContextMock.useWallet,
}));

describe("SignatureTimeoutAlert", () => {
it("renders timeout details and logs a formatted stack trace", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
Expand Down Expand Up @@ -54,9 +88,13 @@ describe("SignatureTimeoutAlert", () => {

it("keeps the loader counter balanced when an external operation is active", async () => {
render(<WalletLoaderOverlay />);
startWalletOperation();
act(() => {
startWalletOperation();
});
expect(screen.getByTestId("wallet-loader-overlay")).toBeInTheDocument();
endWalletOperation();
act(() => {
endWalletOperation();
});
await waitFor(() => expect(screen.queryByTestId("wallet-loader-overlay")).not.toBeInTheDocument());
});
});
Loading