Skip to content
Open
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
78 changes: 78 additions & 0 deletions src/app/api/anchors/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("@/lib/auth", () => ({
getCurrentUser: vi.fn(),
}));

import { GET } from "../route";
import { getCurrentUser } from "@/lib/auth";

const SOME_USER = { id: "u1" } as never;

function makeRequest(query = "") {
return new Request(`http://localhost/api/anchors${query}`) as never;
}

beforeEach(() => {
vi.mocked(getCurrentUser).mockReset();
});

describe("GET /api/anchors", () => {
it("returns 401 when there is no authenticated user", async () => {
vi.mocked(getCurrentUser).mockResolvedValue(null);

const response = await GET(makeRequest());

expect(response.status).toBe(401);
});

it("returns every anchor with an estimatedFee computed against the default 1000 amount", async () => {
vi.mocked(getCurrentUser).mockResolvedValue(SOME_USER);

const response = await GET(makeRequest());
const body = await response.json();

expect(response.status).toBe(200);
expect(body.data.amount).toBe(1000);
expect(body.data.anchors).toHaveLength(4);
// Sorted ascending by feePercent - Tempo (0.35%) leads, and its fee against
// the default 1000 amount should be exactly 3.5.
expect(body.data.anchors[0].id).toBe("tempo");
expect(body.data.anchors[0].estimatedFee).toBe(3.5);
});

it("applies the corridor and assetCode query params and a custom amount", async () => {
vi.mocked(getCurrentUser).mockResolvedValue(SOME_USER);

const response = await GET(makeRequest("?corridor=Nigeria&amount=500"));
const body = await response.json();

expect(body.data.anchors).toHaveLength(1);
expect(body.data.anchors[0].id).toBe("cowrie");
expect(body.data.anchors[0].estimatedFee).toBe(3.75); // 500 * 0.75%
expect(body.data.amount).toBe(500);
});

it("returns an empty anchors array (still 200, not an error) when nothing matches", async () => {
vi.mocked(getCurrentUser).mockResolvedValue(SOME_USER);

const response = await GET(makeRequest("?assetCode=XYZ"));
const body = await response.json();

expect(response.status).toBe(200);
expect(body.data.anchors).toEqual([]);
});

it("falls back to the default amount of 1000 when the amount query param is missing or not a number", async () => {
vi.mocked(getCurrentUser).mockResolvedValue(SOME_USER);

const response = await GET(makeRequest("?amount=not-a-number"));
const body = await response.json();

// parseFloat("not-a-number") is NaN, and JSON has no NaN literal - it
// serializes to null over the wire. Documenting the current behavior
// (this query param has no validation) rather than asserting a crash.
expect(body.data.amount).toBeNull();
expect(body.data.anchors[0].estimatedFee).toBeNull();
});
});
82 changes: 82 additions & 0 deletions src/lib/__tests__/anchors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it, expect } from "vitest";
import { ANCHORS, listAnchors, estimateFee } from "../anchors";

describe("listAnchors", () => {
it("returns every anchor sorted ascending by feePercent when called with no filters", () => {
const result = listAnchors();
expect(result.map((a) => a.id)).toEqual(["tempo", "vibrant", "cowrie", "anclap"]);
expect(result).toHaveLength(ANCHORS.length);
});

it("treats corridor: 'All' the same as no filter", () => {
expect(listAnchors({ corridor: "All" }).map((a) => a.id)).toEqual(
listAnchors().map((a) => a.id)
);
});

it("filters by country via the corridor param, case-insensitively", () => {
expect(listAnchors({ corridor: "Nigeria" }).map((a) => a.id)).toEqual(["cowrie"]);
expect(listAnchors({ corridor: "nigeria" }).map((a) => a.id)).toEqual(["cowrie"]);
expect(listAnchors({ corridor: "NIGERIA" }).map((a) => a.id)).toEqual(["cowrie"]);
});

it("filters by exact assetCode", () => {
expect(listAnchors({ assetCode: "EURC" }).map((a) => a.id)).toEqual(["tempo"]);
expect(listAnchors({ assetCode: "USDC" }).map((a) => a.id)).toEqual(["vibrant", "cowrie", "anclap"]);
});

it("combines corridor and assetCode filters (AND, not OR)", () => {
// Nigeria's only anchor is USDC, so asking for Nigeria + EURC matches nothing.
expect(listAnchors({ corridor: "Nigeria", assetCode: "EURC" })).toEqual([]);
expect(listAnchors({ corridor: "Nigeria", assetCode: "USDC" }).map((a) => a.id)).toEqual(["cowrie"]);
});

it("returns an empty array (not an error) for a corridor/country with no matching anchor", () => {
expect(listAnchors({ corridor: "Atlantis" })).toEqual([]);
});

it("returns an empty array (not an error) for an unrecognized assetCode", () => {
expect(listAnchors({ assetCode: "XYZ" })).toEqual([]);
});

it("edge case: filtering by the literal `corridor` field value (not the country name) matches nothing", () => {
// listAnchors's `corridor` param is actually matched against `Anchor.country`
// (see the .filter() body), not against `Anchor.corridor` itself. The UI only
// ever sends the country name (src/app/(app)/anchors/page.tsx sets
// params.set("corridor", country)), so this never bites in practice - but
// passing the display string straight from the data (e.g. "NGN (Nigeria)")
// silently returns zero results instead of matching Cowrie, which is worth
// a regression test given how easy it'd be to wire a new caller up wrong.
expect(listAnchors({ corridor: "NGN (Nigeria)" })).toEqual([]);
});

it("does not mutate the underlying ANCHORS array or its order across repeated calls", () => {
const before = ANCHORS.map((a) => a.id);
listAnchors({ corridor: "Nigeria" });
listAnchors();
expect(ANCHORS.map((a) => a.id)).toEqual(before);
});
});

describe("estimateFee", () => {
it("computes the fee as amount * feePercent / 100, rounded to 2 decimals", () => {
const anchor = ANCHORS.find((a) => a.id === "vibrant")!; // 0.5%
expect(estimateFee(anchor, 1000)).toBe(5);
});

it("rounds to the nearest cent instead of leaving floating-point noise", () => {
const anchor = ANCHORS.find((a) => a.id === "anclap")!; // 1.2%
// 333.33 * 1.2 / 100 = 3.9999600000000003 unrounded
expect(estimateFee(anchor, 333.33)).toBe(4);
});

it("returns 0 for a zero amount", () => {
const anchor = ANCHORS.find((a) => a.id === "cowrie")!;
expect(estimateFee(anchor, 0)).toBe(0);
});

it("scales linearly with amount", () => {
const anchor = ANCHORS.find((a) => a.id === "tempo")!; // 0.35%
expect(estimateFee(anchor, 200)).toBeCloseTo(estimateFee(anchor, 100) * 2, 5);
});
});