From 21d31b672729c371de706a1b22aebbf687d4b074 Mon Sep 17 00:00:00 2001 From: Isaac Lewis Date: Sun, 25 Oct 2026 16:14:25 +1100 Subject: [PATCH 1/2] test: cover Stellar Horizon and wallet signing failures (#56) --- .../src/hooks/stellar-wallets-kit.test.ts | 16 +++- frontend/src/lib/stellar.test.ts | 79 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/stellar.test.ts diff --git a/frontend/src/hooks/stellar-wallets-kit.test.ts b/frontend/src/hooks/stellar-wallets-kit.test.ts index 709d2fd..dcaba0d 100644 --- a/frontend/src/hooks/stellar-wallets-kit.test.ts +++ b/frontend/src/hooks/stellar-wallets-kit.test.ts @@ -10,6 +10,7 @@ import { createLocalStorageMock, setupLocalStorageMock, } from "@/test/fixtures"; +import { ErrorMessages } from "@/lib/errors"; // --------------------------------------------------------------------------- // Mocks — declared before vi.mock() so they are available in the factory @@ -268,7 +269,20 @@ describe("signTransaction", () => { address: "G" + "A".repeat(55), networkPassphrase: "Test SDF Network ; September 2015", }), - ).rejects.toThrow(); + ).rejects.toThrow(ErrorMessages.WALLET_CONNECTION_FAILED); + }); + + it("normalizes unexpected signing errors to a clear wallet message", async () => { + localStorageMock.getItem.mockReturnValue("freighter"); + mockSignTransaction.mockRejectedValue("transport socket closed"); + + const { signTransaction } = await loadKit(); + await expect( + signTransaction("XDR", { + address: "G" + "A".repeat(55), + networkPassphrase: "Test SDF Network ; September 2015", + }), + ).rejects.toThrow(ErrorMessages.WALLET_CONNECTION_FAILED); }); it("throws when the kit is not available (server-side / no window)", async () => { diff --git a/frontend/src/lib/stellar.test.ts b/frontend/src/lib/stellar.test.ts new file mode 100644 index 0000000..3d324e8 --- /dev/null +++ b/frontend/src/lib/stellar.test.ts @@ -0,0 +1,79 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { ErrorCodes, ErrorMessages } from "@/lib/errors"; +import { getAccountDetails, getNativeBalance } from "@/lib/stellar"; + +const key = "G" + "A".repeat(55); + +function horizon(status: number, payload: unknown = {}) { + return { ok: status >= 200 && status < 300, status, json: vi.fn().mockResolvedValue(payload) } as unknown as Response; +} + +describe("Horizon transaction/account failure feedback", () => { + beforeEach(() => vi.stubGlobal("fetch", vi.fn())); + afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); }); + + it("reports a missing account with an actionable user-facing error", async () => { + vi.mocked(fetch).mockResolvedValue(horizon(404)); + await expect(getAccountDetails(key)).rejects.toMatchObject({ + code: ErrorCodes.STELLAR_ACCOUNT_NOT_FOUND, + message: ErrorMessages.STELLAR_ACCOUNT_NOT_FOUND, + }); + }); + + it.each([400, 429, 500, 503])("reports Horizon HTTP %i as a fetch failure", async (status) => { + vi.mocked(fetch).mockResolvedValue(horizon(status)); + await expect(getAccountDetails(key)).rejects.toMatchObject({ + code: ErrorCodes.STELLAR_FETCH_FAILED, + message: ErrorMessages.STELLAR_FETCH_FAILED, + }); + }); + + it("handles disconnected network errors without exposing transport details", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + vi.mocked(fetch).mockRejectedValue(new TypeError("network unavailable")); + await expect(getAccountDetails(key)).rejects.toMatchObject({ + code: ErrorCodes.STELLAR_FETCH_FAILED, + message: ErrorMessages.STELLAR_FETCH_FAILED, + }); + }); + + it("normalizes a malformed Horizon JSON response to a fetch error", async () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + const response = horizon(200); + vi.mocked(response.json).mockRejectedValue(new SyntaxError("invalid JSON")); + vi.mocked(fetch).mockResolvedValue(response); + + await expect(getAccountDetails(key)).rejects.toMatchObject({ + code: ErrorCodes.STELLAR_FETCH_FAILED, + message: ErrorMessages.STELLAR_FETCH_FAILED, + }); + }); + + it("preserves a missing-account error when fetching native balance", async () => { + vi.mocked(fetch).mockResolvedValue(horizon(404)); + await expect(getNativeBalance(key)).rejects.toMatchObject({ + code: ErrorCodes.STELLAR_ACCOUNT_NOT_FOUND, + message: ErrorMessages.STELLAR_ACCOUNT_NOT_FOUND, + }); + }); + + it("reports absent native balance even when other assets exist", async () => { + vi.mocked(fetch).mockResolvedValue(horizon(200, { + id: key, balances: [{ asset_type: "credit_alphanum4", balance: "10", asset_code: "USDC" }], + })); + await expect(getNativeBalance(key)).rejects.toMatchObject({ + code: ErrorCodes.STELLAR_BALANCE_NOT_FOUND, + message: ErrorMessages.STELLAR_BALANCE_NOT_FOUND, + }); + }); + + it("returns native balance rather than a non-native token balance", async () => { + vi.mocked(fetch).mockResolvedValue(horizon(200, { + id: key, balances: [ + { asset_type: "credit_alphanum4", balance: "100", asset_code: "USDC" }, + { asset_type: "native", balance: "3.5" }, + ], + })); + await expect(getNativeBalance(key)).resolves.toBe("3.5"); + }); +}); From bd8706bcdb29e4b96123a2fed6efe4a6bbf9e197 Mon Sep 17 00:00:00 2001 From: Isaac Lewis Date: Sun, 25 Oct 2026 16:23:25 +1100 Subject: [PATCH 2/2] fix: preserve Stellar account errors and normalize bad responses (#56) --- frontend/src/lib/stellar.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/stellar.ts b/frontend/src/lib/stellar.ts index 5f8dbe7..eb3e715 100644 --- a/frontend/src/lib/stellar.ts +++ b/frontend/src/lib/stellar.ts @@ -35,9 +35,15 @@ export async function getAccountDetails( } throw createError(ErrorCodes.STELLAR_FETCH_FAILED); } - return response.json(); + return await response.json(); } catch (error) { - if (error instanceof Error && "code" in error) { + if ( + typeof error === "object" && + error !== null && + "code" in error && + (error.code === ErrorCodes.STELLAR_ACCOUNT_NOT_FOUND || + error.code === ErrorCodes.STELLAR_FETCH_FAILED) + ) { throw error as StandardError; } console.error("Error fetching account details:", error);