diff --git a/tests/test_migrations.py b/tests/test_migrations.py new file mode 100644 index 0000000..de2b259 --- /dev/null +++ b/tests/test_migrations.py @@ -0,0 +1,69 @@ +"""Real-DB migration test (#14). + +Runs the full migration chain against a fresh temp SQLite via the lnbits +`Database`, then round-trips through `crud` to prove the resulting schema. +Guards the class of bug from #13 (a migration statement that parses in Python +but is invalid SQL on SQLite — the default backend). The rest of the suite +monkeypatches `crud`, so migrations are otherwise never actually executed. + +Isolation: `Database` binds its sqlite path + engine at construction from +`settings.lnbits_data_folder`, and `crud.db` is built at import time. So we +point settings at `tmp_path`, build a fresh `ext_chatelet` DB, and swap it +into `crud` for the test — import-order-independent, no live server. +""" + +import asyncio +import re + +from lnbits.db import Database +from lnbits.settings import settings + +from .. import crud, migrations +from ..models import Booking, BookingStatus, CreateRoomData, RoomStatus + + +def test_full_migration_chain_applies_and_schema_round_trips(monkeypatch, tmp_path): + # Fresh, isolated ext DB in tmp_path; route crud at it for the test. + monkeypatch.setattr(settings, "lnbits_data_folder", str(tmp_path)) + test_db = Database("ext_chatelet") + monkeypatch.setattr(crud, "db", test_db) + + async def run(): + # Apply every m0NN migration in order against the empty DB. If any + # statement is invalid on SQLite (the #13 bug), this raises here. + migfns = sorted( + (n, f) for n, f in vars(migrations).items() if re.match(r"m\d+_", n) + ) + assert migfns, "no m0NN migrations discovered" + async with test_db.connect() as conn: + for _name, fn in migfns: + await fn(conn) + + # Round-trip through crud (now bound to test_db) to prove the schema. + room = await crud.create_room( + CreateRoomData( + wallet="w1", title="Keep", price_amount=90, price_currency="EUR" + ) + ) + got = await crud.get_room(room.id) + assert got is not None + assert got.checkin_instructions == "" # m002 column exists, default '' + got.status = RoomStatus.active + await crud.update_room(got) + + booking = Booking( + id="bk1", room_id=room.id, guest_pubkey="g", + check_in="2026-08-01", check_out="2026-08-04", nights=3, num_guests=1, + currency="EUR", price_fiat=270.0, amount_sat=450000, deposit_sat=450000, + status=BookingStatus.held, + ) + await crud.create_booking(booking) + gb = await crud.get_booking("bk1") + assert gb is not None and gb.amount_sat == 450000 # big_int round-trips + + # Availability computed against real rows (not monkeypatched): the held + # booking blocks its own dates; a non-overlapping range is free. + assert await crud.is_available(room.id, "2026-08-01", "2026-08-04") is False + assert await crud.is_available(room.id, "2026-08-10", "2026-08-12") is True + + asyncio.run(run())