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");
});
});