tests: observe Sentry through the gem test helper
Redefining Sentry.capture_message as a singleton was monkey-patching,
and under -w it warned twice per use. setup_sentry_test records into
a dummy transport instead; setup re-opens the SDK because Sentry.init
registers at_exit { close }, which this runner fires before minitest
ever runs the tests.
Assisted-by: GLM-5.3 via pi
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index 7733a62..6a7e221 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -8,7 +8,8 @@ require "sequel"
require "pro_tacts/store"
class StoreTest < Minitest::Test
- include CapturingSentry
+ include Sentry::TestHelper
+ include SentryMessages
AIDEN = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Aiden\r\nUID:aiden\r\nEND:VCARD\r\n"
# UTC ISO 8601 to the millisecond, which is what SQLite is asked for.
@@ -28,6 +29,16 @@ class StoreTest < Minitest::Test
end
end
+ # The store's reports land in the transport this pins for the test;
+ # teardown clears it so nothing carries into the next one.
+ def setup
+ setup_sentry
+ end
+
+ def teardown
+ teardown_sentry_test
+ end
+
## Cards
def test_a_stored_card_comes_back_byte_for_byte
@@ -551,7 +562,8 @@ class StoreTest < Minitest::Test
shared = AIDEN.sub("END:VCARD\r\n", "BDAY:1985-04-12\rNOTE:b\r\nEND:VCARD\r\n")
with_store({"aiden" => AIDEN_BORN}) do |store|
- messages = capturing_sentry { store.put("aiden", shared) }
+ store.put("aiden", shared)
+ messages = sentry_messages
assert_equal shared, store.contact("aiden").vcard
assert_nil birthday_row(store, "aiden")
@@ -593,7 +605,8 @@ class StoreTest < Minitest::Test
edited = AIDEN.sub("FN:Aiden", "FN:Aiden Smith")
with_store({"aiden" => shared}) do |store|
- messages = capturing_sentry { store.put("aiden", edited) }
+ store.put("aiden", edited)
+ messages = sentry_messages
assert_equal edited, store.contact("aiden").vcard
assert_equal 1, messages.length
@@ -618,12 +631,13 @@ class StoreTest < Minitest::Test
# dropped, and nobody would know.
def test_a_rewrite_dropping_an_unrecognized_bday_is_reported
with_store({"aiden" => AIDEN.sub("END:VCARD\r\n", "BDAY:1985-13\r\nEND:VCARD\r\n")}) do |store|
- messages = capturing_sentry {
- store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
- }
+ # The seed's own arrival report is not this test's subject.
+ clear_sentry_events
+ store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
+ messages = sentry_messages
assert_equal 1, messages.length
- assert_match /BDAY/, messages.fetch(0)
+ assert_match(/BDAY/, messages.fetch(0))
end
end
@@ -633,11 +647,9 @@ class StoreTest < Minitest::Test
def test_a_rewrite_over_known_bdays_stays_quiet
["BDAY:--0412", "BDAY:1985-04"].each do |line|
with_store({"aiden" => AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n")}) do |store|
- messages = capturing_sentry {
- store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
- }
+ store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
- assert_empty messages, line
+ assert_empty sentry_messages, line
end
end
end
@@ -650,11 +662,10 @@ class StoreTest < Minitest::Test
def test_an_unrecognized_bday_arriving_is_reported
["BDAY:1985-13", "BDAY:--0432", "BDAY:19850412", "BDAY:1985-4"].each do |line|
with_store({}) do |store|
- messages = capturing_sentry {
- store.put("aiden", AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"))
- }
+ clear_sentry_events
+ store.put("aiden", AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"))
- assert_equal 1, messages.length, line
+ assert_equal 1, sentry_messages.length, line
end
end
end
@@ -663,11 +674,9 @@ class StoreTest < Minitest::Test
["BDAY:1985-04-12", "BDAY:1985-04-12T23:10:00Z", "BDAY;X-APPLE-OMIT-YEAR=1604:1604-04-12",
"BDAY:--0412", "BDAY:--04-12", "BDAY:1985-04", "BDAY:1985"].each do |line|
with_store({}) do |store|
- messages = capturing_sentry {
- store.put("aiden", AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"))
- }
+ store.put("aiden", AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"))
- assert_empty messages, line
+ assert_empty sentry_messages, line
end
end
end
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index 820ed03..16b098a 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -11,7 +11,8 @@ require "pro_tacts/store"
require "pro_tacts/web"
class WebTest < Minitest::Test
- include CapturingSentry
+ include Sentry::TestHelper
+ include SentryMessages
include Rack::Test::Methods
def app
@@ -19,11 +20,17 @@ class WebTest < Minitest::Test
end
# Every request needs a Tailscale identity; the middleware refuses without
- # one. Tests for that refusal are in TailscaleAuthTest.
+ # one. Tests for that refusal are in TailscaleAuthTest. The sentry setup
+ # pins the transport the app's reports land in; teardown clears it.
def setup
+ setup_sentry
header "Tailscale-User-Login", "test@example.com"
end
+ def teardown
+ teardown_sentry_test
+ end
+
def test_options_returns_dav_headers
options "/dav/"
@@ -409,9 +416,8 @@ class WebTest < Minitest::Test
packed = card("new", "New").sub("FN:New\r\n", "FN:New\rNOTE:b\r\n")
with_contacts({}) do
- messages = capturing_sentry {
- put_request "new", packed, "CONTENT_TYPE" => VCARD
- }
+ put_request "new", packed, "CONTENT_TYPE" => VCARD
+ messages = sentry_messages
assert_equal 201, last_response.status
assert_equal 1, messages.length
@@ -422,9 +428,8 @@ class WebTest < Minitest::Test
# Nothing else reports: an ordinary card arrives in silence.
def test_put_of_an_ordinary_card_is_quiet
with_contacts({}) do
- messages = capturing_sentry {
- put_request "new", card("new", "New"), "CONTENT_TYPE" => VCARD
- }
+ put_request "new", card("new", "New"), "CONTENT_TYPE" => VCARD
+ messages = sentry_messages
assert_equal 201, last_response.status
assert_empty messages
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 29cb674..7a291a3 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -44,30 +44,42 @@ File.truncate("log/test.log", 0) if File.exist?("log/test.log")
require_relative "fixture_data"
require "pro_tacts/web"
+require "sentry-ruby"
+require "sentry/test_helper"
+
+# Sentry is initialized here rather than left dormant because the gem's
+# test helper builds on an initialized SDK. The DSN is forced nil rather
+# than read from the environment, so a real SENTRY_DSN in this shell
+# cannot point a test run at a live project. init also registers
+# at_exit { close }, and the suite's runner requires minitest/autorun
+# before any of this loads — so Sentry's close fires before minitest's
+# at_exit runs the tests, and setup (SentryMessages#setup_sentry)
+# re-opens the SDK when it finds it closed. Before minitest/autorun
+# regardless, per the rule the header states, so direct single-file
+# runs — where this file is the entry — keep the close after the tests.
+Sentry.init { |sentry| sentry.dsn = nil }
+
require "minitest/autorun"
# Standing in for config.ru, which never runs here: build the store and
# hand it to the app.
ProTacts::Web.store = FixtureData.install(data_dir)
-# Sentry's capture_message, made observable for the length of a block and
-# restored after, so what one test captures cannot leak into the next.
-# Sentry is never initialized under test, so the original is inert
-# anyway — this only makes it observable. Returns what was captured.
-#
-# Here rather than in one test class because the reports are raised from
-# two layers: the web's PUT reports a broken parser assumption, the store
-# reports what its birthday model could not recompose.
-module CapturingSentry
- def capturing_sentry
- messages = []
- original = Sentry.method(:capture_message)
- Sentry.define_singleton_method(:capture_message) { |message, **| messages << message }
- begin
- yield messages
- ensure
- Sentry.define_singleton_method(:capture_message, original)
- end
- messages
+# The helper's other half: setup_sentry_test re-points the SDK at a
+# dummy DSN and a recording transport, so what the app reports is
+# observable in sentry_events without anything being sent and without
+# redefining Sentry.capture_message. This suite's assertions read
+# report texts, so the recorded Sentry::ErrorEvents also become the
+# message strings, in capture order. Shared by the two test classes
+# whose layers report — the web's PUT a broken parser assumption, the
+# store its birthday model what it cannot recompose.
+module SentryMessages
+ def setup_sentry
+ Sentry.init { |sentry| sentry.dsn = nil } unless Sentry.initialized?
+ setup_sentry_test
+ end
+
+ def sentry_messages
+ sentry_events.map { it.message }
end
end