test_store_sqlite.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. """
  2. SQLite backend tests for all StoreBacked stores.
  3. Each test injects a SQLiteBackend directly via backend= so no CalDAV or config
  4. is involved. A fresh temp DB is created per test via the sqlite_backend fixture.
  5. """
  6. import json
  7. import pytest
  8. from datetime import date, timedelta
  9. from pathlib import Path
  10. from room_cal.store._sqlite import SQLiteBackend
  11. from room_cal.closed import Closed, ClosedData, _build_title, _build_description as _closed_desc
  12. from room_cal.discount import Discount, DiscountData, DiscountType
  13. from room_cal.reservations import Reservation
  14. from room_cal.user_cal import UserCal
  15. from room_cal.pricing.price_calendar import CalPrice
  16. # ── fixtures ───────────────────────────────────────────────────────────────────
  17. @pytest.fixture
  18. def sqlite_backend(tmp_path):
  19. return SQLiteBackend(tmp_path / 'test.db')
  20. @pytest.fixture
  21. def house_backend(tmp_path):
  22. return SQLiteBackend(tmp_path / 'test.db')
  23. # ── SQLiteBackend direct ───────────────────────────────────────────────────────
  24. @pytest.mark.asyncio
  25. async def test_backend_create_fetch_delete(sqlite_backend):
  26. from room_cal.store._backend import EventRecord
  27. from datetime import datetime
  28. rec = EventRecord(
  29. uid='u1',
  30. start=datetime(2024, 6, 1),
  31. end=datetime(2024, 6, 2),
  32. all_day=True,
  33. title='Test Event',
  34. description='desc',
  35. x_data='{"k": 1}',
  36. )
  37. await sqlite_backend.create(rec)
  38. results = await sqlite_backend.fetch_range(datetime(2024, 5, 31), datetime(2024, 6, 3))
  39. assert len(results) == 1
  40. assert results[0].uid == 'u1'
  41. assert results[0].title == 'Test Event'
  42. assert results[0].x_data == '{"k": 1}'
  43. await sqlite_backend.delete('u1')
  44. results = await sqlite_backend.fetch_range(datetime(2024, 5, 31), datetime(2024, 6, 3))
  45. assert results == []
  46. @pytest.mark.asyncio
  47. async def test_backend_update(sqlite_backend):
  48. from room_cal.store._backend import EventRecord
  49. from datetime import datetime
  50. rec = EventRecord(
  51. uid='u2',
  52. start=datetime(2024, 7, 10),
  53. end=datetime(2024, 7, 11),
  54. all_day=True,
  55. title='Original',
  56. description='',
  57. x_data=None,
  58. )
  59. await sqlite_backend.create(rec)
  60. updated = await sqlite_backend.update('u2', title='Updated', x_data='{"v": 2}')
  61. assert updated.title == 'Updated'
  62. assert updated.x_data == '{"v": 2}'
  63. results = await sqlite_backend.fetch_range(datetime(2024, 7, 9), datetime(2024, 7, 12))
  64. assert results[0].title == 'Updated'
  65. @pytest.mark.asyncio
  66. async def test_backend_fetch_range_boundary(sqlite_backend):
  67. """fetch_range uses exclusive end — event touching the boundary is included."""
  68. from room_cal.store._backend import EventRecord
  69. from datetime import datetime
  70. rec = EventRecord(
  71. uid='u3',
  72. start=datetime(2024, 8, 1),
  73. end=datetime(2024, 8, 2),
  74. all_day=True,
  75. title='Boundary',
  76. description='',
  77. x_data=None,
  78. )
  79. await sqlite_backend.create(rec)
  80. assert await sqlite_backend.fetch_range(datetime(2024, 8, 1), datetime(2024, 8, 2)) != []
  81. assert await sqlite_backend.fetch_range(datetime(2024, 8, 2), datetime(2024, 8, 3)) == []
  82. assert await sqlite_backend.fetch_range(datetime(2024, 7, 31), datetime(2024, 8, 1, 0, 0, 1)) != []
  83. @pytest.mark.asyncio
  84. async def test_backend_uid_is_unique_primary_key(tmp_path):
  85. """uid is the sole primary key; inserting a duplicate uid raises."""
  86. import sqlite3
  87. from room_cal.store._backend import EventRecord
  88. from datetime import datetime
  89. backend = SQLiteBackend(tmp_path / 'test.db')
  90. rec = EventRecord(
  91. uid='dup', start=datetime(2024, 9, 1), end=datetime(2024, 9, 2),
  92. all_day=True, title='First', description='', x_data=None,
  93. )
  94. await backend.create(rec)
  95. with pytest.raises(Exception):
  96. await backend.create(EventRecord(
  97. uid='dup', start=datetime(2024, 9, 3), end=datetime(2024, 9, 4),
  98. all_day=True, title='Second', description='', x_data=None,
  99. ))
  100. # ── Closed ─────────────────────────────────────────────────────────────────────
  101. @pytest.mark.asyncio
  102. async def test_closed_set_and_get(sqlite_backend):
  103. closed = Closed(backend=sqlite_backend)
  104. target = date(2024, 3, 15)
  105. services = ['airbnb', 'booking']
  106. await closed.set(target, ClosedData(services=services, source={'domain': 'test', 'name': 'Test', 'id': 'c1'}))
  107. stored = await closed.get(target)
  108. assert stored is not None
  109. assert sorted(stored.services) == sorted(services)
  110. assert stored.source == {'domain': 'test', 'name': 'Test', 'id': 'c1'}
  111. @pytest.mark.asyncio
  112. async def test_closed_event_title_and_description(sqlite_backend):
  113. closed = Closed(backend=sqlite_backend)
  114. target = date(2024, 3, 16)
  115. await closed.set(target, ClosedData(services=['airbnb', 'booking'], source={'domain': 'test', 'name': 'Test => ', 'id': 'c2'}))
  116. event = await closed._find(target)
  117. assert event is not None
  118. assert event.title == 'Closed: All'
  119. assert event.description == 'AirBnB: Closed\nBooking.com: Closed\nSource: Test => c2'
  120. @pytest.mark.asyncio
  121. async def test_closed_delete_via_empty_services(sqlite_backend):
  122. closed = Closed(backend=sqlite_backend)
  123. target = date(2024, 3, 17)
  124. await closed.set(target, ClosedData(services=['airbnb']))
  125. assert await closed.get(target) is not None
  126. await closed.set(target, ClosedData(services=[]))
  127. assert await closed.get(target) is None
  128. @pytest.mark.asyncio
  129. async def test_closed_set_is_idempotent(sqlite_backend):
  130. """Calling set twice with the same data produces one event, not two."""
  131. closed = Closed(backend=sqlite_backend)
  132. target = date(2024, 3, 18)
  133. data = ClosedData(services=['airbnb'], source={'domain': 'test', 'id': 'c3'})
  134. await closed.set(target, data)
  135. await closed.set(target, data)
  136. events = await closed.list(target, target)
  137. assert len(events) == 1
  138. @pytest.mark.asyncio
  139. async def test_closed_set_updates_on_change(sqlite_backend):
  140. closed = Closed(backend=sqlite_backend)
  141. target = date(2024, 3, 19)
  142. await closed.set(target, ClosedData(services=['airbnb']))
  143. await closed.set(target, ClosedData(services=['airbnb', 'booking']))
  144. stored = await closed.get(target)
  145. assert sorted(stored.services) == ['airbnb', 'booking']
  146. assert len(await closed.list(target, target)) == 1
  147. @pytest.mark.asyncio
  148. async def test_closed_list_range(sqlite_backend):
  149. closed = Closed(backend=sqlite_backend)
  150. for day in range(1, 4):
  151. await closed.set(date(2024, 4, day), ClosedData(services=['airbnb']))
  152. events = await closed.list(date(2024, 4, 1), date(2024, 4, 3))
  153. assert len(events) == 3
  154. @pytest.mark.asyncio
  155. async def test_closed_get_range(sqlite_backend):
  156. closed = Closed(backend=sqlite_backend)
  157. d1, d2 = date(2024, 4, 10), date(2024, 4, 11)
  158. await closed.set(d1, ClosedData(services=['airbnb']))
  159. await closed.set(d2, ClosedData(services=['booking']))
  160. result = await closed.get_range(d1, d2)
  161. assert d1 in result and d2 in result
  162. assert result[d1].services == ['airbnb']
  163. assert result[d2].services == ['booking']
  164. # ── Discount ───────────────────────────────────────────────────────────────────
  165. @pytest.mark.asyncio
  166. async def test_discount_flat_round_trip(sqlite_backend):
  167. discount = Discount(backend=sqlite_backend)
  168. target = date(2024, 5, 1)
  169. source = {'domain': 'test', 'name': 'Test => ', 'id': 'flat-1'}
  170. await discount.set(target, DiscountData(type=DiscountType.FLAT, value=21, source=source))
  171. stored = await discount.get(target)
  172. assert stored is not None
  173. assert stored.type == DiscountType.FLAT
  174. assert stored.value == 21
  175. assert stored.source == source
  176. @pytest.mark.asyncio
  177. async def test_discount_percent_round_trip(sqlite_backend):
  178. discount = Discount(backend=sqlite_backend)
  179. target = date(2024, 5, 2)
  180. source = {'domain': 'test', 'name': 'Test => ', 'id': 'pct-1'}
  181. await discount.set(target, DiscountData(type=DiscountType.PERCENT, value=15, source=source))
  182. stored = await discount.get(target)
  183. assert stored is not None
  184. assert stored.type == DiscountType.PERCENT
  185. assert stored.value == 15
  186. event = await discount._find(target)
  187. assert event.title == 'Discount: 15%'
  188. assert 'Source: Test => pct-1' in event.description
  189. @pytest.mark.asyncio
  190. async def test_discount_flat_event_title(sqlite_backend):
  191. discount = Discount(backend=sqlite_backend)
  192. target = date(2024, 5, 3)
  193. await discount.set(target, DiscountData(type=DiscountType.FLAT, value=20, source={}))
  194. event = await discount._find(target)
  195. assert event.title == 'Discount: $20'
  196. @pytest.mark.asyncio
  197. async def test_discount_delete_on(sqlite_backend):
  198. discount = Discount(backend=sqlite_backend)
  199. target = date(2024, 5, 4)
  200. await discount.set(target, DiscountData(type=DiscountType.FLAT, value=10, source={}))
  201. assert await discount.get(target) is not None
  202. await discount.delete_on(target)
  203. assert await discount.get(target) is None
  204. @pytest.mark.asyncio
  205. async def test_discount_set_is_idempotent(sqlite_backend):
  206. discount = Discount(backend=sqlite_backend)
  207. target = date(2024, 5, 5)
  208. data = DiscountData(type=DiscountType.FLAT, value=5, source={})
  209. await discount.set(target, data)
  210. await discount.set(target, data)
  211. events = await discount.list(target, target)
  212. assert len(events) == 1
  213. @pytest.mark.asyncio
  214. async def test_discount_set_updates_value(sqlite_backend):
  215. discount = Discount(backend=sqlite_backend)
  216. target = date(2024, 5, 6)
  217. await discount.set(target, DiscountData(type=DiscountType.FLAT, value=10, source={}))
  218. await discount.set(target, DiscountData(type=DiscountType.FLAT, value=20, source={}))
  219. stored = await discount.get(target)
  220. assert stored.value == 20
  221. assert len(await discount.list(target, target)) == 1
  222. @pytest.mark.asyncio
  223. async def test_discount_get_range(sqlite_backend):
  224. discount = Discount(backend=sqlite_backend)
  225. d1, d2 = date(2024, 5, 10), date(2024, 5, 11)
  226. await discount.set(d1, DiscountData(type=DiscountType.FLAT, value=5, source={}))
  227. await discount.set(d2, DiscountData(type=DiscountType.PERCENT, value=10, source={}))
  228. result = await discount.get_range(d1, d2)
  229. assert result[d1].type == DiscountType.FLAT
  230. assert result[d2].type == DiscountType.PERCENT
  231. # ── Reservation ────────────────────────────────────────────────────────────────
  232. @pytest.mark.asyncio
  233. async def test_reservation_set_and_get(sqlite_backend):
  234. res = Reservation(backend=sqlite_backend)
  235. checkin = date(2024, 6, 1)
  236. checkout = checkin + timedelta(days=2)
  237. await res.set({
  238. 'guest_name': 'Alice Test',
  239. 'service': 'airbnb',
  240. 'reservation_id': 'RES-001',
  241. 'checkin_date': checkin,
  242. 'checkout_date': checkout,
  243. })
  244. stored = await res.get('RES-001', checkin, checkout)
  245. assert stored is not None
  246. assert stored.guest_name == 'Alice Test'
  247. assert stored.reservation_id == 'RES-001'
  248. assert stored.service == 'airbnb'
  249. @pytest.mark.asyncio
  250. async def test_reservation_generates_wifi_and_door(sqlite_backend):
  251. res = Reservation(backend=sqlite_backend)
  252. checkin = date(2024, 6, 3)
  253. checkout = checkin + timedelta(days=1)
  254. await res.set({
  255. 'guest_name': 'Bob Test',
  256. 'service': 'booking',
  257. 'reservation_id': 'RES-002',
  258. 'checkin_date': checkin,
  259. 'checkout_date': checkout,
  260. })
  261. stored = await res.get('RES-002', checkin, checkout)
  262. assert stored.wifi_key is not None and stored.wifi_key != ''
  263. assert stored.door_code is not None and stored.door_code != ''
  264. assert stored.wifi_ssid is None # no config default set
  265. @pytest.mark.asyncio
  266. async def test_reservation_preserves_wifi_on_update(sqlite_backend):
  267. """Re-setting a reservation preserves wifi_key/wifi_ssid/door_code from the first write."""
  268. res = Reservation(backend=sqlite_backend)
  269. checkin = date(2024, 6, 5)
  270. checkout = checkin + timedelta(days=2)
  271. data = {
  272. 'guest_name': 'Carol Test',
  273. 'service': 'airbnb',
  274. 'reservation_id': 'RES-003',
  275. 'checkin_date': checkin,
  276. 'checkout_date': checkout,
  277. 'wifi_ssid': 'TestNet',
  278. }
  279. await res.set(data)
  280. first = await res.get('RES-003', checkin, checkout)
  281. await res.set({**data, 'wifi_ssid': None}) # omit ssid on second write
  282. second = await res.get('RES-003', checkin, checkout)
  283. assert first.wifi_key == second.wifi_key
  284. assert first.wifi_ssid == second.wifi_ssid # preserved from stored value
  285. assert first.door_code == second.door_code
  286. @pytest.mark.asyncio
  287. async def test_reservation_config_default_wifi(sqlite_backend, monkeypatch):
  288. """Config defaults for wifi_key, wifi_ssid, door_code are used when not otherwise provided."""
  289. from room_cal._utils import ReservationDefaultsConfig
  290. import room_cal.reservations as _res_mod
  291. monkeypatch.setattr(_res_mod, '_reservation_defaults', lambda: ReservationDefaultsConfig(
  292. wifi_key='default-pass',
  293. wifi_ssid='HomeNet',
  294. door_code='9999',
  295. ))
  296. res = Reservation(backend=sqlite_backend)
  297. checkin = date(2024, 7, 1)
  298. checkout = checkin + timedelta(days=1)
  299. await res.set({
  300. 'guest_name': 'Dave Test',
  301. 'service': 'airbnb',
  302. 'reservation_id': 'RES-CFG',
  303. 'checkin_date': checkin,
  304. 'checkout_date': checkout,
  305. })
  306. stored = await res.get('RES-CFG', checkin, checkout)
  307. assert stored.wifi_key == 'default-pass'
  308. assert stored.wifi_ssid == 'HomeNet'
  309. assert stored.door_code == '9999'
  310. @pytest.mark.asyncio
  311. async def test_reservation_list(sqlite_backend):
  312. res = Reservation(backend=sqlite_backend)
  313. checkin = date(2024, 6, 10)
  314. for i in range(3):
  315. c_in = checkin + timedelta(days=i * 5)
  316. await res.set({
  317. 'guest_name': f'Guest {i}',
  318. 'service': 'airbnb',
  319. 'reservation_id': f'RES-L{i}',
  320. 'checkin_date': c_in,
  321. 'checkout_date': c_in + timedelta(days=2),
  322. })
  323. events = await res.list(checkin, checkin + timedelta(days=30))
  324. assert len(events) == 3
  325. @pytest.mark.asyncio
  326. async def test_reservation_delete(sqlite_backend):
  327. res = Reservation(backend=sqlite_backend)
  328. checkin = date(2024, 6, 20)
  329. checkout = checkin + timedelta(days=1)
  330. await res.set({
  331. 'guest_name': 'Dave Test',
  332. 'service': 'airbnb',
  333. 'reservation_id': 'RES-DEL',
  334. 'checkin_date': checkin,
  335. 'checkout_date': checkout,
  336. })
  337. event = await res._find('RES-DEL', checkin, checkout)
  338. assert event is not None
  339. await res.delete(event)
  340. assert await res.get('RES-DEL', checkin, checkout) is None
  341. @pytest.mark.asyncio
  342. async def test_reservation_dates_shift_within_same_range(sqlite_backend):
  343. """Shifting dates within the same search range triggers the delete+recreate path."""
  344. res = Reservation(backend=sqlite_backend)
  345. # Both checkin/checkout fall in the same week so _find can see the old event
  346. checkin1, checkout1 = date(2024, 7, 1), date(2024, 7, 3)
  347. checkin2, checkout2 = date(2024, 7, 2), date(2024, 7, 4)
  348. await res.set({
  349. 'guest_name': 'Eve Test',
  350. 'service': 'airbnb',
  351. 'reservation_id': 'RES-SHIFT',
  352. 'checkin_date': checkin1,
  353. 'checkout_date': checkout1,
  354. })
  355. # Second set uses new dates but _find scans new range — old event at [1,3) is in [2,4) range
  356. await res.set({
  357. 'guest_name': 'Eve Test',
  358. 'service': 'airbnb',
  359. 'reservation_id': 'RES-SHIFT',
  360. 'checkin_date': checkin2,
  361. 'checkout_date': checkout2,
  362. })
  363. stored = await res.get('RES-SHIFT', checkin2, checkout2)
  364. assert stored is not None
  365. assert stored.reservation_id == 'RES-SHIFT'
  366. # ── UserCal ────────────────────────────────────────────────────────────────────
  367. @pytest.mark.asyncio
  368. async def test_usercal_set_and_find_services(house_backend):
  369. ucal = UserCal(backend=house_backend)
  370. service_key = 'airbnb'
  371. reservation_id = 'UC-001'
  372. checkin = date(2024, 8, 1)
  373. checkout = checkin + timedelta(days=3)
  374. await ucal.set(service_key, reservation_id, checkin, checkout)
  375. events = await ucal.find_services(checkin, checkout)
  376. match = next((e for e in events if json.loads(e.x_data).get('reservation_id') == reservation_id), None)
  377. assert match is not None
  378. assert json.loads(match.x_data)['service_key'] == service_key
  379. assert match.all_day
  380. assert match.start.date() == checkin
  381. assert match.end.date() == checkout
  382. @pytest.mark.asyncio
  383. async def test_usercal_set_is_idempotent(house_backend):
  384. ucal = UserCal(backend=house_backend)
  385. checkin = date(2024, 8, 5)
  386. checkout = checkin + timedelta(days=2)
  387. await ucal.set('airbnb', 'UC-002', checkin, checkout)
  388. await ucal.set('airbnb', 'UC-002', checkin, checkout)
  389. events = await ucal.find_services(checkin, checkout)
  390. matches = [e for e in events if json.loads(e.x_data).get('reservation_id') == 'UC-002']
  391. assert len(matches) == 1
  392. @pytest.mark.asyncio
  393. async def test_usercal_find_closed_excludes_service_events(house_backend):
  394. """find_closed only returns 'Room Closed' events; service events are excluded."""
  395. ucal = UserCal(backend=house_backend)
  396. checkin = date(2024, 8, 10)
  397. checkout = checkin + timedelta(days=2)
  398. await ucal.set('airbnb', 'UC-003', checkin, checkout)
  399. closed_events = await ucal.find_closed(checkin, checkout)
  400. assert all(e.title == 'Room Closed' for e in closed_events)
  401. @pytest.mark.asyncio
  402. async def test_usercal_delete(house_backend):
  403. ucal = UserCal(backend=house_backend)
  404. checkin = date(2024, 8, 15)
  405. checkout = checkin + timedelta(days=1)
  406. await ucal.set('booking', 'UC-004', checkin, checkout)
  407. events = await ucal.find_services(checkin, checkout)
  408. for e in events:
  409. if json.loads(e.x_data).get('reservation_id') == 'UC-004':
  410. await ucal.delete(e)
  411. remaining = await ucal.find_services(checkin, checkout)
  412. assert not any(json.loads(e.x_data).get('reservation_id') == 'UC-004' for e in remaining)
  413. # ── CalPrice ───────────────────────────────────────────────────────────────────
  414. @pytest.mark.asyncio
  415. async def test_calprice_set_and_get_prices(sqlite_backend):
  416. cal = CalPrice(backend=sqlite_backend)
  417. target = date(2024, 9, 1)
  418. prices = await cal.set(target, 100, discount_rec=None)
  419. assert prices.base == 100
  420. assert prices.net == 100
  421. readback = await cal._get_prices(target)
  422. assert readback is not None
  423. assert readback.net == 100
  424. assert readback.base == 100
  425. @pytest.mark.asyncio
  426. async def test_calprice_set_includes_service_prices(sqlite_backend):
  427. cal = CalPrice(backend=sqlite_backend)
  428. target = date(2024, 9, 2)
  429. prices = await cal.set(target, 80, discount_rec=None)
  430. assert len(prices.service_prices) > 0
  431. for svc, price in prices.service_prices.items():
  432. assert isinstance(price, (int, float))
  433. assert price > 0
  434. @pytest.mark.asyncio
  435. async def test_calprice_set_with_flat_discount(sqlite_backend):
  436. """CalPrice.set applies a flat discount when passed via discount_rec."""
  437. cal = CalPrice(backend=sqlite_backend)
  438. target = date(2024, 9, 3)
  439. discount = DiscountData(type=DiscountType.FLAT, value=10, source={})
  440. prices = await cal.set(target, 100, discount_rec=discount)
  441. assert prices.net == 90
  442. assert prices.discount == 10
  443. @pytest.mark.asyncio
  444. async def test_calprice_set_with_percent_discount(sqlite_backend):
  445. """CalPrice.set applies a percent discount when passed via discount_rec."""
  446. cal = CalPrice(backend=sqlite_backend)
  447. target = date(2024, 9, 4)
  448. discount = DiscountData(type=DiscountType.PERCENT, value=10, source={})
  449. prices = await cal.set(target, 100, discount_rec=discount)
  450. assert prices.net == 90
  451. @pytest.mark.asyncio
  452. async def test_calprice_set_is_idempotent(sqlite_backend):
  453. cal = CalPrice(backend=sqlite_backend)
  454. target = date(2024, 9, 5)
  455. await cal.set(target, 75, discount_rec=None)
  456. await cal.set(target, 75, discount_rec=None)
  457. events = await cal.list(target, target)
  458. assert len(events) == 1
  459. @pytest.mark.asyncio
  460. async def test_calprice_set_updates_price(sqlite_backend):
  461. cal = CalPrice(backend=sqlite_backend)
  462. target = date(2024, 9, 6)
  463. await cal.set(target, 60, discount_rec=None)
  464. await cal.set(target, 80, discount_rec=None)
  465. readback = await cal._get_prices(target)
  466. assert readback.net == 80
  467. assert len(await cal.list(target, target)) == 1
  468. @pytest.mark.asyncio
  469. async def test_calprice_get_fills_defaults_for_missing_dates(sqlite_backend):
  470. """CalPrice.get returns a default price for dates with no event."""
  471. cal = CalPrice(backend=sqlite_backend)
  472. start = date(2024, 10, 1)
  473. end = date(2024, 10, 3)
  474. result = await cal.get(start, end)
  475. assert len(result) == 3
  476. for prices in result.values():
  477. assert prices is not None
  478. assert prices.net > 0
  479. @pytest.mark.asyncio
  480. async def test_calprice_list(sqlite_backend):
  481. cal = CalPrice(backend=sqlite_backend)
  482. start = date(2024, 10, 5)
  483. for i in range(3):
  484. await cal.set(start + timedelta(days=i), 70 + i, discount_rec=None)
  485. events = await cal.list(start, start + timedelta(days=2))
  486. assert len(events) == 3