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
183 changes: 183 additions & 0 deletions frontend/src/lib/dashboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { describe, it, expect } from "vitest";
import {
mapBackendStreamToFrontend,
getDashboardAnalytics,
dashboardQueryKey,
type DashboardSnapshot,
} from "./dashboard";
import type { BackendStream } from "./api-types";

// ── dashboardQueryKey ───────────────────────────────────────────────────────

describe("dashboardQueryKey", () => {
it("returns tuple of 'dashboard' and publicKey", () => {
expect(dashboardQueryKey("GCXYZ")).toEqual(["dashboard", "GCXYZ"]);
});
});

// ── mapBackendStreamToFrontend ──────────────────────────────────────────────

function makeBackendStream(overrides: Partial<BackendStream> = {}): BackendStream {
return {
id: "1",
streamId: 42,
sender: "GAAAAAAA" + "A".repeat(50),
recipient: "GBBBBBBB" + "B".repeat(50),
tokenAddress: "CASCDUMMY" + "C".repeat(50),
ratePerSecond: "10000000", // 1 XLM/s in stroops
depositedAmount: "1000000000", // 100 XLM
withdrawnAmount: "500000000", // 50 XLM
startTime: 1700000000,
lastUpdateTime: 1700001000,
isActive: true,
isPaused: false,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-02T00:00:00Z",
...overrides,
};
}

describe("mapBackendStreamToFrontend", () => {
it("converts a basic active stream", () => {
const bs = makeBackendStream();
const result = mapBackendStreamToFrontend(bs, "GCOUNTERPARTY");

expect(result.id).toBe("42");
expect(result.recipient).toContain("...");
expect(result.deposited).toBeCloseTo(100);
expect(result.withdrawn).toBeCloseTo(50);
expect(result.ratePerSecond).toBeCloseTo(1);
expect(result.status).toBe("Active");
expect(result.isActive).toBe(true);
});

it("marks paused stream as Paused", () => {
const bs = makeBackendStream({ isPaused: true, isActive: false });
const result = mapBackendStreamToFrontend(bs, "GCOUNTERPARTY");
expect(result.status).toBe("Paused");
});

it("marks completed stream (inactive, no CANCELLED event)", () => {
const bs = makeBackendStream({ isActive: false, events: [] });
const result = mapBackendStreamToFrontend(bs, "GCOUNTERPARTY");
expect(result.status).toBe("Completed");
});

it("marks cancelled stream when CANCELLED event present", () => {
const bs = makeBackendStream({
isActive: false,
events: [{ id: "1", streamId: 42, eventType: "CANCELLED", amount: null, transactionHash: "tx1", ledgerSequence: 1, timestamp: 1, metadata: null, createdAt: "" }],
});
const result = mapBackendStreamToFrontend(bs, "GCOUNTERPARTY");
expect(result.status).toBe("Cancelled");
});

it("formats date as YYYY-MM-DD", () => {
const bs = makeBackendStream({ startTime: 1700000000 });
const result = mapBackendStreamToFrontend(bs, "GCOUNTERPARTY");
expect(result.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
});

// ── getDashboardAnalytics ──────────────────────────────────────────────────

describe("getDashboardAnalytics", () => {
it("returns unavailable text when snapshot is null", () => {
const metrics = getDashboardAnalytics(null);
expect(metrics).toHaveLength(4);
metrics.forEach((m) => {
expect(m.value).toBeNull();
expect(m.unavailableText).toBeTruthy();
});
});

it("computes metrics from a snapshot", () => {
const now = Date.now();
const snapshot: DashboardSnapshot = {
totalSent: 100,
totalReceived: 200,
totalValueLocked: 500,
activeStreamsCount: 2,
recentActivity: [
{ id: "1", title: "Out", description: "", amount: 10, direction: "sent", timestamp: new Date(now).toISOString() },
{ id: "2", title: "In", description: "", amount: 30, direction: "received", timestamp: new Date(now).toISOString() },
],
outgoingStreams: [
{ id: "1", recipient: "", amount: 100, token: "XLM", status: "Active", deposited: 100, withdrawn: 20, date: "", ratePerSecond: 1, lastUpdateTime: now / 1000, isActive: true },
],
incomingStreams: [
{ id: "2", recipient: "", amount: 50, token: "XLM", status: "Active", deposited: 50, withdrawn: 30, date: "", ratePerSecond: 1, lastUpdateTime: now / 1000, isActive: true },
],
};

const metrics = getDashboardAnalytics(snapshot);
expect(metrics).toHaveLength(4);

const volume30d = metrics.find((m) => m.id === "total-volume-30d")!;
expect(volume30d.value).toBe(40); // 10 + 30

const netFlow = metrics.find((m) => m.id === "net-flow-30d")!;
expect(netFlow.value).toBe(20); // 30 - 10

const avgValue = metrics.find((m) => m.id === "avg-value-per-stream")!;
expect(avgValue.value).toBe(250); // 500 / 2

const utilization = metrics.find((m) => m.id === "stream-utilization")!;
// totalWithdrawn = 20 + 30 = 50, totalDeposited = 100 + 50 = 150
expect(utilization.value).toBeCloseTo(50 / 150, 4);
});

it("returns null avg when no active streams", () => {
const snapshot: DashboardSnapshot = {
totalSent: 0,
totalReceived: 0,
totalValueLocked: 0,
activeStreamsCount: 0,
recentActivity: [],
outgoingStreams: [],
incomingStreams: [],
};

const metrics = getDashboardAnalytics(snapshot);
const avgValue = metrics.find((m) => m.id === "avg-value-per-stream")!;
expect(avgValue.value).toBeNull();
});

it("returns null utilization when nothing deposited", () => {
const snapshot: DashboardSnapshot = {
totalSent: 0,
totalReceived: 0,
totalValueLocked: 0,
activeStreamsCount: 0,
recentActivity: [],
outgoingStreams: [],
incomingStreams: [],
};

const metrics = getDashboardAnalytics(snapshot);
const util = metrics.find((m) => m.id === "stream-utilization")!;
expect(util.value).toBeNull();
});

it("filters out activity older than 30 days", () => {
const oldTime = new Date(Date.now() - 40 * 24 * 60 * 60 * 1000).toISOString();
const recentTime = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString();

const snapshot: DashboardSnapshot = {
totalSent: 0,
totalReceived: 0,
totalValueLocked: 0,
activeStreamsCount: 0,
recentActivity: [
{ id: "old", title: "", description: "", amount: 100, direction: "sent", timestamp: oldTime },
{ id: "new", title: "", description: "", amount: 50, direction: "received", timestamp: recentTime },
],
outgoingStreams: [],
incomingStreams: [],
};

const metrics = getDashboardAnalytics(snapshot);
const volume30d = metrics.find((m) => m.id === "total-volume-30d")!;
expect(volume30d.value).toBe(50); // only the recent activity
});
});
33 changes: 33 additions & 0 deletions frontend/src/lib/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { logger } from "./logger";

describe("logger", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("error always calls console.error", () => {
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
logger.error("test error", { detail: 1 });
expect(spy).toHaveBeenCalledWith("test error", { detail: 1 });
});

it("debug calls console.debug in dev", () => {
const spy = vi.spyOn(console, "debug").mockImplementation(() => {});
logger.debug("debug msg");
// In test env (NODE_ENV !== 'production'), debug should fire
expect(spy).toHaveBeenCalledWith("debug msg");
});

it("info calls console.info in dev", () => {
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
logger.info("info msg");
expect(spy).toHaveBeenCalledWith("info msg");
});

it("warn calls console.warn in dev", () => {
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
logger.warn("warn msg");
expect(spy).toHaveBeenCalledWith("warn msg");
});
});
159 changes: 159 additions & 0 deletions frontend/src/lib/wallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { describe, it, expect } from "vitest";
import {
toWalletErrorMessage,
shortenPublicKey,
formatNetwork,
isExpectedNetwork,
SUPPORTED_WALLETS,
FreighterNotInstalledError,
STELLAR_NETWORK,
STELLAR_NETWORK_ID,
} from "./wallet";

// ── STELLAR_NETWORK / STELLAR_NETWORK_ID ────────────────────────────────────

describe("STELLAR_NETWORK", () => {
it("defaults to TESTNET", () => {
expect(STELLAR_NETWORK).toBe("TESTNET");
});

it("STELLAR_NETWORK_ID matches TESTNET", () => {
expect(STELLAR_NETWORK_ID).toContain("Test SDF Network");
});
});

// ── SUPPORTED_WALLETS ──────────────────────────────────────────────────────

describe("SUPPORTED_WALLETS", () => {
it("contains freighter", () => {
expect(SUPPORTED_WALLETS.find((w) => w.id === "freighter")).toBeDefined();
});

it("each entry has required fields", () => {
for (const w of SUPPORTED_WALLETS) {
expect(w.id).toBeTruthy();
expect(w.name).toBeTruthy();
expect(w.badge).toBeTruthy();
expect(w.description).toBeTruthy();
}
});
});

// ── FreighterNotInstalledError ─────────────────────────────────────────────

describe("FreighterNotInstalledError", () => {
it("is an instance of Error", () => {
const err = new FreighterNotInstalledError();
expect(err).toBeInstanceOf(Error);
expect(err.name).toBe("FreighterNotInstalledError");
expect(err.message).toContain("Freighter");
});
});

// ── toWalletErrorMessage ───────────────────────────────────────────────────

describe("toWalletErrorMessage", () => {
it("returns specialized message for FreighterNotInstalledError", () => {
const err = new FreighterNotInstalledError();
expect(toWalletErrorMessage(err)).toBe(err.message);
});

it("returns original message for standard Error", () => {
const err = new Error("Something broke");
expect(toWalletErrorMessage(err)).toBe("Something broke");
});

it("maps rejection-like strings to friendly message", () => {
expect(toWalletErrorMessage("user denied")).toBe(
"You rejected the connection request. Try again when ready."
);
});

it("returns non-rejection strings unchanged", () => {
expect(toWalletErrorMessage("network timeout")).toBe("network timeout");
});

it("returns fallback for unknown types", () => {
expect(toWalletErrorMessage(null)).toBe("Wallet connection failed. Please try again.");
expect(toWalletErrorMessage(undefined)).toBe("Wallet connection failed. Please try again.");
expect(toWalletErrorMessage(42)).toBe("Wallet connection failed. Please try again.");
});

it("maps user-rejection patterns to friendly message", () => {
const rejectionMessages = [
"Request rejected",
"User declined",
"Access denied by user",
"Connection canceled",
"User cancelled the request",
"Popup closed by user",
"Window closed",
];

for (const msg of rejectionMessages) {
expect(toWalletErrorMessage(new Error(msg))).toBe(
"You rejected the connection request. Try again when ready."
);
}
});
});

// ── shortenPublicKey ───────────────────────────────────────────────────────

describe("shortenPublicKey", () => {
it("shortens a long key", () => {
const key = "GAAAAAAA" + "A".repeat(50);
const short = shortenPublicKey(key);
expect(short).toContain("...");
expect(short.startsWith(key.slice(0, 7))).toBe(true);
expect(short.endsWith(key.slice(-7))).toBe(true);
});

it("returns short keys unchanged", () => {
const short = "GCXYZ";
expect(shortenPublicKey(short)).toBe(short);
});
});

// ── formatNetwork ──────────────────────────────────────────────────────────

describe("formatNetwork", () => {
it("maps mainnet passphrase to 'Mainnet'", () => {
expect(formatNetwork("Public Global Stellar Network ; September 2015")).toBe("Mainnet");
});

it("maps 'mainnet' to 'Mainnet'", () => {
expect(formatNetwork("mainnet")).toBe("Mainnet");
});

it("maps testnet passphrase to 'Testnet'", () => {
expect(formatNetwork("Test SDF Network ; September 2015")).toBe("Testnet");
});

it("maps 'testnet' to 'Testnet'", () => {
expect(formatNetwork("testnet")).toBe("Testnet");
});

it("maps 'stellar testnet' to 'Testnet'", () => {
expect(formatNetwork("stellar testnet")).toBe("Testnet");
});

it("returns original for unknown networks", () => {
expect(formatNetwork("custom-network")).toBe("custom-network");
});
});

// ── isExpectedNetwork ──────────────────────────────────────────────────────

describe("isExpectedNetwork", () => {
it("returns true when session matches expected network", () => {
// Default env is TESTNET
expect(isExpectedNetwork("Test SDF Network ; September 2015")).toBe(true);
expect(isExpectedNetwork("testnet")).toBe(true);
});

it("returns false when session does not match expected network", () => {
expect(isExpectedNetwork("Public Global Stellar Network ; September 2015")).toBe(false);
expect(isExpectedNetwork("mainnet")).toBe(false);
});
});
Loading