Raise on non-KDL files in the contacts directory
Dotfiles are exempt so Finder dropping a .DS_Store into the directory
cannot take the address book down; kdl files with unfetchable ids are
skipped with a Sentry capture.

Assisted-by: GLM-5.3 via pi
change muvpmtkzklqlnszwmmkxuznuwvztvrrw
commit 81b58186e73ed297f3b2d92710c7a72e4846528e
author Alpha Chen <alpha@kejadlen.dev>
date
parent oyykqzyw
diff --git a/lib/pro_tacts/contacts.rb b/lib/pro_tacts/contacts.rb
index 6f85def..1ac28d7 100644
--- a/lib/pro_tacts/contacts.rb
+++ b/lib/pro_tacts/contacts.rb
@@ -12,7 +12,8 @@ module ProTacts
     Contact = Data.define(:id, :vcard)
 
     # IDs end up in paths, and they arrive from client-supplied hrefs, so
-    # anything outside this charset simply does not exist.
+    # anything outside this charset simply does not exist. Enforced at
+    # load too, so everything listed is fetchable by that id.
     ID_FORMAT = /\A[\w-]+\z/
 
     attr_reader :directory
@@ -24,7 +25,19 @@ module ProTacts
       end
     end
 
+    # An empty directory is a valid empty address book. Anything in it
+    # that is not a .kdl file means a misplaced file or a wrong
+    # PRO_TACTS_CONTACTS_DIR, so it raises rather than quietly serving a
+    # partial address book. Dotfiles are exempt: Finder drops .DS_Store
+    # into any directory it opens.
     def all
+      unexpected = directory.children
+        .reject { it.basename.to_s.start_with?(".") }
+        .reject { it.extname == ".kdl" }
+      unless unexpected.empty?
+        raise ArgumentError, "unexpected non-KDL file in contacts directory: #{unexpected.first}"
+      end
+
       directory.glob("*.kdl").map { load(it) }.compact
     end
 
@@ -43,6 +56,10 @@ module ProTacts
     # so tests need no DSN.
     def load(path)
       id = path.basename(".kdl").to_s
+      unless id.match?(ID_FORMAT)
+        raise ArgumentError, "invalid contact id: #{id}"
+      end
+
       nodes = KDL.parse(path.read).nodes
       unless nodes.length == 1 && nodes.first.name == "contact"
         raise ArgumentError, "expected exactly one contact node"
diff --git a/test/pro_tacts/test_contacts.rb b/test/pro_tacts/test_contacts.rb
index 83b0923..d4f68f7 100644
--- a/test/pro_tacts/test_contacts.rb
+++ b/test/pro_tacts/test_contacts.rb
@@ -68,6 +68,36 @@ class ContactsTest < Minitest::Test
     end
   end
 
+  def test_non_kdl_files_raise
+    with_contacts({
+      "aiden.kdl" => "contact { name \"Aiden\" }",
+      "notes.txt" => "hello",
+    }) do |contacts|
+      error = assert_raises(ArgumentError) { contacts.all }
+
+      assert_match(/non-KDL file/, error.message)
+      assert_match(/notes\.txt/, error.message)
+    end
+  end
+
+  def test_dotfiles_are_ignored
+    with_contacts({
+      ".DS_Store" => "junk",
+      "aiden.kdl" => "contact { name \"Aiden\" }",
+    }) do |contacts|
+      assert_equal %w[aiden], contacts.all.map { it.id }
+    end
+  end
+
+  def test_files_whose_id_cannot_be_fetched_are_skipped
+    with_contacts({
+      "John Smith.kdl" => "contact { name \"John\" }",
+      "aiden.kdl" => "contact { name \"Aiden\" }",
+    }) do |contacts|
+      assert_equal %w[aiden], contacts.all.map { it.id }
+    end
+  end
+
   def test_unparseable_files_are_skipped
     with_contacts({
       "broken.kdl" => "contact {",