Make config.ru the composition root
Sentry.init, the access logger, and the contacts-dir mkdir all moved
from web.rb into config.ru, so requiring the app has no side effects
and tests need no DSN scrubbing or logger injection.
Assisted-by: GLM-5.3 via pi
diff --git a/Rakefile b/Rakefile
index 1d93de9..0ff7be3 100644
--- a/Rakefile
+++ b/Rakefile
@@ -19,11 +19,7 @@ desc "Regenerate macOS exchange response fixtures from current responses"
task :fixtures do
# Mirrors test/test_helper.rb, which cannot be required here without
# minitest/autorun running its at_exit hook inside rake.
- ENV.delete("SENTRY_DSN")
ENV["PRO_TACTS_DATA_DIR"] = (Pathname.new(__dir__) / "test/fixtures").to_s
- require "logger"
- require "pro_tacts"
- ProTacts.config.access_logger = Logger.new(IO::NULL)
require "pro_tacts/web"
require_relative "test/pro_tacts/exchange_fixtures"
ExchangeFixtures.record_responses(ProTacts::Web)
diff --git a/config.ru b/config.ru
index e86a66d..8fa4d0f 100644
--- a/config.ru
+++ b/config.ru
@@ -1,9 +1,35 @@
-
+# The composition root: everything environment-dependent (data
+# directories, Sentry, the access log) happens here rather than at
+# require time, so requiring the app has no side effects.
require "pathname"
+require "fileutils"
+require "sentry-ruby"
$LOAD_PATH.unshift(Pathname.new(__dir__) / "lib")
require "pro_tacts/web"
-ProTacts.ensure_data_directories
+config = ProTacts.config
+
+# A fresh checkout has no contacts dir; an empty address book beats a
+# 500 on every request.
+FileUtils.mkdir_p(config.contacts_dir)
+
+# A nil DSN initializes Sentry but leaves it inert: capture_message
+# returns nil and the rack middleware reports nothing.
+Sentry.init do |sentry|
+ sentry.dsn = config.sentry_dsn
+
+ # Get breadcrumbs from logs
+ 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
+ sentry.send_default_pii = true
+
+ # Trace all the things!
+ sentry.traces_sample_rate = 1.0
+end
+
+ProTacts::Web.plugin :common_logger, $stderr
run ProTacts::Web.freeze.app
diff --git a/lib/pro_tacts.rb b/lib/pro_tacts.rb
index 317deea..31381b8 100644
--- a/lib/pro_tacts.rb
+++ b/lib/pro_tacts.rb
@@ -1,6 +1,4 @@
-require "fileutils"
-
require "pro_tacts/config"
module ProTacts
@@ -10,12 +8,5 @@ module ProTacts
end
attr_writer :config
-
- # Called from config.ru rather than at require time, so loading the
- # app stays side-effect free. A fresh checkout has no contacts dir;
- # an empty address book beats a 500 on every request.
- def ensure_data_directories
- FileUtils.mkdir_p(config.contacts_dir)
- end
end
end
diff --git a/lib/pro_tacts/config.rb b/lib/pro_tacts/config.rb
index b85b7ca..e60c5bf 100644
--- a/lib/pro_tacts/config.rb
+++ b/lib/pro_tacts/config.rb
@@ -12,9 +12,9 @@ module ProTacts
@env = env
end
- # Sentry DSN; nil when unset. Presence is the switch for Sentry:
- # without it the app runs uninitialized, where capture_message and
- # the rack middleware are no-ops.
+ # Sentry DSN; nil when unset. A nil DSN is passed straight to
+ # Sentry.init, which leaves the client inert — capture_message and
+ # the rack middleware become no-ops.
def sentry_dsn
@env.fetch("SENTRY_DSN", nil)
end
@@ -39,10 +39,6 @@ module ProTacts
data_dir / "contacts"
end
- # Override for Roda's common_logger target; nil keeps the plugin's
- # stderr default. Tests swap in a null logger for quiet runs.
- attr_accessor :access_logger
-
# Where the debug logger writes. A path, overridable with
# PRO_TACTS_DEBUG_LOG; "stderr" keeps it on the process's stderr.
def debug_log_path
diff --git a/lib/pro_tacts/web.rb b/lib/pro_tacts/web.rb
index e83f671..1a22944 100644
--- a/lib/pro_tacts/web.rb
+++ b/lib/pro_tacts/web.rb
@@ -2,24 +2,6 @@
require "pro_tacts"
require "sentry-ruby"
-# Without a DSN, Sentry stays uninitialized and capture_message and
-# the rack middleware are no-ops, so there is nothing to skip in tests.
-if ProTacts.config.sentry_dsn
- Sentry.init do |sentry|
- sentry.dsn = ProTacts.config.sentry_dsn
-
- # Get breadcrumbs from logs
- 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
- sentry.send_default_pii = true
-
- # Trace all the things!
- sentry.traces_sample_rate = 1.0
- end
-end
-
require "rack/rewindable_input"
require "nokogiri"
require "roda"
@@ -41,7 +23,6 @@ module ProTacts
plugin :all_verbs
plugin :dav_verbs
- plugin :common_logger, ProTacts.config.access_logger
plugin :not_found do
Sentry.capture_message("404 Not Found", level: :warning)
diff --git a/test/pro_tacts/test_config.rb b/test/pro_tacts/test_config.rb
index 4437bb7..a28daaa 100644
--- a/test/pro_tacts/test_config.rb
+++ b/test/pro_tacts/test_config.rb
@@ -1,6 +1,4 @@
-require "logger"
-
require "minitest/autorun"
require "pro_tacts/config"
@@ -44,19 +42,6 @@ class ConfigTest < Minitest::Test
refute ProTacts::Config.new("PRO_TACTS_DEBUG" => "0").debug?
end
- def test_access_logger_defaults_to_nil
- assert_nil ProTacts::Config.new({}).access_logger
- end
-
- def test_access_logger_is_overridable
- config = ProTacts::Config.new({})
- null_logger = Logger.new(IO::NULL)
-
- config.access_logger = null_logger
-
- assert_same null_logger, config.access_logger
- end
-
def test_debug_log_path_defaults_to_a_file
assert_equal "log/debug.log", ProTacts::Config.new({}).debug_log_path
end
diff --git a/test/pro_tacts/test_config_ru.rb b/test/pro_tacts/test_config_ru.rb
new file mode 100644
index 0000000..1c58c2c
--- /dev/null
+++ b/test/pro_tacts/test_config_ru.rb
@@ -0,0 +1,14 @@
+require "test_helper"
+
+# Rack reads config.ru with the process's default external encoding,
+# which is US-ASCII under a C locale; any non-ASCII byte in the file
+# crashes boot (seen with rack 3.2.4). Ordinary .rb files are safe
+# because Ruby parses source as UTF-8 regardless of locale.
+class ConfigRuTest < Minitest::Test
+ def test_config_ru_is_ascii_only
+ config_ru = File.expand_path("../../config.ru", __dir__)
+
+ assert File.read(config_ru).ascii_only?,
+ "config.ru must stay ASCII-only or boot crashes under a C locale"
+ end
+end
diff --git a/test/pro_tacts/test_pro_tacts.rb b/test/pro_tacts/test_pro_tacts.rb
deleted file mode 100644
index fcf002b..0000000
--- a/test/pro_tacts/test_pro_tacts.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require "test_helper"
-
-require "tmpdir"
-
-class ProTactsTest < Minitest::Test
- def teardown
- # Reset the swapped-in config so later tests see the default one.
- ProTacts.config = nil
- end
-
- def test_ensure_data_directories_creates_a_nested_contacts_dir
- Dir.mktmpdir do |tmp|
- data_dir = File.join(tmp, "nested", "data")
- ProTacts.config = ProTacts::Config.new("PRO_TACTS_DATA_DIR" => data_dir)
-
- ProTacts.ensure_data_directories
-
- assert_path_exists File.join(data_dir, "contacts")
- end
- end
-
- def test_ensure_data_directories_is_idempotent
- Dir.mktmpdir do |tmp|
- data_dir = File.join(tmp, "data")
- ProTacts.config = ProTacts::Config.new("PRO_TACTS_DATA_DIR" => data_dir)
-
- 2.times { ProTacts.ensure_data_directories }
-
- assert_path_exists File.join(data_dir, "contacts")
- end
- end
-end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 2ef8f9e..eca876f 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,18 +1,10 @@
-# Drops an ambient SENTRY_DSN (e.g. exported by direnv) before anything
-# requires the app, so Sentry stays uninitialized here and the 404 tests
-# cannot ship events. The fixtures directory serves as the data directory:
-# its contacts/ holds the card the recorded macOS exchange asked for, so
-# the replay resolves the same hrefs the client did.
-ENV.delete("SENTRY_DSN")
-
+# The fixtures directory serves as the data directory: its contacts/
+# holds the card the recorded macOS exchange asked for, so the replay
+# resolves the same hrefs the client did. Nothing else is set up:
+# requiring the app has no side effects, and config.ru never runs here.
require "pathname"
ENV["PRO_TACTS_DATA_DIR"] = (Pathname.new(__dir__) / "fixtures").to_s
-# Silence the request log; web.rb reads this when it is required.
-require "logger"
-require "pro_tacts"
-ProTacts.config.access_logger = Logger.new(IO::NULL)
-
require "minitest/autorun"