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
change wruzpsmsynrtslmqzoplsnulwymztmnu
commit 6aa79ba26f3a7d7f385fb9ae433efbbcc5a08458
author Alpha Chen <alpha@kejadlen.dev>
date
parent mxuztkyy
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"]