import { describe, expect, it } from "vitest"; import { createDavFixture, davRequest } from "@tests/worker/helpers/dav"; import { fetchWorker } from "@tests/worker/helpers/http"; import { withEnvBinding } from "@tests/worker/helpers/env"; import { reportBody, uid, vcard } from "./helpers"; describe("CardDAV report limits and origins", () => { it("applies the configured REPORT result limit to addressbook-query and addressbook-multiget", async () => { const fixture = await createDavFixture(["carddav.full"]); const firstName = `limited-a-${crypto.randomUUID()}.vcf`; const secondName = `limited-b-${crypto.randomUUID()}.vcf`; expect( await fetchWorker( davRequest(fixture, `/addressbooks/default/${firstName}`, { method: "PUT", body: vcard({ uid: uid(), fn: "Limited A" }), }), ), ).toMatchObject({ status: 201 }); expect( await fetchWorker( davRequest(fixture, `/addressbooks/default/${secondName}`, { method: "PUT", body: vcard({ uid: uid(), fn: "Limited B" }), }), ), ).toMatchObject({ status: 201 }); await withEnvBinding("MAX_REPORT_RESULTS", "1", async () => { // RFC 6352 8.3: when an addressbook-query result set exceeds the configured // limit, return a DAV:number-of-matches-within-limits precondition error. const query = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody("addressbook-query", ""), }), ); expect(query.status).toBe(403); await expect(query.text()).resolves.toContain("number-of-matches-within-limits"); const multiget = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-multiget", `/addressbooks/default/${firstName}/addressbooks/default/${secondName}`, ), }), ); expect(multiget.status).toBe(403); }); }); it("rejects unsupported addressbook-query text-match options", async () => { const fixture = await createDavFixture(["carddav.full"]); const unsupportedCollation = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-query", 'alice', ), }), ); expect(unsupportedCollation.status).toBe(400); await expect(unsupportedCollation.text()).resolves.toContain("Unsupported CardDAV text-match collation"); const tooLarge = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-query", `${"x".repeat(1025)}`, ), }), ); expect(tooLarge.status).toBe(400); await expect(tooLarge.text()).resolves.toContain("CardDAV text-match is limited"); }); it("rejects addressbook-multiget absolute hrefs outside the subject origin", async () => { const fixture = await createDavFixture(["carddav.full"]); const name = `absolute-origin-${crypto.randomUUID()}.vcf`; const contactUid = uid(); expect( await fetchWorker( davRequest(fixture, `/addressbooks/default/${name}`, { method: "PUT", body: vcard({ uid: contactUid, fn: "Absolute Origin" }), }), ), ).toMatchObject({ status: 201 }); const multiget = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-multiget", `https://other.example.test/addressbooks/default/${name}`, ), }), ); expect(multiget.status).toBe(207); const xml = await multiget.text(); expect(xml).toContain(`https://other.example.test/addressbooks/default/${name}`); expect(xml).toContain("HTTP/1.1 404 Not Found"); expect(xml).not.toContain(contactUid); }); });