Move CardDAV routes under /dav/ prefix
Root PROPFIND returns current-user-principal property instead of
redirecting, since CardDAV clients expect property responses.
Assisted-by: Claude Opus 4.5 via Claude Code
diff --git a/lib/pro_tacts/web.rb b/lib/pro_tacts/web.rb
index 9d67482..bd6ec4e 100644
--- a/lib/pro_tacts/web.rb
+++ b/lib/pro_tacts/web.rb
@@ -36,31 +36,27 @@ module ProTacts
end
route do |r|
- r.options do
- response["DAV"] = "1, 3, addressbook"
- response["Allow"] = "OPTIONS, PROPFIND, REPORT"
- ""
- end
-
- r.get ".well-known/carddav" do
- r.redirect "/principal/", 301
- end
-
- r.on "principal" do
+ r.is "" do
r.propfind do
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:multistatus xmlns:d="DAV:">
<d:response>
- <d:href>/principal/</d:href>
+ <d:href>/</d:href>
<d:propstat>
<d:prop>
- <card:addressbook-home-set>
- <d:href>/addressbook/</d:href>
- </card:addressbook-home-set>
+ <d:current-user-principal>
+ <d:href>/dav/principal/</d:href>
+ </d:current-user-principal>
+ <d:principal-URL>
+ <d:href>/dav/principal/</d:href>
+ </d:principal-URL>
+ <d:resourcetype>
+ <d:collection/>
+ </d:resourcetype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
@@ -70,37 +66,73 @@ module ProTacts
end
end
- r.on "addressbook" do
- r.propfind do
- response["Content-Type"] = "text/xml"
- response.status = 207
+ r.get ".well-known/carddav" do
+ r.redirect "/dav/principal/", 301
+ end
- <<~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>/addressbook/test-contact.vcf</d:href>
- <d:propstat>
- <d:prop>
- <d:getetag>"etag-123"</d:getetag>
- </d:prop>
- <d:status>HTTP/1.1 200 OK</d:status>
- </d:propstat>
- </d:response>
- </d:multistatus>
- XML
+ r.on "dav" do
+ r.options do
+ response["DAV"] = "1, 3, addressbook"
+ response["Allow"] = "OPTIONS, PROPFIND, REPORT"
+ ""
end
- r.get String do |uid|
- response["Content-Type"] = "text/vcard"
+ r.on "principal" do
+ r.propfind do
+ 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
+ end
+ end
- <<~VCARD
- BEGIN:VCARD
- VERSION:3.0
- FN:Test Contact
- N:Contact;Test;;;
- END:VCARD
- VCARD
+ r.on "addressbook" do
+ r.propfind do
+ 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/addressbook/test-contact.vcf</d:href>
+ <d:propstat>
+ <d:prop>
+ <d:getetag>"etag-123"</d:getetag>
+ </d:prop>
+ <d:status>HTTP/1.1 200 OK</d:status>
+ </d:propstat>
+ </d:response>
+ </d:multistatus>
+ XML
+ end
+
+ r.get String do |uid|
+ response["Content-Type"] = "text/vcard"
+
+ <<~VCARD
+ BEGIN:VCARD
+ VERSION:3.0
+ FN:Test Contact
+ N:Contact;Test;;;
+ END:VCARD
+ VCARD
+ end
end
end
end
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index 4a81471..00924a1 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -13,7 +13,7 @@ class WebTest < Minitest::Test
end
def test_options_returns_dav_headers
- options "/"
+ options "/dav/"
assert_equal 200, last_response.status
assert_equal "1, 3, addressbook", last_response["DAV"]
@@ -21,24 +21,33 @@ class WebTest < Minitest::Test
assert_includes last_response["Allow"], "PROPFIND"
end
+ def test_propfind_root
+ request "/", method: "PROPFIND"
+
+ assert_equal 207, last_response.status
+ assert_equal "text/xml", last_response["Content-Type"]
+ assert_includes last_response.body, "current-user-principal"
+ assert_includes last_response.body, "/dav/principal/"
+ end
+
def test_well_known_carddav_redirects_to_principal
get "/.well-known/carddav"
assert_equal 301, last_response.status
- assert_equal "/principal/", last_response["Location"]
+ assert_equal "/dav/principal/", last_response["Location"]
end
def test_propfind_principal
- request "/principal/", method: "PROPFIND"
+ request "/dav/principal/", method: "PROPFIND"
assert_equal 207, last_response.status
assert_equal "text/xml", last_response["Content-Type"]
assert_includes last_response.body, "multistatus"
- assert_includes last_response.body, "/addressbook/"
+ assert_includes last_response.body, "/dav/addressbook/"
end
def test_propfind_addressbook
- request "/addressbook/", method: "PROPFIND"
+ request "/dav/addressbook/", method: "PROPFIND"
assert_equal 207, last_response.status
assert_equal "text/xml", last_response["Content-Type"]
@@ -47,7 +56,7 @@ class WebTest < Minitest::Test
end
def test_get_contact
- get "/addressbook/test-contact.vcf"
+ get "/dav/addressbook/test-contact.vcf"
assert_equal 200, last_response.status
assert_equal "text/vcard", last_response["Content-Type"]