Read all environment config through ProTacts::Config
ProTacts::Config is now the single place that reads ENV; Sentry init and the debug-logging toggle go through ProTacts.config instead of touching the environment directly.

Assisted-by: GLM 5.2 via pi
change ttlsrouyrvspssoolooulvnwowovrwxs
commit 3c497fed9bda19fb3084e253fe4a9e39208c308f
author Alpha Chen <alpha@kejadlen.dev>
date
parent qtwvorrv
diff --git a/lib/pro_tacts.rb b/lib/pro_tacts.rb
index b6f7e50..882af77 100644
--- a/lib/pro_tacts.rb
+++ b/lib/pro_tacts.rb
@@ -1,11 +1,13 @@
 # frozen_string_literal: true
 
+require "pro_tacts/config"
+
 module ProTacts
-  # Whether debug logging is on. When true, every request and response is
-  # dumped in full — headers and bodies on both sides. Off by default because
-  # it logs contact data; see ProTacts::DebugLogger.
-  def self.debug_logging?
-    value = ENV["PRO_TACTS_DEBUG"]
-    !value.nil? && value.match?(/\A(1|true|yes)\z/i)
+  class << self
+    def config
+      @config ||= Config.new
+    end
+
+    attr_writer :config
   end
 end
diff --git a/lib/pro_tacts/config.rb b/lib/pro_tacts/config.rb
new file mode 100644
index 0000000..09909bb
--- /dev/null
+++ b/lib/pro_tacts/config.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ProTacts
+  # Single source of truth for configuration read from the environment.
+  # Nothing else in the app should read ENV directly; add a method here and
+  # read it through ProTacts.config instead.
+  class Config
+    TRUTHY = /\A(1|true|yes)\z/i
+
+    def initialize(env = ENV)
+      @env = env
+    end
+
+    # Sentry DSN. Required at boot; raises if unset so the failure is loud.
+    def sentry_dsn
+      @env.fetch("SENTRY_DSN")
+    end
+
+    # Whether to dump full request/response exchanges to the log. Off by
+    # default because it logs contact data. See ProTacts::DebugLogger.
+    def debug?
+      value = @env.fetch("PRO_TACTS_DEBUG", nil)
+      !value.nil? && value.match?(TRUTHY)
+    end
+  end
+end
diff --git a/lib/pro_tacts/web.rb b/lib/pro_tacts/web.rb
index fed393e..445a180 100644
--- a/lib/pro_tacts/web.rb
+++ b/lib/pro_tacts/web.rb
@@ -1,26 +1,25 @@
 # frozen_string_literal: true
 
+require "pro_tacts"
 require "sentry-ruby"
 
-Sentry.init do |config|
-  # TODO: Consolidate configuration
-  config.dsn = ENV.fetch("SENTRY_DSN")
+Sentry.init do |sentry|
+  sentry.dsn = ProTacts.config.sentry_dsn
 
   # Get breadcrumbs from logs
-  config.breadcrumbs_logger = [:sentry_logger, :http_logger]
+  sentry.breadcrumbs_logger = [:sentry_logger, :http_logger]
 
   # Add data like request headers and IP for users, if applicable;
   # see https://docs.sentry.io/platforms/ruby/data-management/data-collected/ for more info
-  config.send_default_pii = true
+  sentry.send_default_pii = true
 
   # Trace all the things!
-  config.traces_sample_rate = 1.0
+  sentry.traces_sample_rate = 1.0
 end
 
 require "rack/rewindable_input"
 require "roda"
 
-require "pro_tacts"
 require "pro_tacts/debug_logger"
 require "roda/plugins/dav_verbs"
 
@@ -30,7 +29,7 @@ module ProTacts
     # and then rewind it so the application can still access it.
     use Rack::RewindableInput::Middleware
     use Sentry::Rack::CaptureExceptions
-    use ProTacts::DebugLogger if ProTacts.debug_logging?
+    use ProTacts::DebugLogger if ProTacts.config.debug?
 
     plugin :all_verbs
     plugin :dav_verbs
diff --git a/test/pro_tacts/test_config.rb b/test/pro_tacts/test_config.rb
new file mode 100644
index 0000000..0faf130
--- /dev/null
+++ b/test/pro_tacts/test_config.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "minitest/autorun"
+
+require "pro_tacts/config"
+
+class ConfigTest < Minitest::Test
+  def test_sentry_dsn_is_passed_through
+    assert_equal "https://example/1", ProTacts::Config.new("SENTRY_DSN" => "https://example/1").sentry_dsn
+  end
+
+  def test_sentry_dsn_is_required
+    assert_raises(KeyError) { ProTacts::Config.new({}).sentry_dsn }
+  end
+
+  def test_debug_defaults_off
+    refute ProTacts::Config.new({}).debug?
+    refute ProTacts::Config.new("PRO_TACTS_DEBUG" => nil).debug?
+  end
+
+  def test_debug_turns_on_for_truthy_values
+    assert ProTacts::Config.new("PRO_TACTS_DEBUG" => "1").debug?
+    assert ProTacts::Config.new("PRO_TACTS_DEBUG" => "true").debug?
+    assert ProTacts::Config.new("PRO_TACTS_DEBUG" => "YES").debug?
+  end
+
+  def test_debug_ignores_other_values
+    refute ProTacts::Config.new("PRO_TACTS_DEBUG" => "no").debug?
+    refute ProTacts::Config.new("PRO_TACTS_DEBUG" => "0").debug?
+  end
+end
diff --git a/test/pro_tacts/test_debug_logger.rb b/test/pro_tacts/test_debug_logger.rb
index 377977e..e253a0a 100644
--- a/test/pro_tacts/test_debug_logger.rb
+++ b/test/pro_tacts/test_debug_logger.rb
@@ -3,7 +3,6 @@
 require "minitest/autorun"
 require "stringio"
 
-require "pro_tacts"
 require "pro_tacts/debug_logger"
 
 class DebugLoggerTest < Minitest::Test
@@ -82,34 +81,4 @@ class DebugLoggerTest < Minitest::Test
     assert_equal "text/xml", headers["Content-Type"]
     assert_equal ["<multistatus/>"], body
   end
-
-  class ToggleTest < Minitest::Test
-    def test_defaults_off_when_unset
-      with_env("PRO_TACTS_DEBUG" => nil) do
-        refute ProTacts.debug_logging?
-      end
-    end
-
-    def test_turns_on_with_truthy_values
-      with_env("PRO_TACTS_DEBUG" => "1") do
-        assert ProTacts.debug_logging?
-      end
-    end
-
-    def test_ignores_other_values
-      with_env("PRO_TACTS_DEBUG" => "no") do
-        refute ProTacts.debug_logging?
-      end
-    end
-
-    private
-
-    def with_env(vars)
-      saved = vars.keys.to_h { |k| [k, ENV[k]] }
-      vars.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
-      yield
-    ensure
-      saved.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
-    end
-  end
 end