Initial commit.
This commit is contained in:
commit
628e5c3823
26 changed files with 3478 additions and 0 deletions
151
tests/test_grocy.py
Normal file
151
tests/test_grocy.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from ocado_grocy.grocy import Journal, import_receipt
|
||||
from ocado_grocy.receipt import ImportError, Item, Receipt
|
||||
|
||||
|
||||
class FakeGrocy:
|
||||
url = "https://grocy.test/api"
|
||||
|
||||
def __init__(self, products=None, fail=False):
|
||||
self.products = products or []
|
||||
self.posts = []
|
||||
self.fail = fail
|
||||
|
||||
def get(self, path):
|
||||
if path == "/objects/products":
|
||||
return self.products.copy()
|
||||
if path == "/objects/product_barcodes":
|
||||
return []
|
||||
if path.startswith("/stock/products/"):
|
||||
return {"qu_conversion_factor_purchase_to_stock": 3}
|
||||
raise AssertionError(path)
|
||||
|
||||
def named_object(self, *args, **kwargs):
|
||||
return 1
|
||||
|
||||
def post(self, path, payload):
|
||||
self.posts.append((path, payload))
|
||||
if path == "/objects/products":
|
||||
self.products.append({"id": len(self.products) + 1, **payload})
|
||||
return {"created_object_id": len(self.products)}
|
||||
if self.fail and path.endswith("/add"):
|
||||
raise ImportError("Network failed after stock may have been written")
|
||||
return []
|
||||
|
||||
|
||||
def receipt():
|
||||
return Receipt("123", "2026-09-08", [Item("Apples", Decimal(2), Decimal("4.50"), "ocado123")])
|
||||
|
||||
|
||||
def test_import_creation_prices_and_duplicate_protection(tmp_path):
|
||||
api = FakeGrocy()
|
||||
journal = Journal(tmp_path / "state.sqlite3")
|
||||
assert import_receipt(receipt(), api, journal, {}) == 1
|
||||
assert api.posts[0][0] == "/objects/products"
|
||||
stock = api.posts[-1][1]
|
||||
assert stock["amount"] == 2
|
||||
assert stock["price"] == 2.25
|
||||
assert stock["purchased_date"] == "2026-09-08"
|
||||
before = len(api.posts)
|
||||
assert import_receipt(receipt(), api, journal, {}) == 0
|
||||
assert len(api.posts) == before
|
||||
|
||||
|
||||
def test_existing_product_conversion(tmp_path):
|
||||
api = FakeGrocy([{"id": 9, "name": "Apples", "qu_id_purchase": 1, "qu_id_stock": 2}])
|
||||
import_receipt(receipt(), api, Journal(tmp_path / "state.sqlite3"), {})
|
||||
assert len(api.posts) == 1
|
||||
assert api.posts[0][0] == "/stock/products/9/add"
|
||||
assert api.posts[0][1]["amount"] == 6
|
||||
assert api.posts[0][1]["price"] == .75
|
||||
|
||||
|
||||
def test_uncertain_write_not_retried(tmp_path):
|
||||
api = FakeGrocy(fail=True)
|
||||
journal = Journal(tmp_path / "state.sqlite3")
|
||||
with pytest.raises(ImportError, match="Network failed"):
|
||||
import_receipt(receipt(), api, journal, {})
|
||||
writes = len(api.posts)
|
||||
with pytest.raises(ImportError, match="uncertain"):
|
||||
import_receipt(receipt(), api, journal, {})
|
||||
assert len(api.posts) == writes
|
||||
|
||||
|
||||
def test_changed_receipt_not_reimported(tmp_path):
|
||||
api = FakeGrocy()
|
||||
journal = Journal(tmp_path / "state.sqlite3")
|
||||
import_receipt(receipt(), api, journal, {})
|
||||
changed = receipt()
|
||||
changed.items[0].quantity = Decimal(3)
|
||||
with pytest.raises(ImportError, match="changed"):
|
||||
import_receipt(changed, api, journal, {})
|
||||
|
||||
|
||||
def test_preflight_rejects_ambiguity_before_writes(tmp_path):
|
||||
api = FakeGrocy([{"id": 1, "name": "Apples"}, {"id": 2, "name": "Apples"}])
|
||||
with pytest.raises(ImportError, match="Ambiguous"):
|
||||
import_receipt(receipt(), api, Journal(tmp_path / "state.sqlite3"), {})
|
||||
assert not api.posts
|
||||
|
||||
|
||||
def test_same_product_two_lines_created_once(tmp_path):
|
||||
api = FakeGrocy()
|
||||
order = receipt()
|
||||
order.items.append(order.items[0])
|
||||
import_receipt(order, api, Journal(tmp_path / "state.sqlite3"), {})
|
||||
assert len([p for p, _ in api.posts if p == "/objects/products"]) == 1
|
||||
assert len([p for p, _ in api.posts if p.endswith("/add")]) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize('old_note', ['Ocado order 123, line 1.', 'Ocado 2026-09-08, line 1.'])
|
||||
def test_note_updates_never_recreate_deleted_stock(tmp_path, old_note):
|
||||
from copy import deepcopy
|
||||
from ocado_grocy.grocy import update_existing_notes
|
||||
class Api:
|
||||
url='https://test/api'
|
||||
def __init__(self):
|
||||
self.rows=[{'id':10,'product_id':1,'amount':2,'price':1.25,'open':0,
|
||||
'best_before_date':'2027-01-01','purchased_date':'2026-09-08',
|
||||
'location_id':1,'shopping_location_id':1,'note':old_note}]
|
||||
self.writes=[]
|
||||
def get(self,path):
|
||||
if path=='/stock/entry/10':
|
||||
return deepcopy(self.rows[0])
|
||||
assert path=='/objects/stock'
|
||||
return deepcopy(self.rows)
|
||||
def request(self,method,path,data):
|
||||
assert method=='PUT' and path=='/stock/entry/10'
|
||||
self.writes.append(data)
|
||||
self.rows[0].update(data)
|
||||
journal=Journal(tmp_path/'state.sqlite3')
|
||||
api=Api()
|
||||
journal.record(api.url,'123',1,'existing','done',1)
|
||||
journal.record(api.url,'123',2,'deleted','done',2) # Missing from live stock.
|
||||
assert update_existing_notes(api,journal)==1
|
||||
assert api.rows[0]['note']=='Ocado 2026-09-08'
|
||||
assert len(api.rows)==1
|
||||
assert api.rows[0]['amount']==2
|
||||
assert update_existing_notes(api,journal)==0
|
||||
assert len(api.writes)==1
|
||||
|
||||
|
||||
def test_note_update_skips_stock_deleted_after_listing(tmp_path):
|
||||
from ocado_grocy.grocy import update_existing_notes
|
||||
class Api:
|
||||
url='https://test/api'
|
||||
removed=False
|
||||
def get(self,path):
|
||||
if path=='/stock/entry/10':
|
||||
self.removed=True
|
||||
return None # Grocy returns JSON null for a deleted entry.
|
||||
assert path=='/objects/stock'
|
||||
return [] if self.removed else [{'id':10,'product_id':1,'note':'Ocado 2026-09-08, line 1.'}]
|
||||
def request(self,*args,**kwargs):
|
||||
pytest.fail('Must not write deleted stock')
|
||||
journal=Journal(tmp_path/'state.sqlite3')
|
||||
api=Api()
|
||||
journal.record(api.url,'123',1,'existing','done',1)
|
||||
assert update_existing_notes(api,journal)==0
|
||||
Loading…
Add table
Add a link
Reference in a new issue