diff options
| author | Adam Soltys <[email protected]> | 2026-03-15 21:53:52 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-03-15 21:53:52 -0700 |
| commit | ddfe2e3279706f6476364cecfe23427dcabcfd3e (patch) | |
| tree | f37a2ea3d8e904214c7d8b695c3d87dca7c56466 | |
| parent | fddfe10f84908c058e239b93985dfe02ba37df04 (diff) | |
| download | coinos-server-ddfe2e3279706f6476364cecfe23427dcabcfd3e.tar.gz coinos-server-ddfe2e3279706f6476364cecfe23427dcabcfd3e.zip | |
tests
| -rw-r--r-- | .github/workflows/test.yml | 15 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | test/api.test.ts | 126 | ||||
| -rw-r--r-- | test/forward.test.ts | 223 | ||||
| -rw-r--r-- | test/integration/ark-reconcile.test.ts | 9 | ||||
| -rw-r--r-- | test/onchain.test.ts | 86 | ||||
| -rw-r--r-- | test/preload.ts | 93 |
7 files changed, 393 insertions, 160 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..f987ef0f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: Test + +on: + push: + branches: [master, staging] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun test test/api.test.ts test/forward.test.ts test/onchain.test.ts diff --git a/package.json b/package.json index b663fca2..3e0acea2 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "start": "bun index.ts", "dev": "nodemon --exec bun index.ts", + "test": "bun test", "lint": "oxlint . && oxfmt --check .", "lint:fix": "oxlint --fix . && oxfmt .", "format": "oxfmt ." diff --git a/test/api.test.ts b/test/api.test.ts new file mode 100644 index 00000000..92e506f3 --- /dev/null +++ b/test/api.test.ts @@ -0,0 +1,126 @@ +import { describe, test, expect, beforeAll } from "bun:test"; +import app from "$lib/app"; +import { auth, optional } from "$lib/auth"; +import users from "$routes/users"; +import info from "$routes/info"; +import invoices from "$routes/invoices"; + +// Register routes (subset of index.ts — no side effects, no Bun.serve) +app.get("/health", info.health); +app.post("/register", users.create); +app.post("/login", users.login); +app.get("/me", auth, users.me); +app.get("/users/:key", users.get); +app.post("/invoice", optional, invoices.create); +app.get("/invoice/:id", invoices.get); + +const post = (path: string, body: any) => + app.request(path, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + +// Use unique username to avoid collisions with other test files +const USERNAME = `apiuser${Date.now()}`; +const PASSWORD = "testpass123"; + +beforeAll(() => { + // Clear any leftover state from other test files + const store = globalThis.__testStore; + for (const k of Object.keys(store.kvStore)) delete store.kvStore[k]; + for (const k of Object.keys(store.listStore)) delete store.listStore[k]; + for (const k of Object.keys(store.setStore)) delete store.setStore[k]; +}); + +describe("Health", () => { + test("GET /health returns 200", async () => { + const res = await app.request("/health"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.healthy).toBe(true); + }); +}); + +describe("Registration", () => { + test("creates user with valid credentials", async () => { + const res = await post("/register", { + user: { username: USERNAME, password: PASSWORD }, + }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.token).toBeDefined(); + expect(body.username).toBe(USERNAME); + }); + + test("rejects duplicate username", async () => { + const res = await post("/register", { + user: { username: USERNAME, password: PASSWORD }, + }); + expect(res.status).toBe(500); + }); + + test("rejects missing user object", async () => { + const res = await post("/register", { password: PASSWORD }); + expect(res.status).toBe(500); + }); + + test("rejects invalid username", async () => { + const res = await post("/register", { + user: { username: "ab!@#", password: PASSWORD }, + }); + expect(res.status).toBe(500); + }); +}); + +describe("Login", () => { + test("succeeds with valid credentials", async () => { + const res = await post("/login", { + username: USERNAME, + password: PASSWORD, + }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.token).toBeDefined(); + expect(body.user).toBeDefined(); + }); + + test("fails with wrong password", async () => { + const res = await post("/login", { + username: USERNAME, + password: "wrongpass", + }); + expect(res.status).toBe(401); + }); + + test("fails with nonexistent user", async () => { + const res = await post("/login", { + username: "noone", + password: PASSWORD, + }); + expect(res.status).toBe(401); + }); +}); + +describe("Authenticated endpoints", () => { + test("GET /me returns user data", async () => { + const res = await app.request("/me"); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.username).toBeDefined(); + }); +}); + +describe("User lookup", () => { + test("GET /users/:key finds registered user", async () => { + const res = await app.request(`/users/${USERNAME}`); + expect(res.status).toBe(200); + }); +}); + +describe("Routing", () => { + test("unknown route returns 404", async () => { + const res = await app.request("/nonexistent"); + expect(res.status).toBe(404); + }); +}); diff --git a/test/forward.test.ts b/test/forward.test.ts index 4c538153..7354d17c 100644 --- a/test/forward.test.ts +++ b/test/forward.test.ts @@ -109,19 +109,30 @@ const resetAll = () => { seedUser(makeUser()); }; -const makeRes = () => { +const makeContext = (opts: { body?: any; user?: any } = {}) => { let sent: any; - return { - send: (data: any) => { + const ctx: any = { + req: { + json: async () => opts.body, + header: () => undefined, + path: "/test", + method: "POST", + query: () => ({}), + raw: { clone: () => ({ json: async () => opts.body }) }, + }, + get: (key: string) => { + if (key === "user") return opts.user; + return undefined; + }, + set: () => {}, + json: (data: any, _status?: number) => { sent = data; + return new Response(JSON.stringify(data), { status: _status || 200 }); }, - code: (_code: number) => ({ - send: (data: any) => { - sent = data; - }, - }), + env: {}, getSent: () => sent, }; + return ctx; }; // ===================================================================== @@ -234,14 +245,13 @@ describe("arkReceive", () => { const inv = makeInvoice({ aid: arkAccountId, forward: bolt11 }); seedInvoice(inv); - const req = { + const ctx = makeContext({ body: { iid: inv.id, amount: 1000, hash: "ark-txid-999" }, user: makeUser(), - }; - const res = makeRes(); + }); - await routes.arkReceive(req as any, res as any); - const sent = (res as any).getSent(); + await routes.arkReceive(ctx); + const sent = ctx.getSent(); // Response is the custodial credit record expect(sent).toBeTruthy(); @@ -265,14 +275,13 @@ describe("arkReceive", () => { const inv = makeInvoice({ aid: arkAccountId }); seedInvoice(inv); - const req = { + const ctx = makeContext({ body: { iid: inv.id, amount: 1000, hash: "ark-txid-888" }, user: makeUser(), - }; - const res = makeRes(); + }); - await routes.arkReceive(req as any, res as any); - const sent = (res as any).getSent(); + await routes.arkReceive(ctx); + const sent = ctx.getSent(); expect(sent).toBeTruthy(); expect(sent.amount).toBe(1000); @@ -292,14 +301,13 @@ describe("arkReceive", () => { throw new Error("no route"); }); - const req = { + const ctx = makeContext({ body: { iid: inv.id, amount: 1000, hash: "ark-txid-777" }, user: makeUser(), - }; - const res = makeRes(); + }); - await routes.arkReceive(req as any, res as any); - const sent = (res as any).getSent(); + await routes.arkReceive(ctx); + const sent = ctx.getSent(); // Credit still lands in custodial even though forward failed expect(sent).toBeTruthy(); @@ -314,15 +322,14 @@ describe("arkReceive", () => { const inv = makeInvoice(); seedInvoice(inv); - const req = { + const ctx = makeContext({ body: { iid: inv.id, amount: 0, hash: "ark-txid-000" }, user: makeUser(), - }; - const res = makeRes(); + }); - await routes.arkReceive(req as any, res as any); + await routes.arkReceive(ctx); // Should error - check that send wasn't called with a valid payment - const sent = (res as any).getSent(); + const sent = ctx.getSent(); expect(sent).toBeDefined(); }); @@ -330,14 +337,13 @@ describe("arkReceive", () => { const inv = makeInvoice({ uid: "other-user-id" }); seedInvoice(inv); - const req = { + const ctx = makeContext({ body: { iid: inv.id, amount: 1000, hash: "ark-txid-666" }, user: makeUser(), - }; - const res = makeRes(); + }); - await routes.arkReceive(req as any, res as any); - const sent = (res as any).getSent(); + await routes.arkReceive(ctx); + const sent = ctx.getSent(); expect(sent).toBeDefined(); }); }); @@ -350,7 +356,7 @@ describe("arkSync", () => { beforeEach(resetAll); test("records received VTXOs (positive amounts)", async () => { - const req = { + const ctx = makeContext({ body: { aid: arkAccountId, transactions: [ @@ -358,19 +364,17 @@ describe("arkSync", () => { ], }, user: makeUser(), - }; - const res = makeRes(); - - await routes.arkSync(req as any, res as any); - const sent = (res as any).getSent(); + }); + await routes.arkSync(ctx); + const sent = ctx.getSent(); expect(sent.synced).toBe(1); expect(sent.received).toBe(9000); expect(store().listStore[`${arkAccountId}:payments`] ?? []).toHaveLength(1); }); - test("records sent transactions (negative amounts)", async () => { - const req = { + test("skips sent transactions (negative amounts)", async () => { + const ctx = makeContext({ body: { aid: arkAccountId, transactions: [ @@ -378,14 +382,12 @@ describe("arkSync", () => { ], }, user: makeUser(), - }; - const res = makeRes(); - - await routes.arkSync(req as any, res as any); - const sent = (res as any).getSent(); + }); + await routes.arkSync(ctx); + const sent = ctx.getSent(); - expect(sent.synced).toBe(1); - expect((store().listStore[`${arkAccountId}:payments`] ?? []).length).toBe(1); + // Negative amounts are skipped — balance reconciliation handles losses + expect(sent.synced).toBe(0); }); test("deduplicates known transactions", async () => { @@ -397,17 +399,15 @@ describe("arkSync", () => { confirmed: false, }); - const req = { + const ctx = makeContext({ body: { aid: arkAccountId, transactions: [{ hash: "known-hash", amount: -1000, settled: true, createdAt: Date.now() }], }, user: makeUser(), - }; - const res = makeRes(); - - await routes.arkSync(req as any, res as any); - const sent = (res as any).getSent(); + }); + await routes.arkSync(ctx); + const sent = ctx.getSent(); expect(sent.synced).toBe(0); // Existing record should be updated to confirmed @@ -425,6 +425,8 @@ describe("arkSync reconciliation", () => { test("creates expired debit when payment sum exceeds wallet balance", async () => { // Pre-seed two received payments totaling 7128 + // Timestamps must be >120s old to pass hasRecentPayments guard + const oldTime = Date.now() - 200_000; const pay1 = { id: "pay-1", aid: arkAccountId, @@ -435,7 +437,7 @@ describe("arkSync reconciliation", () => { uid: custodialUid, rate: 50000, currency: "USD", - created: Date.now() - 2000, + created: oldTime, }; const pay2 = { id: "pay-2", @@ -447,7 +449,7 @@ describe("arkSync reconciliation", () => { uid: custodialUid, rate: 50000, currency: "USD", - created: Date.now() - 1000, + created: oldTime, }; store().kvStore[`payment:pay-1`] = JSON.stringify(pay1); store().kvStore[`payment:pay-2`] = JSON.stringify(pay2); @@ -456,18 +458,16 @@ describe("arkSync reconciliation", () => { store().listStore[`${arkAccountId}:payments`] = ["pay-2", "pay-1"]; // Sync with no new transactions but balance=0 (VTXOs expired) - const req = { + const ctx = makeContext({ body: { aid: arkAccountId, transactions: [], balance: 0, }, user: makeUser(), - }; - const res = makeRes(); - - await routes.arkSync(req as any, res as any); - const sent = (res as any).getSent(); + }); + await routes.arkSync(ctx); + const sent = ctx.getSent(); expect(sent.synced).toBe(1); expect(sent.payments).toHaveLength(1); @@ -499,18 +499,16 @@ describe("arkSync reconciliation", () => { store().kvStore[`payment:${arkAccountId}:h1`] = JSON.stringify("pay-1"); store().listStore[`${arkAccountId}:payments`] = ["pay-1"]; - const req = { + const ctx = makeContext({ body: { aid: arkAccountId, transactions: [], balance: 5000, }, user: makeUser(), - }; - const res = makeRes(); - - await routes.arkSync(req as any, res as any); - const sent = (res as any).getSent(); + }); + await routes.arkSync(ctx); + const sent = ctx.getSent(); expect(sent.synced).toBe(0); expect(sent.payments).toHaveLength(0); @@ -534,17 +532,15 @@ describe("arkSync reconciliation", () => { store().kvStore[`payment:${arkAccountId}:h1`] = JSON.stringify("pay-1"); store().listStore[`${arkAccountId}:payments`] = ["pay-1"]; - const req = { + const ctx = makeContext({ body: { aid: arkAccountId, transactions: [], }, user: makeUser(), - }; - const res = makeRes(); - - await routes.arkSync(req as any, res as any); - const sent = (res as any).getSent(); + }); + await routes.arkSync(ctx); + const sent = ctx.getSent(); expect(sent.synced).toBe(0); expect(sent.payments).toHaveLength(0); @@ -552,6 +548,7 @@ describe("arkSync reconciliation", () => { test("is idempotent — second sync does not create duplicate", async () => { // Seed payment sum = 5000, balance = 0 + const oldTime = Date.now() - 200_000; const pay1 = { id: "pay-1", aid: arkAccountId, @@ -562,34 +559,37 @@ describe("arkSync reconciliation", () => { uid: custodialUid, rate: 50000, currency: "USD", - created: Date.now(), + created: oldTime, }; store().kvStore[`payment:pay-1`] = JSON.stringify(pay1); store().kvStore[`payment:${arkAccountId}:h1`] = JSON.stringify("pay-1"); store().listStore[`${arkAccountId}:payments`] = ["pay-1"]; - const req = { + const ctx1 = makeContext({ body: { aid: arkAccountId, transactions: [], balance: 0 }, user: makeUser(), - }; + }); // First sync — should create reconciliation - const res1 = makeRes(); - await routes.arkSync(req as any, res1 as any); - const sent1 = (res1 as any).getSent(); + await routes.arkSync(ctx1); + const sent1 = ctx1.getSent(); expect(sent1.synced).toBe(1); expect(sent1.payments[0].amount).toBe(-5000); // Second sync — sum is now 0, matches balance, no new reconciliation - const res2 = makeRes(); - await routes.arkSync(req as any, res2 as any); - const sent2 = (res2 as any).getSent(); + const ctx2 = makeContext({ + body: { aid: arkAccountId, transactions: [], balance: 0 }, + user: makeUser(), + }); + await routes.arkSync(ctx2); + const sent2 = ctx2.getSent(); expect(sent2.synced).toBe(0); expect(sent2.payments).toHaveLength(0); }); test("reconciles partial expiry", async () => { // 10000 received, 3000 sent, sum = 7000. Actual balance = 2000 (5000 expired) + const oldTime = Date.now() - 200_000; const pay1 = { id: "pay-1", aid: arkAccountId, @@ -600,7 +600,7 @@ describe("arkSync reconciliation", () => { uid: custodialUid, rate: 50000, currency: "USD", - created: Date.now(), + created: oldTime, }; const pay2 = { id: "pay-2", @@ -612,7 +612,7 @@ describe("arkSync reconciliation", () => { uid: custodialUid, rate: 50000, currency: "USD", - created: Date.now(), + created: oldTime, }; store().kvStore[`payment:pay-1`] = JSON.stringify(pay1); store().kvStore[`payment:pay-2`] = JSON.stringify(pay2); @@ -620,14 +620,12 @@ describe("arkSync reconciliation", () => { store().kvStore[`payment:${arkAccountId}:h2`] = JSON.stringify("pay-2"); store().listStore[`${arkAccountId}:payments`] = ["pay-2", "pay-1"]; - const req = { + const ctx = makeContext({ body: { aid: arkAccountId, transactions: [], balance: 2000 }, user: makeUser(), - }; - const res = makeRes(); - - await routes.arkSync(req as any, res as any); - const sent = (res as any).getSent(); + }); + await routes.arkSync(ctx); + const sent = ctx.getSent(); expect(sent.synced).toBe(1); expect(sent.payments[0].amount).toBe(-5000); @@ -652,14 +650,12 @@ describe("arkSync reconciliation", () => { store().kvStore[`payment:${arkAccountId}:h1`] = JSON.stringify("pay-1"); store().listStore[`${arkAccountId}:payments`] = ["pay-1"]; - const req = { + const ctx = makeContext({ body: { aid: arkAccountId, transactions: [], balance: 5000 }, user: makeUser(), - }; - const res = makeRes(); - - await routes.arkSync(req as any, res as any); - const sent = (res as any).getSent(); + }); + await routes.arkSync(ctx); + const sent = ctx.getSent(); expect(sent.synced).toBe(0); expect(sent.payments).toHaveLength(0); @@ -687,13 +683,17 @@ describe("BOLT12 autowithdraw", () => { : { type: "bolt12 invoice", invoice_amount_msat: 980_000, invoice_node_id: "bolt12-payee" }, ); - const user = makeUser({ + const user = makeUser(); + seedUser(user); + + const awAccount = { + id: "aw-account", autowithdraw: true, threshold: 500, reserve: 0, destination: bolt12Offer, - }); - seedUser(user); + }; + store().kvStore[`account:${awAccount.id}`] = JSON.stringify(awAccount); const inv = makeInvoice({ type: "lightning", amount: 1000 }); const p = { @@ -701,6 +701,7 @@ describe("BOLT12 autowithdraw", () => { confirmed: true, type: "lightning", uid: custodialUid, + aid: awAccount.id, created: Date.now(), }; @@ -752,13 +753,17 @@ describe("BOLT12 autowithdraw", () => { }); test("regular ln invoice is NOT passed through fetchinvoice", async () => { - const user = makeUser({ + const user = makeUser(); + seedUser(user); + + const awAccount = { + id: "aw-account", autowithdraw: true, threshold: 500, reserve: 0, destination: bolt11, - }); - seedUser(user); + }; + store().kvStore[`account:${awAccount.id}`] = JSON.stringify(awAccount); const inv = makeInvoice({ type: "lightning", amount: 1000 }); const p = { @@ -766,6 +771,7 @@ describe("BOLT12 autowithdraw", () => { confirmed: true, type: "lightning", uid: custodialUid, + aid: awAccount.id, created: Date.now(), }; @@ -800,13 +806,17 @@ describe("BOLT12 autowithdraw", () => { invoice: bolt12Invoice, })); - const user = makeUser({ + const user = makeUser(); + seedUser(user); + + const awAccount = { + id: "aw-account", autowithdraw: true, threshold: 500, reserve: 0, destination: bolt12Offer, - }); - seedUser(user); + }; + store().kvStore[`account:${awAccount.id}`] = JSON.stringify(awAccount); const inv = makeInvoice({ type: "lightning", amount: 1000 }); const p = { @@ -814,6 +824,7 @@ describe("BOLT12 autowithdraw", () => { confirmed: true, type: "lightning", uid: custodialUid, + aid: awAccount.id, created: Date.now(), }; diff --git a/test/integration/ark-reconcile.test.ts b/test/integration/ark-reconcile.test.ts index 342d02a6..c223149d 100644 --- a/test/integration/ark-reconcile.test.ts +++ b/test/integration/ark-reconcile.test.ts @@ -83,6 +83,7 @@ beforeAll(async () => { describe("ark sync reconciliation", () => { test("syncs new transactions and creates payment records", async () => { const hash = `test-recv-${Date.now()}`; + const oldTime = Date.now() - 200_000; const result = await api("/ark/sync", token, { method: "POST", body: JSON.stringify({ @@ -93,7 +94,7 @@ describe("ark sync reconciliation", () => { commitmentTxid: null, amount: 10000, settled: true, - createdAt: Date.now(), + createdAt: oldTime, }, ], balance: 10000, @@ -115,7 +116,7 @@ describe("ark sync reconciliation", () => { method: "POST", body: JSON.stringify({ aid: arkAccountId, - transactions: [{ arkTxid: hash, amount: 3000, settled: true, createdAt: Date.now() }], + transactions: [{ arkTxid: hash, amount: 3000, settled: true, createdAt: Date.now() - 200_000 }], balance: 13000, }), }); @@ -125,7 +126,7 @@ describe("ark sync reconciliation", () => { method: "POST", body: JSON.stringify({ aid: arkAccountId, - transactions: [{ arkTxid: hash, amount: 3000, settled: true, createdAt: Date.now() }], + transactions: [{ arkTxid: hash, amount: 3000, settled: true, createdAt: Date.now() - 200_000 }], balance: 13000, }), }); @@ -177,7 +178,7 @@ describe("ark sync reconciliation", () => { method: "POST", body: JSON.stringify({ aid: arkAccountId, - transactions: [{ arkTxid: hash, amount: 5000, settled: true, createdAt: Date.now() }], + transactions: [{ arkTxid: hash, amount: 5000, settled: true, createdAt: Date.now() - 200_000 }], balance: 5000, }), }); diff --git a/test/onchain.test.ts b/test/onchain.test.ts index 9850bba3..884bcbc9 100644 --- a/test/onchain.test.ts +++ b/test/onchain.test.ts @@ -98,19 +98,30 @@ const resetAll = () => { seedUser(makeUser()); }; -const makeRes = () => { +const makeContext = (opts: { body?: any; user?: any } = {}) => { let sent: any; - return { - send: (data: any) => { + const ctx: any = { + req: { + json: async () => opts.body, + header: () => undefined, + path: "/test", + method: "POST", + query: () => ({}), + raw: { clone: () => ({ json: async () => opts.body }) }, + }, + get: (key: string) => { + if (key === "user") return opts.user; + return undefined; + }, + set: () => {}, + json: (data: any, _status?: number) => { sent = data; + return new Response(JSON.stringify(data), { status: _status || 200 }); }, - code: (_code: number) => ({ - send: (data: any) => { - sent = data; - }, - }), + env: {}, getSent: () => sent, }; + return ctx; }; // ===================================================================== @@ -325,13 +336,12 @@ describe("confirm (liquid)", () => { beforeEach(resetAll); test("ignores non-liquid types", async () => { - const req = { + const ctx = makeContext({ body: { txid: "btctx100", wallet: "test", type: PaymentType.bitcoin }, - }; - const res = makeRes(); + }); - await routes.confirm(req as any, res as any); - expect((res as any).getSent()).toEqual({}); + await routes.confirm(ctx); + expect(ctx.getSent()).toEqual({}); expect(mockTbCredit).not.toHaveBeenCalled(); }); @@ -354,13 +364,11 @@ describe("confirm (liquid)", () => { }), }; - const req = { + const ctx = makeContext({ body: { txid: "lqtx001", wallet: "test", type: PaymentType.liquid }, - }; - const res = makeRes(); - - await routes.confirm(req as any, res as any); - expect((res as any).getSent()).toEqual({}); + }); + await routes.confirm(ctx); + expect(ctx.getSent()).toEqual({}); // Payment should be created via credit() const pid = JSON.parse(store().kvStore[`payment:lqtx001:0`]); @@ -401,12 +409,10 @@ describe("confirm (liquid)", () => { }), }; - const req = { + const ctx = makeContext({ body: { txid: "lqtx002", wallet: "test", type: PaymentType.liquid }, - }; - const res = makeRes(); - - await routes.confirm(req as any, res as any); + }); + await routes.confirm(ctx); // Payment confirmed const p = JSON.parse(store().kvStore[`payment:${pendingPayment.id}`]); @@ -437,12 +443,10 @@ describe("confirm (liquid)", () => { }), }; - const req = { + const ctx = makeContext({ body: { txid: "lqtx003", wallet: "test", type: PaymentType.liquid }, - }; - const res = makeRes(); - - await routes.confirm(req as any, res as any); + }); + await routes.confirm(ctx); expect(mockTbCredit).not.toHaveBeenCalled(); expect(mockTbConfirm).not.toHaveBeenCalled(); }); @@ -463,12 +467,10 @@ describe("confirm (liquid)", () => { }), }; - const req = { + const ctx = makeContext({ body: { txid: "lqtx004", wallet: "test", type: PaymentType.liquid }, - }; - const res = makeRes(); - - await routes.confirm(req as any, res as any); + }); + await routes.confirm(ctx); expect(mockTbCredit).not.toHaveBeenCalled(); }); @@ -494,12 +496,10 @@ describe("confirm (liquid)", () => { }), }; - const req = { + const ctx = makeContext({ body: { txid: "lqtx005", wallet: "test", type: PaymentType.liquid }, - }; - const res = makeRes(); - - await routes.confirm(req as any, res as any); + }); + await routes.confirm(ctx); expect(mockTbCredit).not.toHaveBeenCalled(); }); @@ -533,12 +533,10 @@ describe("confirm (liquid)", () => { }), }; - const req = { + const ctx = makeContext({ body: { txid: "lqtx006", wallet: "test", type: PaymentType.liquid }, - }; - const res = makeRes(); - - await routes.confirm(req as any, res as any); + }); + await routes.confirm(ctx); expect(mockTbConfirm).not.toHaveBeenCalled(); }); }); diff --git a/test/preload.ts b/test/preload.ts index 01629d97..690512b2 100644 --- a/test/preload.ts +++ b/test/preload.ts @@ -1,3 +1,5 @@ +process.env.URL ||= "http://localhost:3119"; + import { mock } from "bun:test"; // This file is loaded before test files via bunfig.toml [test].preload @@ -142,6 +144,17 @@ if (process.env.INTEGRATION) { kv()[k] = v; return true; }, + incr: async (k: string) => { + const v = Number.parseInt(kv()[k] || "0") + 1; + kv()[k] = String(v); + return v; + }, + ttl: async (k: string) => (kv()[k] !== undefined ? 300 : -2), + decrBy: async (k: string, n: number) => { + const v = Number.parseInt(kv()[k] || "0") - n; + kv()[k] = String(v); + return v; + }, zScore: async () => null, zAdd: async () => 1, zCard: async () => 0, @@ -162,6 +175,8 @@ if (process.env.INTEGRATION) { s, gf, ga: async () => null, + gfAll: async () => ({}), + scan: async function* (_pattern: string) {}, archive: { lRange: async () => [] }, }; }); @@ -191,6 +206,13 @@ if (process.env.INTEGRATION) { nostr: "ws://localhost:7777", tigerbeetle: { cluster_id: 0n, replica_addresses: ["localhost:3000"] }, vapid: { pk: "test", sk: "test" }, + jwt: "test-jwt-secret", + publicRelay: "ws://localhost:7777", + relays: ["ws://localhost:7777"], + hostname: "localhost", + admin: "admin", + recaptcha: "", + ipregistry: undefined, support: "[email protected]", txWebhookSecret: "test", }, @@ -208,9 +230,18 @@ if (process.env.INTEGRATION) { tbSetPending: mock(async () => undefined), fundDebit: mock(async () => ({ err: null })), initTigerBeetle: mock(async () => {}), + createBalanceAccount: mock(async () => {}), + createCreditAccounts: mock(async () => {}), + getPending: mock(async () => 0), + getFundBalance: mock(async () => 0), + tbFundCredit: mock(async () => {}), + tbFundDebit: mock(async () => ({ err: null })), })); mock.module("$lib/ln", () => ({ + LightningUnavailableError: class LightningUnavailableError extends Error { + constructor(msg: string) { super(msg); this.name = "LightningUnavailableError"; } + }, default: { decode: mock(async () => ({ type: "bolt11", amount_msat: 1_000_000, payee: "test-payee" })), listpeerchannels: mock(async () => ({ channels: [] })), @@ -226,6 +257,13 @@ if (process.env.INTEGRATION) { }, })); + mock.module("pino", () => { + const noop = () => {}; + const logger = { info: noop, warn: noop, error: noop, debug: noop, trace: noop, fatal: noop }; + const pino: any = () => logger; + pino.destination = () => ({}); + return { default: pino }; + }); mock.module("$lib/logging", () => ({ l: () => {}, warn: mock(() => {}), err: () => {} })); mock.module("$lib/notifications", () => ({ notify: () => {}, nwcNotify: () => {} })); mock.module("$lib/webhooks", () => ({ callWebhook: mock(() => {}) })); @@ -256,6 +294,8 @@ if (process.env.INTEGRATION) { mock.module("$lib/nostr", () => ({ handleZap: async () => {}, publish: async () => {}, + getNostrUser: async () => null, + getProfile: async () => null, serverPubkey: "m", serverPubkey2: "m", serverSecret: "m", @@ -268,6 +308,10 @@ if (process.env.INTEGRATION) { getArkBalance: async () => 0, verifyArkVtxo: async () => true, })); + mock.module("$lib/bip353", () => ({ + setupBip353: async () => {}, + teardownBip353: async () => {}, + })); mock.module("$lib/ecash", () => ({ request: async () => ({}) })); mock.module("$lib/lightning", () => ({ replay: async () => ({}), @@ -277,9 +321,37 @@ if (process.env.INTEGRATION) { mock.module("$lib/mail", () => ({ mail: async () => {}, templates: {} })); mock.module("$lib/auth", () => ({ requirePin: async () => {}, - auth: (_r: any, _s: any, n: any) => n(), - optional: (_r: any, _s: any, n: any) => n(), - admin: (_r: any, _s: any, n: any) => n(), + auth: async (c: any, next: any) => { + c.set("user", { + id: "00000000-0000-0000-0000-000000000001", + username: "authuser", + currency: "USD", + currencies: ["USD", "CAD"], + fiat: false, + }); + await next(); + }, + optional: async (c: any, next: any) => { + c.set("user", { + id: "00000000-0000-0000-0000-000000000001", + username: "authuser", + currency: "USD", + currencies: ["USD", "CAD"], + fiat: false, + }); + await next(); + }, + admin: async (c: any, next: any) => { + c.set("user", { + id: "00000000-0000-0000-0000-000000000001", + username: "authuser", + admin: true, + currency: "USD", + currencies: ["USD", "CAD"], + fiat: false, + }); + await next(); + }, })); mock.module("$lib/invoices", () => ({ generate: mock(async ({ invoice, user }: any) => ({ @@ -358,15 +430,24 @@ if (process.env.INTEGRATION) { } }, getUser: async (username: string) => { - const raw = kv()[`user:${username}`]; - return raw ? JSON.parse(raw) : null; + const k = username?.replace(/\s/g, "").toLowerCase(); + const raw = kv()[`user:${k}`]; + if (raw == null) return null; + let user: any; + try { user = JSON.parse(raw); } catch { user = raw; } + if (typeof user === "string") { + const raw2 = kv()[`user:${user}`]; + if (raw2 == null) return null; + try { user = JSON.parse(raw2); } catch { user = raw2; } + } + return user; }, getAccount: async () => null, link: (id: string) => `http://test/${id}`, sats: (n: number) => Math.round(n * SATS), sleep: async () => {}, t: () => ({ insufficientFunds: "Insufficient funds" }), - bail: (res: any, msg: string) => res.code(500).send(msg), + bail: (c: any, msg: string) => c.json(msg, 500), bip21: () => "", fields: [], nada: () => {}, |
