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

515 lines
19 KiB
TypeScript

/**
* Read API integration tests.
*
* Each test seeds D1 directly with the columns the handlers read, then
* exercises the handler via `SELF.fetch` to a `/xrpc/...` URL — same path
* a real client would take. Asserts on the envelope shape (uri, cid, did,
* indexedAt, mirrors, labels) and on error mappings (404 NotFound, 400
* InvalidRequest).
*
* `mirrors: []` and `labels: []` are the v1 contract; Slice 2 (labels)
* and Slice 3 (mirrors) populate them, but the contract is locked now so
* cached clients don't see a shape change later.
*/
import { NSID } from "@emdash-cms/registry-lexicons";
import { applyD1Migrations, env, SELF } from "cloudflare:test";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
interface TestEnv {
DB: D1Database;
TEST_MIGRATIONS: Parameters<typeof applyD1Migrations>[1];
}
const testEnv = env as unknown as TestEnv;
const DID_A = "did:plc:read000000000000000000aa";
const DID_B = "did:plc:read000000000000000000bb";
const NOW = new Date("2026-05-10T12:00:00.000Z");
beforeAll(async () => {
await applyD1Migrations(testEnv.DB, testEnv.TEST_MIGRATIONS);
});
beforeEach(async () => {
// Tables in dependency order: releases → packages (FK), then publishers
// + verifications.
await testEnv.DB.prepare("DELETE FROM releases").run();
await testEnv.DB.prepare("DELETE FROM packages").run();
await testEnv.DB.prepare("DELETE FROM publishers").run();
await testEnv.DB.prepare("DELETE FROM publisher_verifications").run();
});
interface SeedPackageOpts {
did?: string;
slug?: string;
type?: string;
name?: string | null;
description?: string | null;
license?: string;
keywords?: string[] | null;
latestVersion?: string | null;
cid?: string;
indexedAt?: string;
verifiedAt?: string;
carBytes?: Uint8Array;
}
async function seedPackage(opts: SeedPackageOpts = {}): Promise<void> {
const did = opts.did ?? DID_A;
const slug = opts.slug ?? "demo";
const indexedAt = opts.indexedAt ?? NOW.toISOString();
await testEnv.DB.prepare(
`INSERT INTO packages
(did, slug, type, name, description, license, authors, security, keywords,
sections, last_updated, latest_version, capabilities, record_blob,
signature_metadata, verified_at, indexed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.bind(
did,
slug,
opts.type ?? "emdash-plugin",
opts.name ?? "Demo Plugin",
opts.description ?? "A demo plugin",
opts.license ?? "MIT",
JSON.stringify([{ name: "Tester" }]),
JSON.stringify([{ email: "x@y.test" }]),
opts.keywords === null ? null : JSON.stringify(opts.keywords ?? ["demo"]),
null,
NOW.toISOString(),
opts.latestVersion ?? null,
null,
opts.carBytes ?? new Uint8Array([0xa1, 0xa2, 0xa3]),
JSON.stringify({ cid: opts.cid ?? "bafyseed" }),
opts.verifiedAt ?? NOW.toISOString(),
indexedAt,
)
.run();
}
interface SeedReleaseOpts {
did?: string;
package?: string;
version: string;
versionSort?: string;
tombstoned?: boolean;
cid?: string;
carBytes?: Uint8Array;
}
async function seedRelease(opts: SeedReleaseOpts): Promise<void> {
const did = opts.did ?? DID_A;
const pkg = opts.package ?? "demo";
const rkey = `${pkg}:${opts.version}`;
await testEnv.DB.prepare(
`INSERT INTO releases
(did, package, version, rkey, version_sort, artifacts, requires, suggests,
emdash_extension, repo_url, cts, record_blob, signature_metadata,
verified_at, indexed_at, tombstoned_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.bind(
did,
pkg,
opts.version,
rkey,
opts.versionSort ?? defaultVersionSort(opts.version),
JSON.stringify({ package: { url: "https://x.test/d.tgz", checksum: "bsha256-abc" } }),
null,
null,
JSON.stringify({ declaredAccess: {} }),
null,
NOW.toISOString(),
opts.carBytes ?? new Uint8Array([0xb1, 0xb2, 0xb3]),
JSON.stringify({ cid: opts.cid ?? `bafrel-${opts.version}` }),
NOW.toISOString(),
NOW.toISOString(),
opts.tombstoned ? NOW.toISOString() : null,
)
.run();
}
/** Naive 1.x.y zero-padded version_sort for the test fixtures. Real values
* come from the consumer's `computeVersionSort`; tests just need the
* relative ordering to be right. */
function defaultVersionSort(version: string): string {
const [major = "0", minor = "0", patch = "0"] = version.split(".");
const pad = (s: string) => s.padStart(10, "0");
return `${pad(major)}.${pad(minor)}.${pad(patch)}.~`;
}
describe("getPackage", () => {
it("returns the packageView envelope for an indexed package", async () => {
await seedPackage({ slug: "demo", latestVersion: "1.0.0" });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorGetPackage}?did=${DID_A}&slug=demo`,
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body).toMatchObject({
uri: `at://${DID_A}/${NSID.packageProfile}/demo`,
cid: "bafyseed",
did: DID_A,
slug: "demo",
latestVersion: "1.0.0",
indexedAt: NOW.toISOString(),
labels: [],
});
// `mirrors` is on releaseView only — assert it's NOT on packageView.
expect(body).not.toHaveProperty("mirrors");
const profile = body["profile"] as Record<string, unknown>;
expect(profile["$type"]).toBe(NSID.packageProfile);
expect(profile["id"]).toBe(`at://${DID_A}/${NSID.packageProfile}/demo`);
expect(profile["license"]).toBe("MIT");
expect(profile["slug"]).toBe("demo");
});
it("returns 404 NotFound when no row matches", async () => {
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorGetPackage}?did=${DID_A}&slug=missing`,
);
expect(res.status).toBe(404);
const body = (await res.json()) as { error: string };
expect(body.error).toBe("NotFound");
});
it("returns 400 InvalidRequest on missing required params", async () => {
const res = await SELF.fetch(`https://test/xrpc/${NSID.aggregatorGetPackage}?did=${DID_A}`);
expect(res.status).toBe(400);
});
it("sets Cache-Control: private, no-store on success", async () => {
await seedPackage({ slug: "demo" });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorGetPackage}?did=${DID_A}&slug=demo`,
);
expect(res.headers.get("cache-control")).toBe("private, no-store");
});
it("omits latestVersion when no release has been written yet", async () => {
await seedPackage({ slug: "fresh", latestVersion: null });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorGetPackage}?did=${DID_A}&slug=fresh`,
);
const body = (await res.json()) as Record<string, unknown>;
expect(body).not.toHaveProperty("latestVersion");
});
});
describe("listReleases", () => {
it("returns releases ordered by descending semver", async () => {
await seedPackage({ slug: "demo", latestVersion: "2.0.0" });
await seedRelease({ version: "1.0.0" });
await seedRelease({ version: "1.10.0" });
await seedRelease({ version: "2.0.0" });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorListReleases}?did=${DID_A}&package=demo`,
);
expect(res.status).toBe(200);
const body = (await res.json()) as { releases: Array<{ version: string }>; cursor?: string };
expect(body.releases.map((r) => r.version)).toEqual(["2.0.0", "1.10.0", "1.0.0"]);
expect(body).not.toHaveProperty("cursor");
});
it("filters tombstoned releases", async () => {
await seedPackage({ slug: "demo" });
await seedRelease({ version: "1.0.0" });
await seedRelease({ version: "1.1.0", tombstoned: true });
await seedRelease({ version: "1.2.0" });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorListReleases}?did=${DID_A}&package=demo`,
);
const body = (await res.json()) as { releases: Array<{ version: string }> };
expect(body.releases.map((r) => r.version)).toEqual(["1.2.0", "1.0.0"]);
});
it("paginates via cursor", async () => {
await seedPackage({ slug: "demo" });
for (let i = 1; i <= 5; i++) await seedRelease({ version: `1.${i}.0` });
const first = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorListReleases}?did=${DID_A}&package=demo&limit=2`,
);
const firstBody = (await first.json()) as {
releases: Array<{ version: string }>;
cursor: string;
};
expect(firstBody.releases.map((r) => r.version)).toEqual(["1.5.0", "1.4.0"]);
expect(firstBody.cursor).toBeTruthy();
const second = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorListReleases}?did=${DID_A}&package=demo&limit=2&cursor=${encodeURIComponent(firstBody.cursor)}`,
);
const secondBody = (await second.json()) as {
releases: Array<{ version: string }>;
cursor?: string;
};
expect(secondBody.releases.map((r) => r.version)).toEqual(["1.3.0", "1.2.0"]);
expect(secondBody.cursor).toBeTruthy();
});
it("returns 404 when the parent package is missing", async () => {
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorListReleases}?did=${DID_A}&package=missing`,
);
expect(res.status).toBe(404);
});
it("400s on a provided-but-malformed cursor", async () => {
await seedPackage({ slug: "demo" });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorListReleases}?did=${DID_A}&package=demo&cursor=garbage!!!`,
);
expect(res.status).toBe(400);
});
});
describe("getLatestRelease", () => {
it("returns the release pointed to by packages.latest_version", async () => {
await seedPackage({ slug: "demo", latestVersion: "2.0.0" });
await seedRelease({ version: "1.0.0" });
await seedRelease({ version: "2.0.0" });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorGetLatestRelease}?did=${DID_A}&package=demo`,
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body["version"]).toBe("2.0.0");
expect(body["uri"]).toBe(`at://${DID_A}/${NSID.packageRelease}/demo:2.0.0`);
});
it("returns 404 when ALL releases are tombstoned (or none exist)", async () => {
await seedPackage({ slug: "demo", latestVersion: "1.0.0" });
await seedRelease({ version: "1.0.0", tombstoned: true });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorGetLatestRelease}?did=${DID_A}&package=demo`,
);
expect(res.status).toBe(404);
});
it("falls back to the next-best release when latest_version points at a tombstoned one", async () => {
// `latest_version` was set to 2.0.0, then 2.0.0 was tombstoned but
// `refreshPackageLatestStmt` hasn't run yet (or failed). The fast-path
// JOIN misses; the slow-path ORDER BY should still find 1.0.0 and
// return it instead of 404ing.
await seedPackage({ slug: "demo", latestVersion: "2.0.0" });
await seedRelease({ version: "1.0.0" });
await seedRelease({ version: "2.0.0", tombstoned: true });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorGetLatestRelease}?did=${DID_A}&package=demo`,
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body["version"]).toBe("1.0.0");
});
it("returns 404 when no package row exists", async () => {
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorGetLatestRelease}?did=${DID_A}&package=missing`,
);
expect(res.status).toBe(404);
});
});
describe("searchPackages", () => {
it("returns FTS-matched packages", async () => {
await seedPackage({ slug: "gallery", name: "Gallery Plugin", description: "image gallery" });
await seedPackage({ slug: "form", name: "Form Plugin", description: "form builder" });
const res = await SELF.fetch(`https://test/xrpc/${NSID.aggregatorSearchPackages}?q=gallery`);
expect(res.status).toBe(200);
const body = (await res.json()) as { packages: Array<{ slug: string }> };
expect(body.packages.map((p) => p.slug)).toContain("gallery");
expect(body.packages.map((p) => p.slug)).not.toContain("form");
});
it("returns all packages ordered by last_updated DESC when q is empty", async () => {
await seedPackage({ slug: "alpha" });
await seedPackage({ slug: "beta" });
const res = await SELF.fetch(`https://test/xrpc/${NSID.aggregatorSearchPackages}`);
const body = (await res.json()) as { packages: Array<{ slug: string }> };
expect(body.packages.map((p) => p.slug).toSorted()).toEqual(["alpha", "beta"]);
});
it("paginates via offset cursor", async () => {
for (let i = 0; i < 5; i++) await seedPackage({ slug: `pkg${i}` });
const first = await SELF.fetch(`https://test/xrpc/${NSID.aggregatorSearchPackages}?limit=2`);
const firstBody = (await first.json()) as {
packages: Array<{ slug: string }>;
cursor: string;
};
expect(firstBody.packages).toHaveLength(2);
expect(firstBody.cursor).toBeTruthy();
const second = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorSearchPackages}?limit=2&cursor=${encodeURIComponent(firstBody.cursor)}`,
);
const secondBody = (await second.json()) as { packages: Array<{ slug: string }> };
expect(secondBody.packages).toHaveLength(2);
// Distinct from page 1 — seeded slugs don't overlap.
const overlap = firstBody.packages
.map((p) => p.slug)
.filter((s) => secondBody.packages.some((p) => p.slug === s));
expect(overlap).toEqual([]);
});
it("doesn't blow up on FTS-unsafe query chars (defensive quoting)", async () => {
await seedPackage({ slug: "demo", name: "Demo" });
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorSearchPackages}?q=${encodeURIComponent('demo "*"(')}`,
);
// Either matches nothing or matches normally — but doesn't 500.
expect(res.status).toBe(200);
});
it("treats FTS operators as literal tokens (the escape actually works)", async () => {
await seedPackage({ slug: "alpha", name: "Alpha" });
await seedPackage({ slug: "beta", name: "Beta" });
// `alpha OR beta` would match both packages if `OR` were interpreted
// as the FTS5 operator. With proper escaping the whole string is one
// literal phrase that can't possibly appear in either record's
// indexed text → zero matches. A buggy escape that stripped the
// quotes would return *both* packages.
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorSearchPackages}?q=${encodeURIComponent("alpha OR beta")}`,
);
expect(res.status).toBe(200);
const body = (await res.json()) as { packages: Array<{ slug: string }> };
expect(body.packages).toEqual([]);
});
it("400s on a provided-but-malformed cursor (no silent restart)", async () => {
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorSearchPackages}?cursor=not-a-valid-cursor`,
);
expect(res.status).toBe(400);
});
it("400s on a forged cursor with an out-of-range offset", async () => {
// Encode {offset: 1_000_000} → over MAX_OFFSET → 400.
const forged = btoa(JSON.stringify({ offset: 1_000_000 }))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const res = await SELF.fetch(
`https://test/xrpc/${NSID.aggregatorSearchPackages}?cursor=${forged}`,
);
expect(res.status).toBe(400);
});
});
describe("sync.getRecord", () => {
const PATH = "/xrpc/com.atproto.sync.getRecord";
it("returns CAR bytes for an indexed package profile", async () => {
await seedPackage({ slug: "demo", carBytes: new Uint8Array([0x11, 0x22, 0x33]) });
const res = await SELF.fetch(
`https://test${PATH}?did=${DID_A}&collection=${NSID.packageProfile}&rkey=demo`,
);
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toBe("application/vnd.ipld.car");
expect(res.headers.get("cache-control")).toBe("public, max-age=300");
const bytes = new Uint8Array(await res.arrayBuffer());
expect([...bytes]).toEqual([0x11, 0x22, 0x33]);
});
it("returns CAR bytes for an indexed release", async () => {
await seedPackage({ slug: "demo" });
await seedRelease({ version: "1.0.0", carBytes: new Uint8Array([0x44, 0x55]) });
const res = await SELF.fetch(
`https://test${PATH}?did=${DID_A}&collection=${NSID.packageRelease}&rkey=demo:1.0.0`,
);
expect(res.status).toBe(200);
const bytes = new Uint8Array(await res.arrayBuffer());
expect([...bytes]).toEqual([0x44, 0x55]);
});
it("returns 404 for a tombstoned release", async () => {
await seedPackage({ slug: "demo" });
await seedRelease({ version: "1.0.0", tombstoned: true });
const res = await SELF.fetch(
`https://test${PATH}?did=${DID_A}&collection=${NSID.packageRelease}&rkey=demo:1.0.0`,
);
expect(res.status).toBe(404);
});
it("returns 404 for an unknown rkey", async () => {
const res = await SELF.fetch(
`https://test${PATH}?did=${DID_A}&collection=${NSID.packageProfile}&rkey=does-not-exist`,
);
expect(res.status).toBe(404);
});
it("returns 400 InvalidRequest on missing query params", async () => {
const res = await SELF.fetch(`https://test${PATH}?did=${DID_A}`);
expect(res.status).toBe(400);
});
it("returns 400 on a malformed DID", async () => {
const res = await SELF.fetch(
`https://test${PATH}?did=not-a-did&collection=${NSID.packageProfile}&rkey=demo`,
);
expect(res.status).toBe(400);
});
it("returns HEAD with content-length but no body", async () => {
await seedPackage({ slug: "demo", carBytes: new Uint8Array([0x11, 0x22, 0x33]) });
const res = await SELF.fetch(
`https://test${PATH}?did=${DID_A}&collection=${NSID.packageProfile}&rkey=demo`,
{ method: "HEAD" },
);
expect(res.status).toBe(200);
expect(res.headers.get("content-length")).toBe("3");
const bytes = new Uint8Array(await res.arrayBuffer());
expect(bytes.byteLength).toBe(0);
});
it("rejects non-GET/HEAD methods with 405", async () => {
const res = await SELF.fetch(
`https://test${PATH}?did=${DID_A}&collection=${NSID.packageProfile}&rkey=demo`,
{ method: "POST" },
);
expect(res.status).toBe(405);
expect(res.headers.get("allow")).toBe("GET, HEAD");
});
it("only matches publisher.profile when rkey='self'", async () => {
// publisher.profile rkey is always 'self'; any other rkey returns 404
// even if the (did) row exists.
await testEnv.DB.prepare(
`INSERT INTO publishers
(did, display_name, record_blob, verified_at, indexed_at)
VALUES (?, ?, ?, ?, ?)`,
)
.bind(DID_B, "Pub", new Uint8Array([0x99]), NOW.toISOString(), NOW.toISOString())
.run();
const wrongRkey = await SELF.fetch(
`https://test${PATH}?did=${DID_B}&collection=${NSID.publisherProfile}&rkey=other`,
);
expect(wrongRkey.status).toBe(404);
const correctRkey = await SELF.fetch(
`https://test${PATH}?did=${DID_B}&collection=${NSID.publisherProfile}&rkey=self`,
);
expect(correctRkey.status).toBe(200);
});
});
describe("XRPC dispatcher", () => {
it("returns 404 on non-XRPC paths", async () => {
const res = await SELF.fetch("https://test/some/random/path");
expect(res.status).toBe(404);
});
it("returns 404 on unknown XRPC NSIDs", async () => {
const res = await SELF.fetch("https://test/xrpc/com.example.notARealEndpoint");
expect(res.status).toBe(404);
});
});