Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

175 lines
6.9 KiB
TypeScript

/**
* pds-verify unit tests.
*
* Cover the HTTP / error-shaping logic with a stub `fetch`. The actual
* verification handoff to `@atcute/repo`'s `verifyRecord` is NOT exercised
* end-to-end anywhere in this suite — building a valid signed CAR by hand
* would re-implement what `@atcute/repo` already tests internally, and the
* consumer-level test path stubs verification via `ConsumerDeps.verify`
* (the FakePublisher / MockPds fixture from `@emdash-cms/atproto-test-utils`
* can't load inside `@cloudflare/vitest-pool-workers` due to a transitive
* `@atproto/lex-data` incompatibility; see records-consumer test header).
*
* What we DO test here is the surface every reason code can be reached
* through, plus the `isTransient` policy mapping the consumer relies on.
*/
import { P256PublicKey, P256PrivateKeyExportable } from "@atcute/crypto";
import { beforeAll, describe, expect, it } from "vitest";
import { fetchAndVerifyRecord, isTransient, PdsVerificationError } from "../src/pds-verify.js";
const TEST_DID = "did:plc:test00000000000000000000";
const TEST_PDS = "https://pds.test.example";
let publicKey: P256PublicKey;
beforeAll(async () => {
const kp = await P256PrivateKeyExportable.createKeypair();
const raw = await kp.exportPublicKey("raw");
publicKey = await P256PublicKey.importRaw(raw);
});
async function captureRejection<T>(promise: Promise<T>): Promise<PdsVerificationError> {
try {
await promise;
} catch (err) {
if (err instanceof PdsVerificationError) return err;
throw err;
}
throw new Error("expected promise to reject with PdsVerificationError");
}
function buildOpts(overrides: {
fetch: typeof fetch;
timeoutMs?: number;
maxResponseBytes?: number;
}) {
return {
pds: TEST_PDS,
did: TEST_DID,
collection: "com.emdashcms.experimental.package.profile",
rkey: "demo",
publicKey,
...overrides,
};
}
describe("fetchAndVerifyRecord — HTTP path", () => {
it("builds the canonical sync.getRecord URL with did/collection/rkey", async () => {
let observedUrl: string | undefined;
const fetchImpl: typeof fetch = async (input) => {
observedUrl =
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
return new Response(new Uint8Array([0]), { status: 200 });
};
await fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl })).catch(() => {
/* verifyRecord rejects on the dummy bytes — we only care about the URL */
});
expect(observedUrl).toBe(
`${TEST_PDS}/xrpc/com.atproto.sync.getRecord?did=${encodeURIComponent(TEST_DID)}&collection=com.emdashcms.experimental.package.profile&rkey=demo`,
);
});
it("maps a network error to PDS_NETWORK_ERROR", async () => {
const fetchImpl: typeof fetch = () => Promise.reject(new TypeError("connection refused"));
await expect(fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl }))).rejects.toMatchObject({
name: "PdsVerificationError",
reason: "PDS_NETWORK_ERROR",
});
});
it("maps an aborted fetch (timeout) to PDS_NETWORK_ERROR with the timeout in the message", async () => {
const fetchImpl: typeof fetch = (_input, init) => {
return new Promise((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => {
const err = new DOMException("aborted", "AbortError");
reject(err);
});
});
};
const err = await captureRejection(
fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl, timeoutMs: 25 })),
);
expect(err.reason).toBe("PDS_NETWORK_ERROR");
expect(err.message).toMatch(/aborted after 25ms/);
});
it("maps a 404 to RECORD_NOT_FOUND with status", async () => {
const fetchImpl: typeof fetch = () => Promise.resolve(new Response("", { status: 404 }));
const err = await captureRejection(fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl })));
expect(err.reason).toBe("RECORD_NOT_FOUND");
expect(err.status).toBe(404);
});
it("maps a 500 to PDS_HTTP_ERROR with status", async () => {
const fetchImpl: typeof fetch = () => Promise.resolve(new Response("", { status: 503 }));
const err = await captureRejection(fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl })));
expect(err.reason).toBe("PDS_HTTP_ERROR");
expect(err.status).toBe(503);
});
it("maps a non-404 4xx to PDS_HTTP_ERROR with status", async () => {
const fetchImpl: typeof fetch = () => Promise.resolve(new Response("", { status: 401 }));
const err = await captureRejection(fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl })));
expect(err.reason).toBe("PDS_HTTP_ERROR");
expect(err.status).toBe(401);
});
it("rejects responses larger than maxResponseBytes with RESPONSE_TOO_LARGE", async () => {
const big = new Uint8Array(64);
big.fill(0xff);
const fetchImpl: typeof fetch = () => Promise.resolve(new Response(big, { status: 200 }));
const err = await captureRejection(
fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl, maxResponseBytes: 16 })),
);
expect(err.reason).toBe("RESPONSE_TOO_LARGE");
});
it("rejects a null body with INVALID_PROOF", async () => {
const fetchImpl: typeof fetch = () => {
// Construct a Response with a null body. The Response constructor
// allows null for HEAD-style responses; we never get null in
// practice but the guard is defensive.
return Promise.resolve(new Response(null, { status: 200 }));
};
const err = await captureRejection(fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl })));
expect(err.reason).toBe("INVALID_PROOF");
});
it("hands successful body bytes to verifyRecord (which rejects malformed input as INVALID_PROOF)", async () => {
// Random bytes are guaranteed not to parse as a valid CAR. The point
// of the test is that we got past HTTP and INTO verifyRecord — and
// that verifyRecord's rejection is wrapped as INVALID_PROOF.
const garbage = new Uint8Array([1, 2, 3, 4, 5]);
const fetchImpl: typeof fetch = () => Promise.resolve(new Response(garbage, { status: 200 }));
const err = await captureRejection(fetchAndVerifyRecord(buildOpts({ fetch: fetchImpl })));
expect(err.reason).toBe("INVALID_PROOF");
expect(err.cause).toBeDefined();
});
});
describe("isTransient policy", () => {
it("network errors retry", () => {
expect(isTransient("PDS_NETWORK_ERROR", undefined)).toBe(true);
});
it("HTTP 5xx retries", () => {
expect(isTransient("PDS_HTTP_ERROR", 500)).toBe(true);
expect(isTransient("PDS_HTTP_ERROR", 503)).toBe(true);
});
it("HTTP 4xx is permanent", () => {
expect(isTransient("PDS_HTTP_ERROR", 401)).toBe(false);
expect(isTransient("PDS_HTTP_ERROR", 400)).toBe(false);
});
it("missing status on PDS_HTTP_ERROR is treated as permanent", () => {
// Defensive: PDS_HTTP_ERROR is always raised with a status, but the
// policy must not blow up if a future code path drops it.
expect(isTransient("PDS_HTTP_ERROR", undefined)).toBe(false);
});
it("404, oversized response, and invalid proof are permanent", () => {
expect(isTransient("RECORD_NOT_FOUND", 404)).toBe(false);
expect(isTransient("RESPONSE_TOO_LARGE", undefined)).toBe(false);
expect(isTransient("INVALID_PROOF", undefined)).toBe(false);
});
});