Implement CardDAV discovery endpoints
Assisted-by: Claude Opus 4.5 via Claude Code
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