Implement CardDAV discovery endpoints
Assisted-by: Claude Opus 4.5 via Claude Code
change myzspwxtnsqoowmzzxnoumwxmrslulrr
commit b0247fa5c305a3a285ce2b5a284ae8672e33c199
author Alpha Chen <alpha@kejadlen.dev>
date
parent xnnylwxp
diff --git a/TODO.md b/TODO.md
index b375833..aa1b9de 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,2 +1,4 @@
+- [ ] Authentication
 - [ ] Disable Sentry in tests
 - [ ] Add type checking
+- [ ] Hide warnings when requiring Sentry (CGI)
diff --git a/lib/pro_tacts/web.rb b/lib/pro_tacts/web.rb
index 1622e54..d1a67c5 100644
--- a/lib/pro_tacts/web.rb
+++ b/lib/pro_tacts/web.rb
@@ -3,6 +3,8 @@
 require "roda"
 require "sentry-ruby"
 
+require "roda/plugins/dav_verbs"
+
 Sentry.init do |config|
   # TODO: Consolidate configuration
   config.dsn = ENV.fetch("SENTRY_DSN")
@@ -20,6 +22,9 @@ end
 
 module ProTacts
   class Web < Roda
+    plugin :all_verbs
+    plugin :dav_verbs
+
     plugin :not_found do
       Sentry.capture_message("404 Not Found", level: :warning, extra: {
         path: request.path,
@@ -29,33 +34,37 @@ module ProTacts
     end
 
     route do |r|
-      # GET / request
-      r.root do
-        r.redirect "/hello"
+      r.options do
+        response["DAV"] = "1, 3, addressbook"
+        response["Allow"] = "OPTIONS, PROPFIND, REPORT"
+        ""
       end
 
-      # /hello branch
-      r.on "hello" do
-        # Set variable for all routes in /hello branch
-        @greeting = "Hello"
+      r.get ".well-known/carddav" do
+        r.redirect "/principal/", 301
+      end
 
-        # GET /hello/world request
-        r.get "world" do
-          "#{@greeting} world!"
-        end
+      r.on "principal" do
+        r.propfind do
+          response["Content-Type"] = "text/xml"
+          response.status = 207
 
-        # /hello request
-        r.is do
-          # GET /hello request
-          r.get do
-            "#{@greeting}!"
-          end
-
-          # POST /hello request
-          r.post do
-            puts "Someone said #{@greeting}!"
-            r.redirect
-          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>/principal/</d:href>
+                <d:propstat>
+                  <d:prop>
+                    <card:addressbook-home-set>
+                      <d:href>/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
     end
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index abfad25..7c7f7bb 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -12,10 +12,28 @@ class WebTest < Minitest::Test
     ProTacts::Web
   end
 
-  def test_hello
-    get "/hello"
+  def test_options_returns_dav_headers
+    options "/"
 
     assert_equal 200, last_response.status
-    assert_equal "Hello!", last_response.body
+    assert_equal "1, 3, addressbook", last_response["DAV"]
+    assert_includes last_response["Allow"], "OPTIONS"
+    assert_includes last_response["Allow"], "PROPFIND"
+  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"]
+  end
+
+  def test_propfind_principal
+    request "/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/"
   end
 end