test: booking-flow, availability, and #4 concurrency regression
16 tests, run under the LNbits pytest env (spirekeeper pattern: monkeypatch crud/invoice, drive async via asyncio.run — no live DB/wallet): - test_availability: half-open overlap semantics (back-to-back stays OK), and is_available blocking on held/confirmed bookings + manual blocks. - test_booking_flow: canonical amount_sat, awaiting_payment transition, min-nights guard, and hold-release (declined) on InvoiceError. - test_atomic_hold: the #4 regression — two concurrent same-date requests yield exactly one hold + one conflict; non-overlapping both succeed. The fakes yield mid-check to open the race window, so the test fails without the per-room lock (verified by neutering it) and passes with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
This commit is contained in:
parent
8b50263aac
commit
7d704a8549
5 changed files with 353 additions and 0 deletions
87
tests/test_availability.py
Normal file
87
tests/test_availability.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Availability arbiter: half-open overlap semantics + is_available()."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from .. import crud
|
||||
from ..models import Block, Booking, BookingStatus, RoomStatus
|
||||
from .conftest import make_room
|
||||
|
||||
|
||||
# --- pure interval logic ---------------------------------------------------
|
||||
|
||||
|
||||
class TestOverlap:
|
||||
def test_disjoint_ranges_do_not_overlap(self):
|
||||
assert not crud._overlaps("2026-08-01", "2026-08-04", "2026-08-04", "2026-08-06")
|
||||
|
||||
def test_back_to_back_same_day_does_not_overlap(self):
|
||||
# check_out is exclusive: one guest leaves 08-04, next arrives 08-04.
|
||||
assert not crud._overlaps("2026-08-01", "2026-08-04", "2026-08-04", "2026-08-07")
|
||||
|
||||
def test_partial_overlap(self):
|
||||
assert crud._overlaps("2026-08-01", "2026-08-05", "2026-08-04", "2026-08-08")
|
||||
|
||||
def test_full_containment(self):
|
||||
assert crud._overlaps("2026-08-01", "2026-08-10", "2026-08-03", "2026-08-05")
|
||||
|
||||
def test_nights_between(self):
|
||||
assert crud.nights_between("2026-08-01", "2026-08-04") == 3
|
||||
|
||||
|
||||
# --- is_available (crud getters monkeypatched) -----------------------------
|
||||
|
||||
|
||||
def _patch(monkeypatch, *, room, bookings=None, blocks=None):
|
||||
async def _get_room(_):
|
||||
return room
|
||||
|
||||
async def _get_bookings(_):
|
||||
return bookings or []
|
||||
|
||||
async def _get_blocks(_):
|
||||
return blocks or []
|
||||
|
||||
monkeypatch.setattr(crud, "get_room", _get_room)
|
||||
monkeypatch.setattr(crud, "get_bookings_for_room", _get_bookings)
|
||||
monkeypatch.setattr(crud, "get_blocks_for_room", _get_blocks)
|
||||
|
||||
|
||||
def _booking(status: BookingStatus, ci="2026-08-02", co="2026-08-05") -> Booking:
|
||||
return Booking(
|
||||
id="b1", room_id="room1", guest_pubkey="g", check_in=ci, check_out=co,
|
||||
nights=3, num_guests=1, currency="sat", price_fiat=300.0,
|
||||
amount_sat=300, deposit_sat=300, status=status,
|
||||
)
|
||||
|
||||
|
||||
class TestIsAvailable:
|
||||
def test_open_range_is_available(self, monkeypatch):
|
||||
_patch(monkeypatch, room=make_room())
|
||||
assert asyncio.run(crud.is_available("room1", "2026-08-01", "2026-08-04"))
|
||||
|
||||
def test_inactive_room_is_never_available(self, monkeypatch):
|
||||
_patch(monkeypatch, room=make_room(status=RoomStatus.inactive))
|
||||
assert not asyncio.run(crud.is_available("room1", "2026-08-01", "2026-08-04"))
|
||||
|
||||
def test_overlapping_confirmed_booking_blocks(self, monkeypatch):
|
||||
_patch(monkeypatch, room=make_room(),
|
||||
bookings=[_booking(BookingStatus.confirmed)])
|
||||
assert not asyncio.run(crud.is_available("room1", "2026-08-01", "2026-08-04"))
|
||||
|
||||
def test_overlapping_held_booking_blocks(self, monkeypatch):
|
||||
# A mere hold occupies the calendar — this is what makes the atomic
|
||||
# check-then-hold work.
|
||||
_patch(monkeypatch, room=make_room(),
|
||||
bookings=[_booking(BookingStatus.held)])
|
||||
assert not asyncio.run(crud.is_available("room1", "2026-08-01", "2026-08-04"))
|
||||
|
||||
def test_cancelled_booking_does_not_block(self, monkeypatch):
|
||||
_patch(monkeypatch, room=make_room(),
|
||||
bookings=[_booking(BookingStatus.cancelled)])
|
||||
assert asyncio.run(crud.is_available("room1", "2026-08-01", "2026-08-04"))
|
||||
|
||||
def test_manual_block_blocks(self, monkeypatch):
|
||||
blk = Block(id="k1", room_id="room1", start_date="2026-08-03",
|
||||
end_date="2026-08-06", reason="maintenance")
|
||||
_patch(monkeypatch, room=make_room(), blocks=[blk])
|
||||
assert not asyncio.run(crud.is_available("room1", "2026-08-01", "2026-08-04"))
|
||||
Loading…
Add table
Add a link
Reference in a new issue