Skip to content
Draft
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
8 changes: 8 additions & 0 deletions spec/controllers/bookings_clash_e2e_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ describe Bookings do
Attendee.truncate
Guest.truncate

# these specs rely on the tenant having no booking limits configured; other
# specs mutate the shared tenant, so establish a clean baseline here.
tenant = get_tenant
unless tenant.booking_limits.as_h.empty?
tenant.booking_limits = JSON::Any.new({} of String => JSON::Any)
tenant.save!
end

# the create action fans out signals to the engine -- stub them
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token")
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
Expand Down
38 changes: 30 additions & 8 deletions spec/controllers/bookings_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2462,7 +2462,7 @@ describe Bookings do
end

describe "booking_limits" do
before_all do
before_each do
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token")
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/changed")
Expand All @@ -2477,6 +2477,11 @@ describe Bookings do

after_all do
WebMock.reset
# these specs mutate the shared tenant's booking limits; restore the
# default (no limits) so the state does not leak into other specs.
tenant = get_tenant
tenant.booking_limits = JSON::Any.new({} of String => JSON::Any)
tenant.save!
end

# add support for configurable booking limits on resources
Expand Down Expand Up @@ -3158,7 +3163,15 @@ describe Bookings do
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
end

# Reset and re-register the stubs at the start of each spec so our capturing
# stub is the first match (WebMock uses the first matching stub, not the
# last) and any stubs / allow_net_connect leaked from earlier specs are
# cleared. The signals are emitted from spawned fibres, so `wait_until` polls
# until they arrive rather than relying on a fixed sleep.
it "#create emits staff/booking/changed with action create" do
WebMock.reset
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token")
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
captured_bodies = [] of String
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/changed")
.to_return do |request|
Expand All @@ -3176,7 +3189,7 @@ describe Bookings do
client.post(BOOKINGS_BASE, headers: headers,
body: %({"asset_id":"desk-signal-1","booking_start":#{starting},"booking_end":#{ending},"booking_type":"desk"}))

sleep 50.milliseconds # let spawn fibres run
wait_until { captured_bodies.size >= 1 }

captured_bodies.size.should be >= 1
payload = JSON.parse(captured_bodies.last)
Expand All @@ -3186,6 +3199,9 @@ describe Bookings do
end

it "#update with shrunk time window emits action metadata_changed with previous values" do
WebMock.reset
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token")
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
captured_bodies = [] of String
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/changed")
.to_return do |request|
Expand All @@ -3203,7 +3219,7 @@ describe Bookings do
created = JSON.parse(client.post(BOOKINGS_BASE, headers: headers,
body: %({"asset_id":"desk-signal-2","booking_start":#{starting},"booking_end":#{ending},"booking_type":"desk"})).body)

sleep 50.milliseconds
wait_until { captured_bodies.size >= 1 }

# Shrink the window: move start later, move end earlier
new_starting = starting + 600
Expand All @@ -3212,7 +3228,7 @@ describe Bookings do
client.patch("#{BOOKINGS_BASE}/#{created["id"]}", headers: headers,
body: %({"booking_start":#{new_starting},"booking_end":#{new_ending}}))

sleep 50.milliseconds
wait_until { captured_bodies.size >= 2 }

# The last captured body should be from the update, not the create
captured_bodies.size.should be >= 2
Expand All @@ -3225,6 +3241,9 @@ describe Bookings do
end

it "#update with metadata-only change emits action metadata_changed" do
WebMock.reset
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token")
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
captured_bodies = [] of String
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/changed")
.to_return do |request|
Expand All @@ -3242,13 +3261,13 @@ describe Bookings do
created = JSON.parse(client.post(BOOKINGS_BASE, headers: headers,
body: %({"asset_id":"desk-signal-3","booking_start":#{starting},"booking_end":#{ending},"booking_type":"desk"})).body)

sleep 50.milliseconds
wait_until { captured_bodies.size >= 1 }

# Update only the title — no visitor-relevant fields change
client.patch("#{BOOKINGS_BASE}/#{created["id"]}", headers: headers,
body: %({"title":"just a title change"}))

sleep 50.milliseconds
wait_until { captured_bodies.size >= 2 }

captured_bodies.size.should be >= 2
update_payload = JSON.parse(captured_bodies.last)
Expand All @@ -3257,6 +3276,9 @@ describe Bookings do
end

it "#destroy emits staff/booking/changed with action cancelled" do
WebMock.reset
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token")
.to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json"))
captured_bodies = [] of String
WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/changed")
.to_return do |request|
Expand All @@ -3272,11 +3294,11 @@ describe Bookings do
created = JSON.parse(client.post(BOOKINGS_BASE, headers: headers,
body: %({"asset_id":"desk-signal-4","booking_start":#{starting},"booking_end":#{ending},"booking_type":"desk"})).body)

sleep 50.milliseconds
wait_until { captured_bodies.size >= 1 }

client.delete("#{BOOKINGS_BASE}/#{created["id"]}", headers: headers)

sleep 50.milliseconds
wait_until { captured_bodies.size >= 2 }

captured_bodies.size.should be >= 2
destroy_payload = JSON.parse(captured_bodies.last)
Expand Down
17 changes: 17 additions & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,25 @@ def truncate_db
Tenant.truncate
end

# Waits for an asynchronous expectation to be satisfied, yielding to the
# scheduler between checks so spawned fibres (e.g. signal emitters) can run.
# Returns as soon as the block is truthy, or after exhausting the budget.
# Iteration-based (not wall-clock) so it is unaffected by any Timecop scaling.
def wait_until(attempts : Int32 = 150, delay : Time::Span = 20.milliseconds, & : -> Bool) : Nil
attempts.times do
return if yield
sleep delay
end
end

Spec.before_each &->WebMock.reset

# Timecop.scale/travel push onto an internal stack that is never popped unless a
# block form is used (see lib/timecop). Individual specs that scale time without
# restoring it would otherwise leak scaled time into every subsequent example
# (e.g. breaking sleep-based fibre timing). Clear the stack after every example.
Spec.after_each { Timecop.return }

module Mock
extend self

Expand Down
Loading