From a694afeb5fcad4f7161ef814d32ba546e1cf4e60 Mon Sep 17 00:00:00 2001 From: Tiago Lupepic Date: Fri, 31 Jul 2026 11:19:31 +0200 Subject: [PATCH 1/4] feat(purchase-order): cover purchase_order_number payloads --- tests/subscription.test.ts | 12 +++++++-- tests/utils.ts | 7 ++++- tests/wallet.test.ts | 44 +++++++++++++++++++++++++++++--- tests/wallet_transaction.test.ts | 7 ++++- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/tests/subscription.test.ts b/tests/subscription.test.ts index 4f8a586..110e739 100644 --- a/tests/subscription.test.ts +++ b/tests/subscription.test.ts @@ -13,8 +13,11 @@ const subscriptionInput = { "external_id": "54321", "billing_time": "calendar", "subscription_at": "2022-08-08T00:00:00Z", + "purchase_order_number": "PO-123", }, -} satisfies SubscriptionCreateInput; +} satisfies SubscriptionCreateInput & { + subscription: { purchase_order_number: string }; +}; const subscriptionResponse = { "subscription": { @@ -34,8 +37,11 @@ const subscriptionResponse = { "previous_plan_code": "previous_code", "next_plan_code": "next_code", "downgrade_plan_date": "2022-09-14T16:35:31Z", + "purchase_order_number": "PO-123", }, -} satisfies Subscription; +} satisfies Subscription & { + subscription: { purchase_order_number: string }; +}; const subscriptionsResponse = { subscriptions: [subscriptionResponse.subscription], @@ -50,6 +56,7 @@ Deno.test("Successfully sent subscription responds with 2xx", async (t) => { inputParams: [subscriptionInput], responseObject: subscriptionResponse, status: 200, + expectedBody: subscriptionInput, }); }); @@ -76,6 +83,7 @@ Deno.test( inputParams: ["id", subscriptionInput], responseObject: subscriptionResponse, status: 200, + expectedBody: subscriptionInput, }); }, ); diff --git a/tests/utils.ts b/tests/utils.ts index f006108..2c3ad6e 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -48,6 +48,7 @@ export async function lagoTest< status, testType, urlParams, + expectedBody, }: { testType: "error" | "200"; t: Deno.TestContext; @@ -59,17 +60,21 @@ export async function lagoTest< >; status: number; urlParams?: Record; + expectedBody?: unknown; }, ) { const client = setupMockClient( route, - (_req) => { + async (_req) => { if (urlParams) { const urlSearchParams = new URLSearchParams(new URL(_req.url).search); Object.entries(urlParams).forEach(([key, value]) => { assertEquals(urlSearchParams.get(key), value); }); } + if (expectedBody) { + assertEquals(await _req.json(), expectedBody); + } return new Response( responseObject ? JSON.stringify(responseObject) : null, { status }, diff --git a/tests/wallet.test.ts b/tests/wallet.test.ts index 4e0b86d..395066c 100644 --- a/tests/wallet.test.ts +++ b/tests/wallet.test.ts @@ -15,8 +15,24 @@ const walletInput = { "granted_credits": 10, "external_customer_id": "12345", "expiration_at": "2022-09-14T23:59:59Z", + "purchase_order_number": "PO-123", + "recurring_transaction_rules": [ + { + "trigger": "interval", + "interval": "monthly", + "method": "fixed", + "paid_credits": 100, + "granted_credits": 10, + "purchase_order_number": "PO-RULE-123", + }, + ], }, -} as const satisfies WalletInput; +} satisfies WalletInput & { + wallet: { + purchase_order_number: string; + recurring_transaction_rules: Array<{ purchase_order_number: string }>; + }; +}; const walletResponse = { "wallet": { @@ -35,15 +51,35 @@ const walletResponse = { "last_balance_sync_at": "2022-09-14T16:35:31Z", "last_consumed_credit_at": "2022-09-14T16:35:31Z", "terminated_at": "2022-09-14T16:35:31Z", + "purchase_order_number": "PO-123", + "recurring_transaction_rules": [ + { + "lago_id": "483da83c-c007-4fbb-afcd-b00c07c41ffe", + "trigger": "interval", + "interval": "monthly", + "method": "fixed", + "paid_credits": 100, + "granted_credits": 10, + "purchase_order_number": "PO-RULE-123", + }, + ], }, -} as const satisfies Wallet; +} satisfies Wallet & { + wallet: { + purchase_order_number: string; + recurring_transaction_rules: Array<{ purchase_order_number: string }>; + }; +}; const walletUpdateInput = { "wallet": { "name": "Wallet name", "expiration_at": "2022-09-14T23:59:59Z", + "purchase_order_number": "PO-123", }, -} as const satisfies WalletUpdateInput; +} satisfies WalletUpdateInput & { + wallet: { purchase_order_number: string }; +}; const walletsResponse = { wallets: [walletInput.wallet], @@ -58,6 +94,7 @@ Deno.test("Successfully sent wallet responds with 2xx", async (t) => { inputParams: [walletInput], responseObject: walletResponse, status: 200, + expectedBody: walletInput, }); }); @@ -82,6 +119,7 @@ Deno.test("Successfully sent wallet update request responds with 2xx", async (t) inputParams: ["id", walletUpdateInput], responseObject: walletResponse, status: 200, + expectedBody: walletUpdateInput, }); }); diff --git a/tests/wallet_transaction.test.ts b/tests/wallet_transaction.test.ts index b73c4f0..0911568 100644 --- a/tests/wallet_transaction.test.ts +++ b/tests/wallet_transaction.test.ts @@ -6,8 +6,11 @@ const walletTransactionInput = { "wallet_id": "985da83c-c007-4fbb-afcd-b00c07c41ffe", "paid_credits": 100, "granted_credits": 10, + "purchase_order_number": "PO-123", }, -} as const satisfies WalletTransactionInput; +} satisfies WalletTransactionInput & { + wallet_transaction: { purchase_order_number: string }; +}; Deno.test( "Successfully sent wallet transaction responds with 2xx", @@ -27,12 +30,14 @@ Deno.test( transaction_type: "inbound", amount: 500, credit_amount: 500, + purchase_order_number: "PO-123", settled_at: "2022-09-14T16:35:31Z", created_at: "2022-09-14T16:35:31Z", }, ], }, status: 200, + expectedBody: walletTransactionInput, }); }, ); From 2781e4af04edb2e25b7bbdec0b50196b160febca Mon Sep 17 00:00:00 2001 From: Tiago Lupepic Date: Fri, 31 Jul 2026 11:29:46 +0200 Subject: [PATCH 2/4] fix(test): replace mock_fetch dependency --- dev_deps.ts | 1 - tests/utils.ts | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dev_deps.ts b/dev_deps.ts index 3a00c34..8f97861 100644 --- a/dev_deps.ts +++ b/dev_deps.ts @@ -1,2 +1 @@ export { assertEquals } from "https://deno.land/std@0.196.0/assert/mod.ts"; -export * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; diff --git a/tests/utils.ts b/tests/utils.ts index 2c3ad6e..a641542 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,6 +1,5 @@ // deno-lint-ignore-file no-explicit-any ban-types import { assertEquals } from "../dev_deps.ts"; -import { mf } from "../dev_deps.ts"; import { Client, getLagoError } from "../mod.ts"; import type { Api, @@ -27,10 +26,20 @@ type ExtractLagoResponse = E extends ( const errorMessage = "Lago Error" as const; -export function setupMockClient(route: string, handler: mf.MatchHandler) { - const { fetch, mock } = mf.sandbox(); +type MatchHandler = (request: Request) => Response | Promise; - mock(route, handler); +export function setupMockClient(route: string, handler: MatchHandler) { + const [method, path] = route.split("@"); + + const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const request = input instanceof Request ? input : new Request(input, init); + const url = new URL(request.url); + + assertEquals(request.method, method); + assertEquals(url.pathname, path); + + return await handler(request); + }) as typeof globalThis.fetch; return Client("api_key", { customFetch: fetch }); } From 003f907572abb4e8ca60ad1ea67a2fd3f736345a Mon Sep 17 00:00:00 2001 From: Tiago Lupepic Date: Thu, 27 Aug 2026 11:06:15 +0200 Subject: [PATCH 3/4] fix(test): address PR #95 review feedback on fixtures and mocks - Drop `satisfies X & { ... }` intersections that made purchase_order_number checks unable to fail if the field disappeared - Fix stale generated type names (WalletInput, Wallets, WalletTransactionInput) - Fix numeric fixtures that should be strings (paid_credits, granted_credits, amount, credit_amount) and fill in required recurring-rule response fields - Build walletsResponse from the response fixture instead of the input fixture - Restore expectedBody support in lagoTest with an undefined check instead of truthiness, and assert after the client call returns so failures surface as normal assertion errors instead of being swallowed by the error path - Dedupe the hand-rolled mock fetch in rate_limit.test.ts to reuse tests/utils.ts's createMockFetch Co-Authored-By: Claude Sonnet 5 --- tests/rate_limit.test.ts | 13 ++++----- tests/subscription.test.ts | 8 ++---- tests/utils.ts | 10 +++++-- tests/wallet.test.ts | 49 ++++++++++++++++---------------- tests/wallet_transaction.test.ts | 14 ++++----- 5 files changed, 45 insertions(+), 49 deletions(-) diff --git a/tests/rate_limit.test.ts b/tests/rate_limit.test.ts index 4d42aa2..90665b1 100644 --- a/tests/rate_limit.test.ts +++ b/tests/rate_limit.test.ts @@ -9,17 +9,14 @@ import { type RateLimitInfo, rateLimitUsagePct, } from "../mod.ts"; +import { createMockFetch as createLagoMockFetch } from "./utils.ts"; -// Simple fetch mock helper (replaces broken mock_fetch library) +// Route is irrelevant here since these tests never assert method/path, +// so any value satisfying LagoRoute works. function createMockFetch( - handler: ( - input: RequestInfo | URL, - init?: RequestInit, - ) => Response | Promise, + handler: (request: Request) => Response | Promise, ): typeof fetch { - return (input: RequestInfo | URL, init?: RequestInit) => { - return Promise.resolve(handler(input, init)); - }; + return createLagoMockFetch("GET@/api/v1/mock", handler).fetch; } Deno.test("LagoRateLimitError contains rate limit information", () => { diff --git a/tests/subscription.test.ts b/tests/subscription.test.ts index 110e739..2eb8c60 100644 --- a/tests/subscription.test.ts +++ b/tests/subscription.test.ts @@ -15,9 +15,7 @@ const subscriptionInput = { "subscription_at": "2022-08-08T00:00:00Z", "purchase_order_number": "PO-123", }, -} satisfies SubscriptionCreateInput & { - subscription: { purchase_order_number: string }; -}; +} as const satisfies SubscriptionCreateInput; const subscriptionResponse = { "subscription": { @@ -39,9 +37,7 @@ const subscriptionResponse = { "downgrade_plan_date": "2022-09-14T16:35:31Z", "purchase_order_number": "PO-123", }, -} satisfies Subscription & { - subscription: { purchase_order_number: string }; -}; +} satisfies Subscription; const subscriptionsResponse = { subscriptions: [subscriptionResponse.subscription], diff --git a/tests/utils.ts b/tests/utils.ts index 3e1b8d8..346c031 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -66,6 +66,7 @@ export async function lagoTest< status, testType, urlParams, + expectedBody, }: { testType: "error" | "200"; t: Deno.TestContext; @@ -77,6 +78,7 @@ export async function lagoTest< >; status: number; urlParams?: Record; + expectedBody?: unknown; }, ) { const { fetch, getRequest, expectedMethod, expectedPath } = createMockFetch( @@ -90,7 +92,7 @@ export async function lagoTest< ); const client = Client("api_key", { customFetch: fetch }); - const assertRequest = () => { + const assertRequest = async () => { const request = getRequest(); if (!request) throw new Error("Expected a request to be sent"); @@ -104,6 +106,10 @@ export async function lagoTest< assertEquals(urlSearchParams.get(key), value); }); } + + if (expectedBody !== undefined) { + assertEquals(await request.json(), expectedBody); + } }; switch (testType) { @@ -125,7 +131,7 @@ export async function lagoTest< (lagoError as ApiErrorUnprocessableEntity).error, errorMessage, ); - assertRequest(); + await assertRequest(); } }); break; diff --git a/tests/wallet.test.ts b/tests/wallet.test.ts index 395066c..8ab4254 100644 --- a/tests/wallet.test.ts +++ b/tests/wallet.test.ts @@ -1,7 +1,7 @@ import type { Wallet, - WalletInput, - Wallets, + WalletCreateInput, + WalletsPaginated, WalletUpdateInput, } from "../mod.ts"; import { lagoTest, unprocessableErrorResponse } from "./utils.ts"; @@ -11,8 +11,8 @@ const walletInput = { "name": "Wallet name", "rate_amount": 2, "currency": "EUR", - "paid_credits": 500, - "granted_credits": 10, + "paid_credits": "500", + "granted_credits": "10", "external_customer_id": "12345", "expiration_at": "2022-09-14T23:59:59Z", "purchase_order_number": "PO-123", @@ -21,18 +21,13 @@ const walletInput = { "trigger": "interval", "interval": "monthly", "method": "fixed", - "paid_credits": 100, - "granted_credits": 10, + "paid_credits": "100", + "granted_credits": "10", "purchase_order_number": "PO-RULE-123", }, ], }, -} satisfies WalletInput & { - wallet: { - purchase_order_number: string; - recurring_transaction_rules: Array<{ purchase_order_number: string }>; - }; -}; +} satisfies WalletCreateInput; const walletResponse = { "wallet": { @@ -58,18 +53,24 @@ const walletResponse = { "trigger": "interval", "interval": "monthly", "method": "fixed", - "paid_credits": 100, - "granted_credits": 10, + "status": "active", + "threshold_credits": "10", + "paid_credits": "100", + "granted_credits": "10", + "grants_target_top_up": null, + "started_at": "2022-08-08T00:00:00Z", + "target_ongoing_balance": null, + "created_at": "2022-09-14T16:35:31Z", + "expiration_at": null, + "invoice_requires_successful_payment": false, + "transaction_name": null, + "ignore_paid_top_up_limits": false, + "transaction_metadata": [], "purchase_order_number": "PO-RULE-123", }, ], }, -} satisfies Wallet & { - wallet: { - purchase_order_number: string; - recurring_transaction_rules: Array<{ purchase_order_number: string }>; - }; -}; +} satisfies Wallet; const walletUpdateInput = { "wallet": { @@ -77,13 +78,11 @@ const walletUpdateInput = { "expiration_at": "2022-09-14T23:59:59Z", "purchase_order_number": "PO-123", }, -} satisfies WalletUpdateInput & { - wallet: { purchase_order_number: string }; -}; +} satisfies WalletUpdateInput; const walletsResponse = { - wallets: [walletInput.wallet], -} satisfies Wallets; + wallets: [walletResponse.wallet], +} satisfies WalletsPaginated; Deno.test("Successfully sent wallet responds with 2xx", async (t) => { await lagoTest({ diff --git a/tests/wallet_transaction.test.ts b/tests/wallet_transaction.test.ts index 0911568..127298f 100644 --- a/tests/wallet_transaction.test.ts +++ b/tests/wallet_transaction.test.ts @@ -1,16 +1,14 @@ -import type { WalletTransaction, WalletTransactionInput } from "../mod.ts"; +import type { WalletTransactionCreateInput } from "../mod.ts"; import { lagoTest, unprocessableErrorResponse } from "./utils.ts"; const walletTransactionInput = { "wallet_transaction": { "wallet_id": "985da83c-c007-4fbb-afcd-b00c07c41ffe", - "paid_credits": 100, - "granted_credits": 10, + "paid_credits": "100", + "granted_credits": "10", "purchase_order_number": "PO-123", }, -} satisfies WalletTransactionInput & { - wallet_transaction: { purchase_order_number: string }; -}; +} satisfies WalletTransactionCreateInput; Deno.test( "Successfully sent wallet transaction responds with 2xx", @@ -28,8 +26,8 @@ Deno.test( lago_wallet_id: "", status: "settled", transaction_type: "inbound", - amount: 500, - credit_amount: 500, + amount: "500", + credit_amount: "500", purchase_order_number: "PO-123", settled_at: "2022-09-14T16:35:31Z", created_at: "2022-09-14T16:35:31Z", From 92ddbeaa95eb7209f1eb763ecbe788363e0b28d2 Mon Sep 17 00:00:00 2001 From: Tiago Lupepic Date: Thu, 27 Aug 2026 11:13:34 +0200 Subject: [PATCH 4/4] fix(test): await assertRequest in the 200 success path The error path awaited assertRequest but the 200 path didn't, so a body mismatch there would surface as an unhandled promise rejection instead of a normal assertion failure. Co-Authored-By: Claude Sonnet 5 --- tests/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils.ts b/tests/utils.ts index 346c031..6138a13 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -144,7 +144,7 @@ export async function lagoTest< ) as Response; assertEquals(response.status, 200); - assertRequest(); + await assertRequest(); }); break;