chatelet/tests/test_availability.py
Padreug 7d704a8549 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
2026-07-19 17:48:11 +02:00

87 lines
3.4 KiB
Python

"""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"))