dav: build response bodies with a builder, not heredocs
The heredocs sent each CRLF raw, and a client's parser kept only the LF;
the builder writes CR as , so the card arrives as itself.
An element leaves only through a DavXml method, one per name;
the other fixtures only reindent — what they say is the same.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/AGENTS.md b/AGENTS.md
index a7d4adc..c73709b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -41,6 +41,8 @@ lib/pro_tacts/
│ # each first segment is a hash_branch in web/
├── web/dav.rb # Discovery and the address book: every DAV route
│ # and response body
+├── dav_xml.rb # The elements those bodies are written with, one
+│ # object per namespace
├── web/contacts.rb # The card browser and its editor
├── web/groups.rb # The group screens and their editor
├── web/setup.rb # The device setup screen and its profile
@@ -87,9 +89,10 @@ not a fixture; edit a `.vcf` to change what the replay serves.
## Gotchas
-- Response bodies are built with heredocs in `web/dav.rb`. A comment written
- inside one is sent to the client — keep notes about the code in Ruby
- comments outside the heredoc.
+- Response bodies in `web/dav.rb` are built with Nokogiri's builder through
+ `DavXml`, whose methods are the only elements the server can send. Inside
+ an element's block, `it` is the builder, not an enclosing block's value,
+ so name the outer block's parameter.
- Every request needs `Tailscale-User-Login` and `Tailscale-User-Name`
headers or it gets a 401, so a bare `curl` against `rake dev` is
refused until you pass both. The
diff --git a/lib/pro_tacts/dav_xml.rb b/lib/pro_tacts/dav_xml.rb
new file mode 100644
index 0000000..83e5933
--- /dev/null
+++ b/lib/pro_tacts/dav_xml.rb
@@ -0,0 +1,154 @@
+require "nokogiri"
+
+module ProTacts
+ # The XML the DAV routes answer with (web/dav.rb). Each namespace is an
+ # object whose methods are the elements this server sends in it, so an
+ # element outside them is a NoMethodError, and adding one is a change
+ # here — the real decision AGENTS.md says adding a property is.
+ module DavXml
+ PREFIXES = {
+ "d" => "DAV:",
+ "card" => "urn:ietf:params:xml:ns:carddav",
+ "cs" => "http://calendarserver.org/ns/",
+ }.freeze #: Hash[String, String]
+
+ # A whole body under the DAV: element root, declaring "d" and each of
+ # prefixes, with the children the block writes. Writing through a
+ # namespace left undeclared raises.
+ #: (String root, *String prefixes) { (DAV d, CardDAV card, CalendarServer cs) -> void } -> String
+ def self.document(root, *prefixes)
+ namespaces = ["d", *prefixes].to_h do |prefix|
+ ["xmlns:#{prefix}", PREFIXES.fetch(prefix)] #: [String, String]
+ end
+
+ Nokogiri::XML::Builder.new(encoding: "UTF-8") { |x|
+ x["d"].send(root, namespaces) do
+ yield DAV.new(x, "d"), CardDAV.new(x, "card"), CalendarServer.new(x, "cs")
+ end
+ }.to_xml
+ end
+
+ # One namespace's elements. The builder yields itself to an element's
+ # block, so `it` inside one is the builder rather than an enclosing
+ # block's element.
+ class Namespace
+ # @rbs @x: Nokogiri::XML::Builder
+ # @rbs @prefix: String
+
+ #: (Nokogiri::XML::Builder x, String prefix) -> void
+ def initialize(x, prefix)
+ @x = x
+ @prefix = prefix
+ end
+
+ private
+
+ #: (String name, ?String? text) ?{ () -> void } -> void
+ def tag(name, text = nil, &)
+ element = @x[@prefix]
+ text ? element.send(name, text, &) : element.send(name, &)
+ end
+ end
+
+ class DAV < Namespace
+ # A member answered with a 200, holding the properties the block
+ # writes.
+ #: (String location) { () -> void } -> void
+ def found(location, &)
+ response do
+ href location
+ propstat do
+ prop(&)
+ status "HTTP/1.1 200 OK"
+ end
+ end
+ end
+
+ # A member the collection does not answer for: the multiget miss,
+ # reported as a 404 inside the 207 rather than failing the request
+ # (RFC 6352 section 8.7), and the removal shape a sync-collection
+ # delta reports (RFC 6578 section 3.2 — href and 404, no propstat).
+ #: (String location) -> void
+ def missing(location)
+ response do
+ href location
+ status "HTTP/1.1 404 Not Found"
+ end
+ end
+
+ #: () { () -> void } -> void
+ def response(&) = tag("response", &)
+ #: () { () -> void } -> void
+ def propstat(&) = tag("propstat", &)
+ #: () { () -> void } -> void
+ def prop(&) = tag("prop", &)
+ #: (String location) -> void
+ def href(location) = tag("href", location)
+ #: (String line) -> void
+ def status(line) = tag("status", line)
+
+ #: (String etag) -> void
+ def getetag(etag) = tag("getetag", etag)
+ #: (String token) -> void
+ def sync_token(token) = tag("sync-token", token)
+ #: () { () -> void } -> void
+ def current_user_principal(&) = tag("current-user-principal", &)
+ #: () { () -> void } -> void
+ def resourcetype(&) = tag("resourcetype", &)
+ #: () -> void
+ def collection = tag("collection")
+
+ #: () { () -> void } -> void
+ def supported_report_set(&) = tag("supported-report-set", &)
+ # Also the precondition RFC 3253 section 3.6 names, empty.
+ #: () ?{ () -> void } -> void
+ def supported_report(&) = tag("supported-report", &)
+ #: () { () -> void } -> void
+ def report(&) = tag("report", &)
+ #: () -> void
+ def sync_collection = tag("sync-collection")
+ #: () -> void
+ def valid_sync_token = tag("valid-sync-token")
+
+ #: () { () -> void } -> void
+ def current_user_privilege_set(&) = tag("current-user-privilege-set", &)
+ #: () { () -> void } -> void
+ def privilege(&) = tag("privilege", &)
+ #: () -> void
+ def read = tag("read")
+ #: () -> void
+ def write = tag("write")
+ #: () -> void
+ def bind = tag("bind")
+ #: () -> void
+ def unbind = tag("unbind")
+ end
+
+ class CardDAV < Namespace
+ #: () { () -> void } -> void
+ def addressbook_home_set(&) = tag("addressbook-home-set", &)
+ #: () -> void
+ def addressbook = tag("addressbook")
+
+ # The card's carriage returns go out as character references: a raw
+ # CR in text is normalized away by the client's parser (XML 1.0
+ # section 2.11), which would hand it LF-only lines.
+ #: (String card) -> void
+ def address_data(card) = tag("address-data", card)
+
+ # The preconditions of RFC 6352 section 6.3.2.1, each holding what
+ # the block writes.
+ #: () ?{ () -> void } -> void
+ def supported_address_data(&) = tag("supported-address-data", &)
+ #: () ?{ () -> void } -> void
+ def valid_address_data(&) = tag("valid-address-data", &)
+ #: () ?{ () -> void } -> void
+ def no_uid_conflict(&) = tag("no-uid-conflict", &)
+ end
+
+ class CalendarServer < Namespace
+ #: (String ctag) -> void
+ def getctag(ctag) = tag("getctag", ctag)
+ end
+ end
+end
diff --git a/lib/pro_tacts/web/dav.rb b/lib/pro_tacts/web/dav.rb
index 685109a..59ad7ce 100644
--- a/lib/pro_tacts/web/dav.rb
+++ b/lib/pro_tacts/web/dav.rb
@@ -1,6 +1,8 @@
require "digest"
require "nokogiri"
+require "pro_tacts/dav_xml"
+
module ProTacts
# The CardDAV half of the router: service discovery and the one
# address book each user syncs (docs/plans/2026-09-12-per-user-books.md).
@@ -42,22 +44,11 @@ module ProTacts
response["Content-Type"] = "text/xml"
response.status = 207
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
- <d:response>
- <d:href>/dav/principal/</d:href>
- <d:propstat>
- <d:prop>
- <card:addressbook-home-set>
- <d:href>/dav/addressbook/</d:href>
- </card:addressbook-home-set>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
- </d:response>
- </d:multistatus>
- XML
+ multistatus("card") do |d, card|
+ d.found("/dav/principal/") do
+ card.addressbook_home_set { d.href "/dav/addressbook/" }
+ end
+ end
end
end
@@ -75,72 +66,57 @@ module ProTacts
etag_only = body.include?("getetag") && !body.include?("displayname") && !body.include?("resourcetype")
- # The collection self-entry is omitted from an etag-only ask
- # until a client is found to need it.
- collection_response = ""
- unless etag_only
- # Every property in this body and where it comes from:
- # DAV:resourcetype, which an address book collection MUST report
- # as both collection and addressbook (RFC 6352 section 5.2);
- # DAV:supported-report-set (RFC 3253 section 3.1.5), which RFC
- # 6578 section 3.2 requires list sync-collection;
- # DAV:sync-token (RFC 6578 section 4); and
- # DAV:current-user-privilege-set (RFC 3744 section 5.4), which
- # RFC 6352 section 7 requires of a CardDAV server. getctag alone
- # is not standardized — an Apple CalendarServer extension in the
- # calendarserver.org namespace, kept because macOS polls it.
- #
- # The privileges are advertised ahead of the methods
- # granting some of them. macOS Contacts asks for this
- # property on every poll and attempts no write without it,
- # so claiming them is what makes the client send writes at
- # all — the PUT they prompted was what log/unhandled
- # captured them for. DAV:write covers PUT and PROPPATCH
- # (RFC 3744 section 3.2), and PUT is the one of the pair
- # answered; DAV:unbind is removing a member from the
- # collection (section 3.10), which DELETE answers.
- # DAV:bind is adding one (section 3.9), and the create it
- # names — POST to the collection with DAV:add-member — has
- # no route: PUT to the member URI is how both clients
- # create, so nothing has asked for it.
- collection_response = <<~XML
- <d:response>
- <d:href>/dav/addressbook/</d:href>
- <d:propstat>
- <d:prop>
- <d:resourcetype>
- <d:collection/>
- <card:addressbook/>
- </d:resourcetype>
- <d:supported-report-set>
- <d:supported-report>
- <d:report><d:sync-collection/></d:report>
- </d:supported-report>
- </d:supported-report-set>
- <cs:getctag>#{ctag}</cs:getctag>
- <d:sync-token>#{sync_token}</d:sync-token>
- <d:current-user-privilege-set>
- <d:privilege><d:read/></d:privilege>
- <d:privilege><d:write/></d:privilege>
- <d:privilege><d:bind/></d:privilege>
- <d:privilege><d:unbind/></d:privilege>
- </d:current-user-privilege-set>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
- </d:response>
- XML
- end
-
- members = depth == "0" ? "" : contacts.map { etag_response(it) }.join
+ multistatus("card", "cs") do |d, card, cs|
+ # The collection self-entry is omitted from an etag-only ask
+ # until a client is found to need it.
+ unless etag_only
+ # Every property in this body and where it comes from:
+ # DAV:resourcetype, which an address book collection MUST report
+ # as both collection and addressbook (RFC 6352 section 5.2);
+ # DAV:supported-report-set (RFC 3253 section 3.1.5), which RFC
+ # 6578 section 3.2 requires list sync-collection;
+ # DAV:sync-token (RFC 6578 section 4); and
+ # DAV:current-user-privilege-set (RFC 3744 section 5.4), which
+ # RFC 6352 section 7 requires of a CardDAV server. getctag alone
+ # is not standardized — an Apple CalendarServer extension in the
+ # calendarserver.org namespace, kept because macOS polls it.
+ #
+ # The privileges are advertised ahead of the methods
+ # granting some of them. macOS Contacts asks for this
+ # property on every poll and attempts no write without it,
+ # so claiming them is what makes the client send writes at
+ # all — the PUT they prompted was what log/unhandled
+ # captured them for. DAV:write covers PUT and PROPPATCH
+ # (RFC 3744 section 3.2), and PUT is the one of the pair
+ # answered; DAV:unbind is removing a member from the
+ # collection (section 3.10), which DELETE answers.
+ # DAV:bind is adding one (section 3.9), and the create it
+ # names — POST to the collection with DAV:add-member — has
+ # no route: PUT to the member URI is how both clients
+ # create, so nothing has asked for it.
+ d.found("/dav/addressbook/") do
+ d.resourcetype do
+ d.collection
+ card.addressbook
+ end
+ d.supported_report_set do
+ d.supported_report do
+ d.report { d.sync_collection }
+ end
+ end
+ cs.getctag ctag
+ d.sync_token sync_token
+ d.current_user_privilege_set do
+ d.privilege { d.read }
+ d.privilege { d.write }
+ d.privilege { d.bind }
+ d.privilege { d.unbind }
+ end
+ end
+ end
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cs="http://calendarserver.org/ns/">
- #{collection_response}
- #{members}
- </d:multistatus>
- XML
+ contacts.each { etag_response(d, it) } unless depth == "0"
+ end
end
r.report do
@@ -193,15 +169,18 @@ module ProTacts
if token.empty?
# No token is the initial sync: every member, changed
# (section 3.4).
- multistatus(contacts.map { etag_response(it) }, sync_token:)
+ multistatus("card", sync_token:) do |d|
+ contacts.each { etag_response(d, it) }
+ end
elsif (sequence = token[%r{\Ahttp://pro-tacts/sync/(\d+)/#{book_digest}\z}, 1]) &&
sequence.to_i <= store.latest_sequence
net = store.changes(after: sequence.to_i).map { it.card_id }.uniq
- responses = net.map { |id|
- contact = contacts.find { it.id == id }
- contact ? etag_response(contact) : missing_response(contact_href(id))
- }
- multistatus(responses, sync_token:)
+ multistatus("card", sync_token:) do |d|
+ net.each do |id|
+ contact = contacts.find { it.id == id }
+ contact ? etag_response(d, contact) : d.missing(contact_href(id))
+ end
+ end
else
# A token this server never issued, one issued for another
# book, or one naming a state past the present: the
@@ -212,28 +191,25 @@ module ProTacts
# token names is gone.
response.status = 410
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <d:error xmlns:d="DAV:">
- <d:valid-sync-token/>
- </d:error>
- XML
+ DavXml.document("error") { |d| d.valid_sync_token }
end
when "addressbook-multiget"
# CARDDAV:addressbook-multiget (RFC 6352 section 8.7); the
# address-data the client asks for is section 10.4.
wants_cards = doc.xpath("//address-data").any?
- multistatus(doc.xpath("//href").map { it.text }.map { |requested|
- id = requested[%r{\A/dav/addressbook/([^/]+)\.vcf\z}, 1]
- contact = id && contacts.find { it.id == id }
+ multistatus("card") do |d, card|
+ doc.xpath("//href").map { it.text }.each do |requested|
+ id = requested[%r{\A/dav/addressbook/([^/]+)\.vcf\z}, 1]
+ contact = id && contacts.find { it.id == id }
- if contact
- wants_cards ? card_response(contact) : etag_response(contact)
- else
- missing_response(requested)
+ if contact
+ wants_cards ? card_response(d, card, contact) : etag_response(d, contact)
+ else
+ d.missing(requested)
+ end
end
- })
+ end
else
# The DAV:supported-report precondition on REPORT (RFC 3253
# section 3.6) — the report asked for has to be one the
@@ -250,12 +226,7 @@ module ProTacts
# macOS Contacts has never sent one.
response.status = 403
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <d:error xmlns:d="DAV:">
- <d:supported-report/>
- </d:error>
- XML
+ DavXml.document("error") { |d| d.supported_report }
end
end
@@ -307,22 +278,11 @@ module ProTacts
response["Content-Type"] = "text/xml"
response.status = 207
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <d:multistatus xmlns:d="DAV:">
- <d:response>
- <d:href>#{href}</d:href>
- <d:propstat>
- <d:prop>
- <d:current-user-principal>
- <d:href>/dav/principal/</d:href>
- </d:current-user-principal>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
- </d:response>
- </d:multistatus>
- XML
+ multistatus do |d|
+ d.found(href) do
+ d.current_user_principal { d.href "/dav/principal/" }
+ end
+ end
end
# Read once per request — Roda builds a fresh app instance for each
@@ -391,7 +351,7 @@ module ProTacts
# this server stores. Asked before the body is read because it is
# a question about the request, not about what the request
# carried.
- return precondition("supported-address-data") unless request.media_type == "text/vcard"
+ return precondition(:supported_address_data) unless request.media_type == "text/vcard"
# Decoding the body, both halves of it: Rack requires input in
# ASCII-8BIT and Rack::RewindableInput enforces it again, so the
@@ -407,7 +367,7 @@ module ProTacts
# Paths need no counterpart: Puma hands PATH_INFO over still
# percent-encoded, so an id off the wire is ASCII.
bytes = request.body.read.force_encoding(Encoding::UTF_8)
- return precondition("valid-address-data") unless bytes.valid_encoding?
+ return precondition(:valid_address_data) unless bytes.valid_encoding?
# CARDDAV:valid-address-data again, on the card this time: the
# envelope RFC 2426 section 4 requires. That is the whole test. A
@@ -416,7 +376,7 @@ module ProTacts
# what it does not understand, and it is kept — the stored bytes
# are what goes back out.
vcard = VCard.new(bytes)
- return precondition("valid-address-data") unless vcard.card?
+ return precondition(:valid_address_data) unless vcard.card?
# CARDDAV:no-uid-conflict: the submitted UID must not belong to a
# different resource, and a mapped URI must not be overwritten by
@@ -427,7 +387,7 @@ module ProTacts
# the first clause — report where the UID already lives.
uid = vcard.uid
owner = uid && store.card_id_with_uid(uid)
- return precondition("no-uid-conflict", owner) if uid != id || owner && owner != id
+ return precondition(:no_uid_conflict, owner) if uid != id || owner && owner != id
existing = store.contact(id)
@@ -489,7 +449,7 @@ module ProTacts
# The change-log entry Store#delete leaves in the same transaction
# is what a syncing client is told: sync-collection answers a
- # removed member as href plus 404 (#missing_response).
+ # removed member as href plus 404 (DavXml::DAV#missing).
store.delete(id)
response.status = 204
@@ -544,21 +504,15 @@ module ProTacts
# are RFC 6352 section 6.3.2.1's, qualified by the card: namespace
# the body declares. no-uid-conflict carries the conflicting card's
# href when there is one.
- #: (String element, ?String? conflict_id) -> String
+ #: (:supported_address_data | :valid_address_data | :no_uid_conflict element, ?String? conflict_id) -> String
def precondition(element, conflict_id = nil)
- named = if conflict_id
- "<card:#{element}><d:href>#{contact_href(conflict_id)}</d:href></card:#{element}>"
- else
- "<card:#{element}/>"
- end
response.status = 412
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <d:error xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
- #{named}
- </d:error>
- XML
+ DavXml.document("error", "card") do |d, card|
+ card.public_send(element) do
+ d.href contact_href(conflict_id) if conflict_id
+ end
+ end
end
# A bare 412: these failures are HTTP's own conditionals (RFC 7232),
@@ -593,15 +547,12 @@ module ProTacts
# A 207 body. The trailing DAV:sync-token is RFC 6578 section 3.2's
# — a sync-collection response MUST carry one naming the state the
# answer reaches; no other multistatus here has a state to name.
- #: (Array[String] responses, ?sync_token: String) -> String
- def multistatus(responses, sync_token: nil)
- trailing = sync_token ? " <d:sync-token>#{sync_token}</d:sync-token>\n" : ""
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
- #{responses.join}#{trailing}
- </d:multistatus>
- XML
+ #: (*String prefixes, ?sync_token: String?) { (DavXml::DAV d, DavXml::CardDAV card, DavXml::CalendarServer cs) -> void } -> String
+ def multistatus(*prefixes, sync_token: nil)
+ DavXml.document("multistatus", *prefixes) do |d, card, cs|
+ yield d, card, cs
+ d.sync_token sync_token if sync_token
+ end
end
#: (String id) -> String
@@ -610,56 +561,17 @@ module ProTacts
end
# DAV:getetag (RFC 4918 section 15.6).
- #: (Contact contact) -> String
- def etag_response(contact)
- <<~XML
- <d:response>
- <d:href>#{contact_href(contact.id)}</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>#{contact.etag}</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
- </d:response>
- XML
+ #: (DavXml::DAV d, Contact contact) -> void
+ def etag_response(d, contact)
+ d.found(contact_href(contact.id)) { d.getetag contact.etag }
end
- #: (Contact contact) -> String
- def card_response(contact)
- <<~XML
- <d:response>
- <d:href>#{contact_href(contact.id)}</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>#{contact.etag}</d:getetag>
- <card:address-data>#{xml_escape(contact.vcard.to_s.chomp)}</card:address-data>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
- </d:response>
- XML
- end
-
- # A member the collection does not answer for: the multiget miss,
- # reported as a 404 inside the 207 rather than failing the request
- # (RFC 6352 section 8.7), and the removal shape a sync-collection
- # delta reports (RFC 6578 section 3.2 — href and 404, no propstat).
- #: (String requested) -> String
- def missing_response(requested)
- <<~XML
- <d:response>
- <d:href>#{xml_escape(requested)}</d:href>
- <d:status>HTTP/1.1 404 Not Found</d:status>
- </d:response>
- XML
- end
-
- # Text nodes in XML built by interpolation; hrefs and vCard content
- # can all contain &, <, or >.
- #: (String text) -> String
- def xml_escape(text)
- text.gsub(/[&<>]/, "&" => "&", "<" => "<", ">" => ">")
+ #: (DavXml::DAV d, DavXml::CardDAV card, Contact contact) -> void
+ def card_response(d, card, contact)
+ d.found(contact_href(contact.id)) do
+ d.getetag contact.etag
+ card.address_data contact.vcard.to_s.chomp
+ end
end
end
end
diff --git a/sig/gems/nokogiri.rbs b/sig/gems/nokogiri.rbs
index b7798cf..6aa933b 100644
--- a/sig/gems/nokogiri.rbs
+++ b/sig/gems/nokogiri.rbs
@@ -1,5 +1,8 @@
# The nokogiri surface pro-tacts uses to read REPORT bodies: parse, drop
-# the namespaces, and pull elements out by xpath.
+# the namespaces, and pull elements out by xpath. And to write the DAV
+# bodies, through a Builder whose elements are method_missing calls,
+# which DavXml::Namespace#tag makes with send so nothing else needs
+# declaring.
#
# `root` is declared non-nil because Nokogiri::XML only returns a
# document without one for input that is not XML at all, and web.rb
@@ -24,5 +27,11 @@ module Nokogiri
def each: () { (Node) -> void } -> void
end
+
+ class Builder
+ def self.new: (?encoding: String) { (Builder) -> void } -> Builder
+ def []: (String prefix) -> Builder
+ def to_xml: () -> String
+ end
end
end
diff --git a/test/fixtures/ios-exchange/02-propfind-addressbook-ctag/response b/test/fixtures/ios-exchange/02-propfind-addressbook-ctag/response
index d298041..3dbff83 100644
--- a/test/fixtures/ios-exchange/02-propfind-addressbook-ctag/response
+++ b/test/fixtures/ios-exchange/02-propfind-addressbook-ctag/response
@@ -4,30 +4,38 @@ Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cs="http://calendarserver.org/ns/">
<d:response>
- <d:href>/dav/addressbook/</d:href>
- <d:propstat>
- <d:prop>
- <d:resourcetype>
- <d:collection/>
- <card:addressbook/>
- </d:resourcetype>
- <d:supported-report-set>
- <d:supported-report>
- <d:report><d:sync-collection/></d:report>
- </d:supported-report>
- </d:supported-report-set>
- <cs:getctag>19</cs:getctag>
- <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
- <d:current-user-privilege-set>
- <d:privilege><d:read/></d:privilege>
- <d:privilege><d:write/></d:privilege>
- <d:privilege><d:bind/></d:privilege>
- <d:privilege><d:unbind/></d:privilege>
- </d:current-user-privilege-set>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
-
+ <d:href>/dav/addressbook/</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:resourcetype>
+ <d:collection/>
+ <card:addressbook/>
+ </d:resourcetype>
+ <d:supported-report-set>
+ <d:supported-report>
+ <d:report>
+ <d:sync-collection/>
+ </d:report>
+ </d:supported-report>
+ </d:supported-report-set>
+ <cs:getctag>19</cs:getctag>
+ <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
+ <d:current-user-privilege-set>
+ <d:privilege>
+ <d:read/>
+ </d:privilege>
+ <d:privilege>
+ <d:write/>
+ </d:privilege>
+ <d:privilege>
+ <d:bind/>
+ </d:privilege>
+ <d:privilege>
+ <d:unbind/>
+ </d:privilege>
+ </d:current-user-privilege-set>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
</d:multistatus>
diff --git a/test/fixtures/ios-exchange/03-propfind-addressbook-bootstrap/response b/test/fixtures/ios-exchange/03-propfind-addressbook-bootstrap/response
index ee3f601..55254af 100644
--- a/test/fixtures/ios-exchange/03-propfind-addressbook-bootstrap/response
+++ b/test/fixtures/ios-exchange/03-propfind-addressbook-bootstrap/response
@@ -4,192 +4,200 @@ Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cs="http://calendarserver.org/ns/">
<d:response>
- <d:href>/dav/addressbook/</d:href>
- <d:propstat>
- <d:prop>
- <d:resourcetype>
- <d:collection/>
- <card:addressbook/>
- </d:resourcetype>
- <d:supported-report-set>
- <d:supported-report>
- <d:report><d:sync-collection/></d:report>
- </d:supported-report>
- </d:supported-report-set>
- <cs:getctag>19</cs:getctag>
- <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
- <d:current-user-privilege-set>
- <d:privilege><d:read/></d:privilege>
- <d:privilege><d:write/></d:privilege>
- <d:privilege><d:bind/></d:privilege>
- <d:privilege><d:unbind/></d:privilege>
- </d:current-user-privilege-set>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
+ <d:href>/dav/addressbook/</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:resourcetype>
+ <d:collection/>
+ <card:addressbook/>
+ </d:resourcetype>
+ <d:supported-report-set>
+ <d:supported-report>
+ <d:report>
+ <d:sync-collection/>
+ </d:report>
+ </d:supported-report>
+ </d:supported-report-set>
+ <cs:getctag>19</cs:getctag>
+ <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
+ <d:current-user-privilege-set>
+ <d:privilege>
+ <d:read/>
+ </d:privilege>
+ <d:privilege>
+ <d:write/>
+ </d:privilege>
+ <d:privilege>
+ <d:bind/>
+ </d:privilege>
+ <d:privilege>
+ <d:unbind/>
+ </d:privilege>
+ </d:current-user-privilege-set>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
<d:response>
- <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/D0504597-CF12-4AF6-9894-43724366D39D.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"6ce139775700c82011936b1958b6e081d62f27c711931804ecac2a1e5bb6e8f9"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-basic-noyear.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"81f0da34c506a6c92460dda5bf490dc4282cdf83c8b5d0b99bc6f4348f2bbd7b"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-complete.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"4f18a47439816f1488e1bea6ddd31b26dd098de0e5245e57e70e32944be12804"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-datetime.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"e05100ccdcc77884ab0cd415544df3be9caaed41b37cf6f15b1675462e92bc14"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-day-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"63611249675e47a061f15a77ec97c1c3c370915f6772aeb827a9dbc8b5223d2a"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-extended-noyear.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"6dca6844193e0f29ff9b8ef6051bbe6d049d0d649467404edaa84b2abea3f6e9"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-month-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"c6fdf9e92e169a012b5afedb252156de2e447a31f4ad2e9a2956504111203776"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-omit-year.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"fe423fff45d51c9c49cc25d667a9fd2ee3f9f53dbb2be12acad15f1088b5c2a0"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-year-month.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"5444c5bd0fad1dd3bccb1111f52590d329078e28d9003d1c65fd0ba2f7544ec7"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-year-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"f8d1a272129107f5575cc0f3b186a4c830ae5d49294df31f46a80c42f5840772"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/emoji.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"f2de8c9cbbc34e304510ff6f076ed3275e620802bc15731550b5d43b0d0ae994"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-alicia.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"ca751ac9f3b3be48e95809594bfcfcce2774fac924698e4e6fdf342645df2af8"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-george.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"89143354379038718238e5f146f71535724c00cc28d9f21add2f864e62b3953e"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-mary.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"a94d2aeb791106372057f2759864f46ea78e1fe410cacbe15026b303e1bb174f"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/memoji.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"bb0734ee418c5bb60047503ea7675ccb46151d8e0084af665ba628bfc4acd804"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"e63c84519663074659f6cd80ba7c4de31fe7c72c87736cbf630619bb0448d1cd"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/photo.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"9bd93308c95dc178599a5102bdc6f194f8cb02f838262f421fbe7e2d7c1ab281"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
+ <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/D0504597-CF12-4AF6-9894-43724366D39D.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"6ce139775700c82011936b1958b6e081d62f27c711931804ecac2a1e5bb6e8f9"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-basic-noyear.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"81f0da34c506a6c92460dda5bf490dc4282cdf83c8b5d0b99bc6f4348f2bbd7b"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-complete.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"4f18a47439816f1488e1bea6ddd31b26dd098de0e5245e57e70e32944be12804"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-datetime.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"e05100ccdcc77884ab0cd415544df3be9caaed41b37cf6f15b1675462e92bc14"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-day-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"63611249675e47a061f15a77ec97c1c3c370915f6772aeb827a9dbc8b5223d2a"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-extended-noyear.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"6dca6844193e0f29ff9b8ef6051bbe6d049d0d649467404edaa84b2abea3f6e9"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-month-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"c6fdf9e92e169a012b5afedb252156de2e447a31f4ad2e9a2956504111203776"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-omit-year.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"fe423fff45d51c9c49cc25d667a9fd2ee3f9f53dbb2be12acad15f1088b5c2a0"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-year-month.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"5444c5bd0fad1dd3bccb1111f52590d329078e28d9003d1c65fd0ba2f7544ec7"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-year-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"f8d1a272129107f5575cc0f3b186a4c830ae5d49294df31f46a80c42f5840772"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/emoji.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"f2de8c9cbbc34e304510ff6f076ed3275e620802bc15731550b5d43b0d0ae994"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-alicia.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"ca751ac9f3b3be48e95809594bfcfcce2774fac924698e4e6fdf342645df2af8"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-george.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"89143354379038718238e5f146f71535724c00cc28d9f21add2f864e62b3953e"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-mary.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"a94d2aeb791106372057f2759864f46ea78e1fe410cacbe15026b303e1bb174f"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/memoji.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"bb0734ee418c5bb60047503ea7675ccb46151d8e0084af665ba628bfc4acd804"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"e63c84519663074659f6cd80ba7c4de31fe7c72c87736cbf630619bb0448d1cd"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/photo.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"9bd93308c95dc178599a5102bdc6f194f8cb02f838262f421fbe7e2d7c1ab281"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
</d:multistatus>
diff --git a/test/fixtures/ios-exchange/04-report-multiget/response b/test/fixtures/ios-exchange/04-report-multiget/response
index a023f27..ea0f3c3 100644
--- a/test/fixtures/ios-exchange/04-report-multiget/response
+++ b/test/fixtures/ios-exchange/04-report-multiget/response
@@ -4,19 +4,18 @@ Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
<d:response>
- <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
- <card:address-data>BEGIN:VCARD
-VERSION:3.0
-N:Contact;Test;;;
-FN:Test Contact
-UID:AB12C345-6789-0DEF-1234-567890ABCDEF
+ <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
+ <card:address-data>BEGIN:VCARD
+VERSION:3.0
+N:Contact;Test;;;
+FN:Test Contact
+UID:AB12C345-6789-0DEF-1234-567890ABCDEF
END:VCARD</card:address-data>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
</d:multistatus>
diff --git a/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response b/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response
index d298041..3dbff83 100644
--- a/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response
+++ b/test/fixtures/macos-exchange/05-propfind-addressbook-ctag/response
@@ -4,30 +4,38 @@ Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cs="http://calendarserver.org/ns/">
<d:response>
- <d:href>/dav/addressbook/</d:href>
- <d:propstat>
- <d:prop>
- <d:resourcetype>
- <d:collection/>
- <card:addressbook/>
- </d:resourcetype>
- <d:supported-report-set>
- <d:supported-report>
- <d:report><d:sync-collection/></d:report>
- </d:supported-report>
- </d:supported-report-set>
- <cs:getctag>19</cs:getctag>
- <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
- <d:current-user-privilege-set>
- <d:privilege><d:read/></d:privilege>
- <d:privilege><d:write/></d:privilege>
- <d:privilege><d:bind/></d:privilege>
- <d:privilege><d:unbind/></d:privilege>
- </d:current-user-privilege-set>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
-
+ <d:href>/dav/addressbook/</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:resourcetype>
+ <d:collection/>
+ <card:addressbook/>
+ </d:resourcetype>
+ <d:supported-report-set>
+ <d:supported-report>
+ <d:report>
+ <d:sync-collection/>
+ </d:report>
+ </d:supported-report>
+ </d:supported-report-set>
+ <cs:getctag>19</cs:getctag>
+ <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
+ <d:current-user-privilege-set>
+ <d:privilege>
+ <d:read/>
+ </d:privilege>
+ <d:privilege>
+ <d:write/>
+ </d:privilege>
+ <d:privilege>
+ <d:bind/>
+ </d:privilege>
+ <d:privilege>
+ <d:unbind/>
+ </d:privilege>
+ </d:current-user-privilege-set>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
</d:multistatus>
diff --git a/test/fixtures/macos-exchange/06-propfind-addressbook-listing/response b/test/fixtures/macos-exchange/06-propfind-addressbook-listing/response
index 9e9de80..8a5c4df 100644
--- a/test/fixtures/macos-exchange/06-propfind-addressbook-listing/response
+++ b/test/fixtures/macos-exchange/06-propfind-addressbook-listing/response
@@ -3,168 +3,166 @@ Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cs="http://calendarserver.org/ns/">
-
- <d:response>
- <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/D0504597-CF12-4AF6-9894-43724366D39D.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"6ce139775700c82011936b1958b6e081d62f27c711931804ecac2a1e5bb6e8f9"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-basic-noyear.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"81f0da34c506a6c92460dda5bf490dc4282cdf83c8b5d0b99bc6f4348f2bbd7b"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-complete.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"4f18a47439816f1488e1bea6ddd31b26dd098de0e5245e57e70e32944be12804"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-datetime.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"e05100ccdcc77884ab0cd415544df3be9caaed41b37cf6f15b1675462e92bc14"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-day-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"63611249675e47a061f15a77ec97c1c3c370915f6772aeb827a9dbc8b5223d2a"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-extended-noyear.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"6dca6844193e0f29ff9b8ef6051bbe6d049d0d649467404edaa84b2abea3f6e9"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-month-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"c6fdf9e92e169a012b5afedb252156de2e447a31f4ad2e9a2956504111203776"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-omit-year.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"fe423fff45d51c9c49cc25d667a9fd2ee3f9f53dbb2be12acad15f1088b5c2a0"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-year-month.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"5444c5bd0fad1dd3bccb1111f52590d329078e28d9003d1c65fd0ba2f7544ec7"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-year-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"f8d1a272129107f5575cc0f3b186a4c830ae5d49294df31f46a80c42f5840772"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/emoji.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"f2de8c9cbbc34e304510ff6f076ed3275e620802bc15731550b5d43b0d0ae994"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-alicia.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"ca751ac9f3b3be48e95809594bfcfcce2774fac924698e4e6fdf342645df2af8"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-george.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"89143354379038718238e5f146f71535724c00cc28d9f21add2f864e62b3953e"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-mary.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"a94d2aeb791106372057f2759864f46ea78e1fe410cacbe15026b303e1bb174f"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/memoji.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"bb0734ee418c5bb60047503ea7675ccb46151d8e0084af665ba628bfc4acd804"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"e63c84519663074659f6cd80ba7c4de31fe7c72c87736cbf630619bb0448d1cd"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/photo.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"9bd93308c95dc178599a5102bdc6f194f8cb02f838262f421fbe7e2d7c1ab281"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
+ <d:response>
+ <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/D0504597-CF12-4AF6-9894-43724366D39D.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"6ce139775700c82011936b1958b6e081d62f27c711931804ecac2a1e5bb6e8f9"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-basic-noyear.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"81f0da34c506a6c92460dda5bf490dc4282cdf83c8b5d0b99bc6f4348f2bbd7b"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-complete.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"4f18a47439816f1488e1bea6ddd31b26dd098de0e5245e57e70e32944be12804"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-datetime.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"e05100ccdcc77884ab0cd415544df3be9caaed41b37cf6f15b1675462e92bc14"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-day-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"63611249675e47a061f15a77ec97c1c3c370915f6772aeb827a9dbc8b5223d2a"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-extended-noyear.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"6dca6844193e0f29ff9b8ef6051bbe6d049d0d649467404edaa84b2abea3f6e9"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-month-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"c6fdf9e92e169a012b5afedb252156de2e447a31f4ad2e9a2956504111203776"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-omit-year.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"fe423fff45d51c9c49cc25d667a9fd2ee3f9f53dbb2be12acad15f1088b5c2a0"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-year-month.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"5444c5bd0fad1dd3bccb1111f52590d329078e28d9003d1c65fd0ba2f7544ec7"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-year-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"f8d1a272129107f5575cc0f3b186a4c830ae5d49294df31f46a80c42f5840772"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/emoji.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"f2de8c9cbbc34e304510ff6f076ed3275e620802bc15731550b5d43b0d0ae994"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-alicia.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"ca751ac9f3b3be48e95809594bfcfcce2774fac924698e4e6fdf342645df2af8"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-george.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"89143354379038718238e5f146f71535724c00cc28d9f21add2f864e62b3953e"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-mary.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"a94d2aeb791106372057f2759864f46ea78e1fe410cacbe15026b303e1bb174f"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/memoji.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"bb0734ee418c5bb60047503ea7675ccb46151d8e0084af665ba628bfc4acd804"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"e63c84519663074659f6cd80ba7c4de31fe7c72c87736cbf630619bb0448d1cd"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/photo.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"9bd93308c95dc178599a5102bdc6f194f8cb02f838262f421fbe7e2d7c1ab281"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
</d:multistatus>
diff --git a/test/fixtures/macos-exchange/07-report-multiget/response b/test/fixtures/macos-exchange/07-report-multiget/response
index a023f27..ea0f3c3 100644
--- a/test/fixtures/macos-exchange/07-report-multiget/response
+++ b/test/fixtures/macos-exchange/07-report-multiget/response
@@ -4,19 +4,18 @@ Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
<d:response>
- <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
- <card:address-data>BEGIN:VCARD
-VERSION:3.0
-N:Contact;Test;;;
-FN:Test Contact
-UID:AB12C345-6789-0DEF-1234-567890ABCDEF
+ <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
+ <card:address-data>BEGIN:VCARD
+VERSION:3.0
+N:Contact;Test;;;
+FN:Test Contact
+UID:AB12C345-6789-0DEF-1234-567890ABCDEF
END:VCARD</card:address-data>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
</d:multistatus>
diff --git a/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response b/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response
index ee3f601..55254af 100644
--- a/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response
+++ b/test/fixtures/macos-exchange/09-propfind-addressbook-bootstrap/response
@@ -4,192 +4,200 @@ Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:cs="http://calendarserver.org/ns/">
<d:response>
- <d:href>/dav/addressbook/</d:href>
- <d:propstat>
- <d:prop>
- <d:resourcetype>
- <d:collection/>
- <card:addressbook/>
- </d:resourcetype>
- <d:supported-report-set>
- <d:supported-report>
- <d:report><d:sync-collection/></d:report>
- </d:supported-report>
- </d:supported-report-set>
- <cs:getctag>19</cs:getctag>
- <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
- <d:current-user-privilege-set>
- <d:privilege><d:read/></d:privilege>
- <d:privilege><d:write/></d:privilege>
- <d:privilege><d:bind/></d:privilege>
- <d:privilege><d:unbind/></d:privilege>
- </d:current-user-privilege-set>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
+ <d:href>/dav/addressbook/</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:resourcetype>
+ <d:collection/>
+ <card:addressbook/>
+ </d:resourcetype>
+ <d:supported-report-set>
+ <d:supported-report>
+ <d:report>
+ <d:sync-collection/>
+ </d:report>
+ </d:supported-report>
+ </d:supported-report-set>
+ <cs:getctag>19</cs:getctag>
+ <d:sync-token>http://pro-tacts/sync/19/c8dae6371b33b8d6</d:sync-token>
+ <d:current-user-privilege-set>
+ <d:privilege>
+ <d:read/>
+ </d:privilege>
+ <d:privilege>
+ <d:write/>
+ </d:privilege>
+ <d:privilege>
+ <d:bind/>
+ </d:privilege>
+ <d:privilege>
+ <d:unbind/>
+ </d:privilege>
+ </d:current-user-privilege-set>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
<d:response>
- <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/D0504597-CF12-4AF6-9894-43724366D39D.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"6ce139775700c82011936b1958b6e081d62f27c711931804ecac2a1e5bb6e8f9"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-basic-noyear.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"81f0da34c506a6c92460dda5bf490dc4282cdf83c8b5d0b99bc6f4348f2bbd7b"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-complete.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"4f18a47439816f1488e1bea6ddd31b26dd098de0e5245e57e70e32944be12804"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-datetime.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"e05100ccdcc77884ab0cd415544df3be9caaed41b37cf6f15b1675462e92bc14"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-day-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"63611249675e47a061f15a77ec97c1c3c370915f6772aeb827a9dbc8b5223d2a"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-extended-noyear.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"6dca6844193e0f29ff9b8ef6051bbe6d049d0d649467404edaa84b2abea3f6e9"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-month-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"c6fdf9e92e169a012b5afedb252156de2e447a31f4ad2e9a2956504111203776"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-omit-year.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"fe423fff45d51c9c49cc25d667a9fd2ee3f9f53dbb2be12acad15f1088b5c2a0"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-year-month.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"5444c5bd0fad1dd3bccb1111f52590d329078e28d9003d1c65fd0ba2f7544ec7"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/bday-year-only.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"f8d1a272129107f5575cc0f3b186a4c830ae5d49294df31f46a80c42f5840772"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/emoji.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"f2de8c9cbbc34e304510ff6f076ed3275e620802bc15731550b5d43b0d0ae994"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-alicia.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"ca751ac9f3b3be48e95809594bfcfcce2774fac924698e4e6fdf342645df2af8"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-george.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"89143354379038718238e5f146f71535724c00cc28d9f21add2f864e62b3953e"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/household-mary.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"a94d2aeb791106372057f2759864f46ea78e1fe410cacbe15026b303e1bb174f"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/memoji.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"bb0734ee418c5bb60047503ea7675ccb46151d8e0084af665ba628bfc4acd804"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"e63c84519663074659f6cd80ba7c4de31fe7c72c87736cbf630619bb0448d1cd"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-<d:response>
- <d:href>/dav/addressbook/photo.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"9bd93308c95dc178599a5102bdc6f194f8cb02f838262f421fbe7e2d7c1ab281"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
-</d:response>
-
+ <d:href>/dav/addressbook/AB12C345-6789-0DEF-1234-567890ABCDEF.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"ce8bef6ad9abb83e81da4d5aceb7999b43781726f148e2607ed853532f9b8db6"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/D0504597-CF12-4AF6-9894-43724366D39D.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"6ce139775700c82011936b1958b6e081d62f27c711931804ecac2a1e5bb6e8f9"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-basic-noyear.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"81f0da34c506a6c92460dda5bf490dc4282cdf83c8b5d0b99bc6f4348f2bbd7b"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-complete.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"4f18a47439816f1488e1bea6ddd31b26dd098de0e5245e57e70e32944be12804"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-datetime.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"e05100ccdcc77884ab0cd415544df3be9caaed41b37cf6f15b1675462e92bc14"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-day-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"63611249675e47a061f15a77ec97c1c3c370915f6772aeb827a9dbc8b5223d2a"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-extended-noyear.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"6dca6844193e0f29ff9b8ef6051bbe6d049d0d649467404edaa84b2abea3f6e9"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-month-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"c6fdf9e92e169a012b5afedb252156de2e447a31f4ad2e9a2956504111203776"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-omit-year.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"fe423fff45d51c9c49cc25d667a9fd2ee3f9f53dbb2be12acad15f1088b5c2a0"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-year-month.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"5444c5bd0fad1dd3bccb1111f52590d329078e28d9003d1c65fd0ba2f7544ec7"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/bday-year-only.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"f8d1a272129107f5575cc0f3b186a4c830ae5d49294df31f46a80c42f5840772"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/emoji.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"f2de8c9cbbc34e304510ff6f076ed3275e620802bc15731550b5d43b0d0ae994"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-alicia.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"ca751ac9f3b3be48e95809594bfcfcce2774fac924698e4e6fdf342645df2af8"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-george.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"89143354379038718238e5f146f71535724c00cc28d9f21add2f864e62b3953e"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/household-mary.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"a94d2aeb791106372057f2759864f46ea78e1fe410cacbe15026b303e1bb174f"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/memoji.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"bb0734ee418c5bb60047503ea7675ccb46151d8e0084af665ba628bfc4acd804"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/oqmvpzooywwmtwwx.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"e63c84519663074659f6cd80ba7c4de31fe7c72c87736cbf630619bb0448d1cd"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ <d:response>
+ <d:href>/dav/addressbook/photo.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"9bd93308c95dc178599a5102bdc6f194f8cb02f838262f421fbe7e2d7c1ab281"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
</d:multistatus>
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index 8bcd2c4..3a7fd67 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -718,8 +718,9 @@ class WebTest < Minitest::Test
request "/dav/addressbook/", method: "REPORT", input: multiget("new")
assert_equal 207, last_response.status
- served = last_response.body[%r{<card:address-data>(.*)</card:address-data>}m, 1]
- assert_equal card.chomp, xml_unescape(served)
+ served = Nokogiri::XML(last_response.body)
+ .at_xpath("//card:address-data", "card" => "urn:ietf:params:xml:ns:carddav").text
+ assert_equal card.chomp, served
end
end
@@ -773,12 +774,6 @@ class WebTest < Minitest::Test
end
end
- # The route's xml_escape, reversed, for reading a card back out of
- # a multiget body and comparing it with what was PUT.
- def xml_unescape(text)
- text.gsub("<", "<").gsub(">", ">").gsub("&", "&")
- end
-
# Contacts by id and name, each a bare card, and every one in `sync:*`
# so the requester's book is all of them. Seeded without a change-log
# entry, so the sequences the sync tests read are the puts' alone.