From 5b848ed4a69a811098fec93128d26018eff5d3ac Mon Sep 17 00:00:00 2001 From: Evelyn Osman Date: Wed, 22 Jul 2026 10:35:28 +0200 Subject: [PATCH] test: cover flush re-queue, single-flight sync, cursor url, torn audit line --- server/src/auditLog.test.ts | 28 +++++++++++++++++ server/src/sync.test.ts | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 server/src/auditLog.test.ts create mode 100644 server/src/sync.test.ts diff --git a/server/src/auditLog.test.ts b/server/src/auditLog.test.ts new file mode 100644 index 0000000..ebef3b9 --- /dev/null +++ b/server/src/auditLog.test.ts @@ -0,0 +1,28 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { writeFile, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +test("readRecent skips a torn trailing line", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "audit-")); + const logPath = path.join(dir, "audit.log"); + await writeFile( + logPath, + JSON.stringify({ at: "2026-01-01T00:00:00Z", operator: "op", action: "a" }) + + "\n" + + JSON.stringify({ at: "2026-01-02T00:00:00Z", operator: "op", action: "b" }) + + '\n{"at":"2026-', // crash mid-append + ); + // the module resolves AUDIT_LOG_PATH at import time + process.env.AUDIT_LOG_PATH = logPath; + const { readRecent } = await import("./auditLog.js"); + + const entries = await readRecent(); + assert.equal(entries.length, 2); + assert.deepEqual( + entries.map((e) => e.action), + ["b", "a"], // newest first + ); + await rm(dir, { recursive: true, force: true }); +}); diff --git a/server/src/sync.test.ts b/server/src/sync.test.ts new file mode 100644 index 0000000..582e1b6 --- /dev/null +++ b/server/src/sync.test.ts @@ -0,0 +1,62 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import type { FastifyBaseLogger } from "fastify"; +import { Syncer, accountStreamUrl } from "./sync.js"; +import { openDb, setSyncState, getSyncState } from "./db.js"; +import type { PdsClient } from "./pdsClient.js"; + +const stubLogger = { info() {}, warn() {}, error() {} } as unknown as FastifyBaseLogger; + +test("flushDirty re-queues dids when the flush fails", async () => { + const pds = { + hostname: "pds.test", + accountInfos: async () => { + throw new Error("pds down"); + }, + repoStatus: async () => { + throw new Error("pds down"); + }, + } as unknown as PdsClient; + const syncer = new Syncer(openDb(":memory:"), pds, [], stubLogger); + (syncer as any).fetchAvatars = async () => new Map(); // no network in tests + + (syncer as any).dirty.add("did:plc:x"); + await assert.rejects(() => (syncer as any).flushDirty(), /pds down/); + assert.ok((syncer as any).dirty.has("did:plc:x"), "failed flush must keep the did dirty"); +}); + +test("fullSync is single-flight", async () => { + let calls = 0; + let release!: () => void; + const gate = new Promise((r) => (release = r)); + const pds = { + hostname: "pds.test", + listAllRepos: async () => { + calls++; + await gate; + return []; + }, + accountInfos: async () => [], + } as unknown as PdsClient; + const syncer = new Syncer(openDb(":memory:"), pds, [], stubLogger); + (syncer as any).fetchAvatars = async () => new Map(); + + const first = syncer.fullSync(); + const second = syncer.fullSync(); // overlapping tick — must be a no-op + release(); + await Promise.all([first, second]); + assert.equal(calls, 1); +}); + +test("account stream url resumes from the persisted cursor", () => { + const db = openDb(":memory:"); + assert.equal( + accountStreamUrl("pds.test", getSyncState(db, "account_stream_cursor")), + "wss://pds.test/xrpc/com.atproto.sync.subscribeRepos", + ); + setSyncState(db, "account_stream_cursor", "42"); + assert.equal( + accountStreamUrl("pds.test", getSyncState(db, "account_stream_cursor")), + "wss://pds.test/xrpc/com.atproto.sync.subscribeRepos?cursor=42", + ); +}); -- 2.51.2