TODO section 2, designed and approved, now built. A brand-new dataset arrives with DKK/USD/EUR rates instead of erroring on the first EUR purchase. build.ts fetches at build time and embeds the result via `define`. A network failure does NOT fail the build — it bakes committed constants instead, and an incomplete response is treated as a failure too, since one unconvertible currency is the problem this solves. Fetched rates carry source "api" and the BUILD date, which is why parseFxResponse now takes an explicit updatedAt; the constants carry source "manual" and their own date, so Settings never claims a hardcoded number came from a rate service. SKIP_FX_FETCH=1 and FX_API_URL are there for offline, CI and reproducible builds. They seed a NEW dataset and nothing else. They are NOT a fallback for a missing rate: clearing a rate removes its row so MissingRateError surfaces, rather than silently converting money at a number the user never chose. A test asserts a cleared rate still throws, and loading an existing dataset never touches its rates. Domain purity forced the shape. createSeedDataset takes the rates as an argument (and copies the rows, so shared constants cannot be edited out from under the caller); the env read lives in the new src/store/bakedRates.ts behind the try/catch accessor, because an unset BUN_PUBLIC_* leaves a bare process.env in the browser bundle; and createSnapshotStore takes seed rates as a parameter so it stays free of the ambient environment, with src/store/index.ts wiring it. Verified all three build paths: live fetch bakes api-sourced rates with today's date, an unreachable endpoint and SKIP_FX_FETCH both bake manual constants and still build, and the payload is present in the bundle in each case. Verified at runtime that a fresh install converts EUR 100 -> 747.50 DKK and USD 100 -> 644.95 DKK with the variable both set and unset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
import { test, expect, describe } from "bun:test";
|
|
import { buildFxUrl, parseFxResponse, DEFAULT_FX_API_URL } from "./fxApi.ts";
|
|
|
|
describe("buildFxUrl", () => {
|
|
test("substitutes the base and target placeholders", () => {
|
|
expect(buildFxUrl(DEFAULT_FX_API_URL, "DKK", ["USD", "EUR"])).toBe(
|
|
"https://api.frankfurter.app/latest?from=DKK&to=USD,EUR",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseFxResponse", () => {
|
|
test("inverts the rates into base units per one unit", () => {
|
|
// The API returns how many target units 1 base unit buys; we store the
|
|
// inverse, so 1 EUR = 7.46 DKK becomes baseUnitsPerOne 7.46.
|
|
const rates = parseFxResponse({ rates: { EUR: 0.134048, USD: 0.144928 } }, "DKK");
|
|
expect(rates.find((r) => r.currency === "EUR")!.baseUnitsPerOne).toBeCloseTo(7.46, 2);
|
|
expect(rates.every((r) => r.source === "api")).toBe(true);
|
|
expect(rates.every((r) => r.updatedAt !== "")).toBe(true);
|
|
});
|
|
|
|
test("skips the base currency's own row if the API returns one", () => {
|
|
const rates = parseFxResponse({ rates: { DKK: 1, EUR: 0.134 } }, "DKK");
|
|
expect(rates.map((r) => r.currency)).toEqual(["EUR"]);
|
|
});
|
|
|
|
test("ignores unsupported currencies", () => {
|
|
const rates = parseFxResponse({ rates: { GBP: 0.11, EUR: 0.134 } }, "DKK");
|
|
expect(rates.map((r) => r.currency)).toEqual(["EUR"]);
|
|
});
|
|
|
|
test("rejects a response with no rates object", () => {
|
|
expect(() => parseFxResponse({}, "DKK")).toThrow(/rates/);
|
|
expect(() => parseFxResponse("nope", "DKK")).toThrow(/rates/);
|
|
});
|
|
|
|
test("rejects a zero or negative rate rather than storing an infinity", () => {
|
|
expect(() => parseFxResponse({ rates: { EUR: 0 } }, "DKK")).toThrow(/EUR/);
|
|
});
|
|
});
|
|
|
|
test("an explicit updatedAt is used instead of the clock", () => {
|
|
// The build bakes rates in with the BUILD date, not the date the browser
|
|
// happens to run parseFxResponse.
|
|
const rates = parseFxResponse({ rates: { EUR: 0.13378, USD: 0.15505 } }, "DKK", "2026-09-01");
|
|
expect(rates.map((r) => r.updatedAt)).toEqual(["2026-09-01", "2026-09-01"]);
|
|
expect(rates.every((r) => r.source === "api")).toBe(true);
|
|
});
|