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
87 lines
3.4 KiB
Python
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"))
|