import { describe, expect, it } from "vitest"; import { createDavFixture, davRequest } from "@tests/worker/helpers/dav"; import { fetchWorker } from "@tests/worker/helpers/http"; import { propfindBody, reportBody, uid, vcard } from "./helpers"; describe("CardDAV reports", () => { it("supports addressbook-query and addressbook-multiget", async () => { const fixture = await createDavFixture(["carddav.full"]); const aliceUid = uid(); const bobUid = uid(); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/alice.vcf", { method: "PUT", body: vcard({ uid: aliceUid, fn: "Alice Query", email: "alice-query@example.test" }), }), ), ).toMatchObject({ status: 201 }); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/bob.vcf", { method: "PUT", body: vcard({ uid: bobUid, fn: "Bob Query", email: "bob-query@example.test" }), }), ), ).toMatchObject({ status: 201 }); const query = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-query", 'alice-query', ), }), ); expect(query.status).toBe(207); const queryXml = await query.text(); expect(queryXml).toContain("/addressbooks/default/alice.vcf"); expect(queryXml).not.toContain("/addressbooks/default/bob.vcf"); expect(queryXml).toContain(`UID:${aliceUid}`); const multiget = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-multiget", "/addressbooks/default/bob.vcf/addressbooks/default/missing.vcf", ), }), ); expect(multiget.status).toBe(207); const multigetXml = await multiget.text(); expect((multigetXml.match(//g) ?? []).length).toBe(2); expect(multigetXml).toContain("/addressbooks/default/bob.vcf"); expect(multigetXml).toContain(`UID:${bobUid}`); expect(multigetXml).toContain("/addressbooks/default/missing.vcf"); expect(multigetXml).toContain("HTTP/1.1 404 Not Found"); }); it("matches grouped vCard EMAIL and TEL properties", async () => { const fixture = await createDavFixture(["carddav.full"]); const contactUid = uid(); const grouped = [ "BEGIN:VCARD", "VERSION:4.0", `UID:${contactUid}`, "FN:Grouped Contact", "N:Contact;Grouped;;;", "item1.EMAIL:grouped@example.test", "item2.TEL:+15555550100", "END:VCARD", "", ].join("\r\n"); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/grouped.vcf", { method: "PUT", body: grouped, }), ), ).toMatchObject({ status: 201 }); const emailQuery = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-query", 'grouped@example', ), }), ); expect(emailQuery.status).toBe(207); expect(await emailQuery.text()).toContain("/addressbooks/default/grouped.vcf"); const telQuery = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-query", '5550100', ), }), ); expect(telQuery.status).toBe(207); expect(await telQuery.text()).toContain("/addressbooks/default/grouped.vcf"); }); it("does not return address-data from PROPFIND but still returns it from REPORT", async () => { const fixture = await createDavFixture(["carddav.full"]); const contactUid = uid(); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/no-propfind-data.vcf", { method: "PUT", body: vcard({ uid: contactUid, fn: "No PROPFIND Data" }), }), ), ).toMatchObject({ status: 201 }); const propfind = await fetchWorker( davRequest(fixture, "/addressbooks/default/no-propfind-data.vcf", { method: "PROPFIND", headers: { depth: "0", "content-type": "application/xml" }, body: propfindBody(""), }), ); expect(propfind.status).toBe(207); expect(await propfind.text()).toContain("HTTP/1.1 404 Not Found"); const report = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody("addressbook-query", ""), }), ); expect(report.status).toBe(207); expect(await report.text()).toContain(`UID:${contactUid}`); }); it("keeps addressbook-query Depth scoped and treats missing Depth as compatibility Depth 1", async () => { const fixture = await createDavFixture(["carddav.full"]); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/depth.vcf", { method: "PUT", body: vcard({ uid: uid(), fn: "Depth Query" }), }), ), ).toMatchObject({ status: 201 }); const body = reportBody("addressbook-query", ""); const missingDepth = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body, }), ); expect(missingDepth.status).toBe(207); expect(await missingDepth.text()).toContain("/addressbooks/default/depth.vcf"); const depth0 = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml", depth: "0" }, body, }), ); expect(depth0.status).toBe(207); expect(await depth0.text()).not.toContain("/addressbooks/default/depth.vcf"); }); it("rejects REPORT elements from the wrong namespace and returns per-property statuses", async () => { const fixture = await createDavFixture(["carddav.full"]); const wrongNamespace = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: '', }), ); expect(wrongNamespace.status).toBe(400); expect( await fetchWorker( davRequest(fixture, "/addressbooks/default/unknown-prop.vcf", { method: "PUT", body: vcard({ uid: uid(), fn: "Unknown Prop" }), }), ), ).toMatchObject({ status: 201 }); const unknownProp = await fetchWorker( davRequest(fixture, "/addressbooks/default/", { method: "REPORT", headers: { "content-type": "application/xml" }, body: reportBody( "addressbook-query", '', ), }), ); expect(unknownProp.status).toBe(207); const xml = await unknownProp.text(); expect(xml).toContain(""); expect(xml).toContain("HTTP/1.1 404 Not Found"); expect(xml).toContain("unknown"); }); });