from dataclasses import replace from decimal import Decimal import json from pathlib import Path import pytest from ocado_grocy.history import order_links,private_json,cached_orders,select_orders,OrderLink from ocado_grocy.pantry import classify from ocado_grocy.receipt import ImportError,Item,parse_ocado_order from ocado_grocy.grocy import (Journal,import_receipt,description,refresh_imported_products, item_fingerprint,align_recorded_lines) from test_grocy import FakeGrocy,receipt FIXTURE=Path(__file__).parent/'fixtures'/'history_receipt.json' def test_storage_and_categories_select_without_a_product_list(): order=parse_ocado_order(json.loads(FIXTURE.read_text())) assert sum(classify(i).include for i in order.items)==9 assert not any(classify(i).uncertain for i in order.items) banana=next(i for i in order.items if 'Bananas' in i.name) assert banana.metadata['storage_type']=='CUPBOARD' assert not classify(banana).include feta=next(i for i in order.items if 'Feta' in i.name) assert feta.best_before.startswith('2030') assert not classify(feta).include def test_names_do_not_override_missing_metadata(): item=Item('Shelf-stable sounding Chocolate Biscuits',Decimal(1),Decimal(1),product_id='55') assert classify(item).uncertain assert not classify(item).include assert classify(item,{'55':True}).include with pytest.raises(ValueError):classify(item,{'55':'yes'}) def test_cold_storage_takes_precedence_over_category(): item=Item('Chocolate pudding',Decimal(1),Decimal(1),metadata={ 'storage_type':'FRIDGE','ocado':{'categoryPath':['Treats & Snacks']}}) assert not classify(item).include def test_order_links_ignore_cancelled_and_future_and_keep_year(): links=order_links(''' Sep 8, 2026 5:00pm£56.75Delivered Sep 8, 2025 5:00pm View order Delivered Sep 8, 2025 5:00pm You cancelled this order Fri 11 Sep You're editing this order''') assert [(r.order_id,r.purchased_date) for r in links]==[('1','2026-09-08'),('2','2025-09-08')] def test_selection_does_not_change_complete_manifest(tmp_path): links=[OrderLink('1','2026-09-08','https://www.ocado.com/orders/1/details'), OrderLink('2','2025-09-08','https://www.ocado.com/orders/2/details')] assert select_orders(links,limit=1)==links[:1] assert select_orders(links,order_ids=('2',))==links[1:] assert len(links)==2 with pytest.raises(ImportError,match='not found'):select_orders(links,order_ids=('missing',)) def test_partial_import_preserves_original_line_numbers(tmp_path): order=receipt() order.items.extend([Item('Milk',Decimal(1),Decimal(1)),Item('Tea',Decimal(1),Decimal(2))]) api=FakeGrocy() journal=Journal(tmp_path/'journal.sqlite3') assert import_receipt(order,api,journal,{},include_lines={3})==1 assert api.posts[-1][1]['note']=='Ocado 2026-09-08 Expiry unknown.' assert journal.db.execute('SELECT line FROM imports').fetchone()[0]==3 assert import_receipt(order,api,journal,{},include_lines={3})==0 assert import_receipt(order,api,journal,{})==2 assert journal.db.execute('SELECT COUNT(*) FROM imports').fetchone()[0]==3 def test_cache_permissions_and_missing_order(tmp_path): private_json(tmp_path/'orders.json',[{'order_id':'1','purchased_date':'2026-09-08','url':'https://www.ocado.com/orders/1/details'}]) assert (tmp_path/'orders.json').stat().st_mode & 0o777==0o600 orders,failures=cached_orders(tmp_path) assert not orders and failures[0]['order_id']=='1' @pytest.mark.parametrize("sanitized", [False, True]) def test_refresh_existing_includes_perishables_without_booking_stock(tmp_path, sanitized): order=parse_ocado_order(json.loads(FIXTURE.read_text())) index,item=next((i,item) for i,item in enumerate(order.items,1) if 'Lettuce' in item.name) old=replace(item,metadata={'additional_details':{'brand':'Preserved'}, 'openfoodfacts':{'code':'00000048'}}) class Api: url='https://test/api' def __init__(self): self.product={'id':1,'name':item.name,'description':description(old)} if sanitized: self.product['description'] = self.product['description'].replace('
', '').replace('
', '') self.writes=[] def get(self,path): assert path=='/objects/products/1' return self.product def request(self,method,path,data): assert method=='PUT' and path=='/objects/products/1' self.writes.append(data) self.product.update(data) api=Api() journal=Journal(tmp_path/'state.sqlite3') journal.record(api.url,order.order_id,index,item_fingerprint(order,item),'done',1) assert not classify(item).include assert refresh_imported_products(order,api,journal)==1 assert 'FRIDGE' in api.product['description'] assert 'Preserved' in api.product['description'] assert '00000048' in api.product['description'] assert refresh_imported_products(order,api,journal)==0 assert len(api.writes)==1 def test_reordered_receipt_recovers_original_journal_lines(tmp_path): original=receipt() original.items.append(Item('Tea',Decimal(1),Decimal(2))) api=FakeGrocy() journal=Journal(tmp_path/'state.sqlite3') import_receipt(original,api,journal,{}) reordered=replace(original,items=list(reversed(original.items))) restored=align_recorded_lines(reordered,api.url,journal) assert restored.items==original.items assert import_receipt(restored,api,journal,{})==0 @pytest.mark.parametrize("flags,expected", [([],21), (["--all-products"],21), (["--shelf-stable-only"],9)]) def test_cached_cli_preview_does_not_write_stock_or_use_browser(tmp_path,monkeypatch,flags,expected): from click.testing import CliRunner from ocado_grocy.cli import main order=parse_ocado_order(json.loads(FIXTURE.read_text())) private_json(tmp_path/'orders.json',[{'order_id':order.order_id,'purchased_date':order.purchased_date, 'url':f'https://www.ocado.com/orders/{order.order_id}/details'}]) private_json(tmp_path/f'{order.order_id}.json',order.export()) monkeypatch.setattr('requests.Session.request',lambda *a,**kw:pytest.fail('Unexpected Grocy request')) monkeypatch.setattr('ocado_grocy.history.download_orders',lambda *a,**kw:pytest.fail('Unexpected browser')) result=CliRunner().invoke(main,flags+['--cached','--dry-run','--archive',str(tmp_path), '--config',str(tmp_path/'missing-config.toml')]) assert result.exit_code==0,result.output report=json.loads((tmp_path/'report.json').read_text()) assert sum(i['include'] for i in report['items'])==expected assert report['imported_lines']==0