Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions spec/current_attributes_reset_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require "rails_helper"

# Regression guard for test-order pollution via ActiveSupport::CurrentAttributes.
# The executor only auto-resets Current around real requests/jobs; a spec that
# sets Current in the test thread (controller/view/service specs) has nothing to
# clear it, so a stale value leaks into later examples and silently changes
# model behavior that branches on Current (Organization affiliation-lock
# validation, AhoyTrackable lifecycle tracking). rails_helper resets Current
# after every example β€” this proves it. order: :defined pins the two examples so
# the second reliably follows the first regardless of the suite seed.
RSpec.describe "Current attributes reset between examples", order: :defined do
it "leaves Current set within an example" do
Current.user = User.new
Current.source = "leak_probe"

expect(Current.source).to eq "leak_probe"
end

it "starts the next example with a clean Current" do
expect(Current.user).to be_nil
expect(Current.source).to be_nil
end
end
8 changes: 8 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
# each example (no-op when time wasn't traveled).
config.after { travel_back }

# ActiveSupport::CurrentAttributes (Current.user/source) is only auto-reset by
# the executor around real requests/jobs. Controller/view/service specs set it
# in the test thread with no executor to clear it, so a stale Current leaks
# into later examples and silently changes model behavior that branches on it
# (Organization affiliation-lock validation, AhoyTrackable lifecycle tracking).
# Reset after every example, like travel_back above (no-op when nothing set it).
config.after { Current.reset }

# Include pagination helper globally
config.include PaginationHelpers

Expand Down
7 changes: 6 additions & 1 deletion spec/services/organization_services/upsert_address_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@
end

it "updates the matching city/state address in place instead of duplicating" do
existing = create(:address, addressable: organization, city: "Austin", state: "TX", primary: true)
# Pin the fields this example expects to change to values distinct from the
# upsert below. The factory otherwise fills them from Faker's shared,
# order-dependent stream, which can coincidentally match a new value (e.g.
# country "Canada") and drop it from the reported changes under some seeds.
existing = create(:address, addressable: organization, city: "Austin", state: "TX", primary: true,
street_address: "1 Old St", zip_code: "10001", country: "United States")

result = described_class.call(
organization: organization,
Expand Down
19 changes: 15 additions & 4 deletions spec/support/faker.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# give it a specific seed to ensure the same data is generated. This combined
# with running rspec with a specific seed value will ensure that specs are run
# in the same order and the same "faked" data is generated each time: giving
# us deterministic results.
# Reseed Faker's RNG before every example so each example draws from the same
# fixed sequence regardless of test order. Faker uses one shared, global stream,
# so without this a factory's "random" value depends on how many Faker calls ran
# earlier in the suite β€” making data order-dependent and specs flaky under some
# seeds (e.g. an address factory landing on country "Canada"). Reseeding per
# example makes the faked data deterministic AND independent of ordering.
Faker::Config.random = Random.new(42)

RSpec.configure do |config|
config.before(:each) do
Faker::Config.random = Random.new(42)
# Faker's .unique generator remembers used values for the whole run; reseeding
# to the same stream would replay them and hit RetryLimitExceeded, so clear it.
Faker::UniqueGenerator.clear
end
end