Limit order discovery and Open Food Facts searches
This commit is contained in:
parent
628e5c3823
commit
41772b4d1a
5 changed files with 185 additions and 78 deletions
|
|
@ -5,7 +5,8 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from ocado_grocy.history import order_links,private_json,cached_orders,select_orders,OrderLink
|
||||
from ocado_grocy.history import (order_links,private_json,cached_orders,select_orders,OrderLink,
|
||||
discover_orders,save_order_manifest)
|
||||
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,
|
||||
|
|
@ -15,6 +16,76 @@ from test_grocy import FakeGrocy,receipt
|
|||
FIXTURE=Path(__file__).parent/'fixtures'/'history_receipt.json'
|
||||
|
||||
|
||||
class HistoryPage:
|
||||
def __init__(self, batches, end_marker=True):
|
||||
self.batches = batches
|
||||
self.index = 0
|
||||
self.end_marker = end_marker
|
||||
self.mouse = self
|
||||
|
||||
def locator(self, selector):
|
||||
page = self
|
||||
class Locator:
|
||||
def count(self):
|
||||
if selector == 'a[href*="/orders/"]':
|
||||
return len(page.batches[page.index])
|
||||
return int(page.end_marker and page.index == len(page.batches) - 1)
|
||||
@property
|
||||
def last(self):
|
||||
return self
|
||||
def scroll_into_view_if_needed(self):
|
||||
pass
|
||||
return Locator()
|
||||
|
||||
def get_by_role(self, *args, **kwargs):
|
||||
return self.locator('button')
|
||||
|
||||
def content(self):
|
||||
return '<html>' + ''.join(
|
||||
f'<a href="/orders/{order}/details">Sep 8, 2026 Delivered</a>'
|
||||
for order in self.batches[self.index]) + '</html>'
|
||||
|
||||
def wheel(self, *args):
|
||||
self.index = min(self.index + 1, len(self.batches) - 1)
|
||||
|
||||
def wait_for_timeout(self, milliseconds):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize('batches,completed,expected_index', [
|
||||
([['3', '2'], ['3', '2', '1']], {'2', '1'}, 0),
|
||||
([['3'], ['3', '2'], ['3', '2', '1']], {'2', '1'}, 1),
|
||||
([['3'], ['3', '2'], ['3', '2', '1']], set(), 2),
|
||||
])
|
||||
def test_discovery_stops_at_completed_batch_or_end(batches, completed, expected_index):
|
||||
page = HistoryPage(batches)
|
||||
links = discover_orders(page, lambda message: None, completed=completed)
|
||||
assert page.index == expected_index
|
||||
assert [link.order_id for link in links] == batches[expected_index]
|
||||
|
||||
|
||||
def test_explicit_discovery_searches_past_completed_orders():
|
||||
page = HistoryPage([['3'], ['3', '2'], ['3', '2', '1']], end_marker=False)
|
||||
links = discover_orders(page, lambda message: None, completed={'3'}, order_ids=('2',))
|
||||
assert page.index == 1
|
||||
assert [link.order_id for link in links] == ['3', '2']
|
||||
|
||||
|
||||
def test_discovery_still_rejects_stalled_history():
|
||||
with pytest.raises(ImportError, match='stopped loading'):
|
||||
discover_orders(HistoryPage([['3']], end_marker=False), lambda message: None)
|
||||
|
||||
|
||||
def test_incremental_manifest_preserves_older_pending_orders(tmp_path):
|
||||
old = [OrderLink('2', '2026-09-07', 'old-url'), OrderLink('1', '2026-09-06', 'url')]
|
||||
save_order_manifest(tmp_path, old)
|
||||
new = [OrderLink('3', '2026-09-08', 'url'), replace(old[0], url='updated-url')]
|
||||
merged = save_order_manifest(tmp_path, new)
|
||||
assert merged == new + old[1:]
|
||||
assert [link.order_id for link in select_orders(merged, completed={'2'})] == ['3', '1']
|
||||
assert len(json.loads((tmp_path/'orders.json').read_text())) == 3
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -83,19 +83,54 @@ def test_two_exact_barcodes_are_ambiguous():
|
|||
assert match is None
|
||||
|
||||
|
||||
def test_truncated_catalog_is_split_and_pages_must_be_complete(tmp_path):
|
||||
def test_product_search_is_bounded_and_includes_name_and_brand(tmp_path):
|
||||
from ocado_grocy.openfoodfacts import OpenFoodFacts
|
||||
calls = []
|
||||
class OFF(OpenFoodFacts):
|
||||
def fetch(self, path, params, **kwargs):
|
||||
if params['q'] == 'brands_tags:(a OR b)':
|
||||
return {'is_count_exact':False, 'hits':[]}
|
||||
code = '00000048' if params['q'] == 'brands_tags:(a)' else '00000055'
|
||||
return {'is_count_exact':True, 'count':1, 'page_count':1, 'hits':[{'code':code,'brands':['a']}]}
|
||||
off = OFF(tmp_path, 'test@example.com')
|
||||
assert len(off.catalog({'a','b'})) == 2
|
||||
off.fetch = lambda *args, **kwargs: {'is_count_exact':True, 'count':2, 'page_count':1, 'hits':[]}
|
||||
with pytest.raises(ValueError, match='Incomplete'):
|
||||
off.catalog({'a'})
|
||||
calls.append(params)
|
||||
return {'is_count_exact': True, 'count': 9000,
|
||||
'hits': [{**candidate(), 'brands': ['Marks & Spencer']}]}
|
||||
candidates, result = OFF(tmp_path, 'test@example.com').search(product(), metadata())
|
||||
assert len(calls) == 1
|
||||
assert calls[0]['page_size'] == 100 and calls[0]['page'] == 1
|
||||
assert 'ginger' in calls[0]['q'] and 'snaps' in calls[0]['q']
|
||||
assert 'marks-spencer' in calls[0]['q']
|
||||
assert candidates[candidate()['code']]['brands'] == 'Marks & Spencer'
|
||||
assert not result['search_complete']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('complete', [True, False])
|
||||
def test_matching_searches_only_selected_products_and_requires_complete_results(tmp_path, monkeypatch, complete):
|
||||
from ocado_grocy import openfoodfacts as module
|
||||
searched = []
|
||||
class OFF:
|
||||
def __init__(self, *args): pass
|
||||
def search(self, p, m, offline):
|
||||
searched.append(p['id'])
|
||||
return {candidate()['code']: candidate()}, {'search_complete': complete}
|
||||
class Client:
|
||||
def get(self, path):
|
||||
assert path == '/objects/product_barcodes'
|
||||
return []
|
||||
monkeypatch.setattr(module, 'OpenFoodFacts', OFF)
|
||||
nonfood = {**product(), 'id': 43}
|
||||
nonfood['description'] = replace_metadata(nonfood['description'],
|
||||
{**metadata(), 'ocado': {'brand': 'Miniml'}})
|
||||
result = module.run_openfoodfacts(Client(), [nonfood, product(), {**product(), 'id': 44}],
|
||||
tmp_path, 'test@example.com', apply=False, limit=1)
|
||||
assert searched == [42]
|
||||
assert [r['status'] for r in result['products']] == [
|
||||
'non_food', 'matched' if complete else 'review', 'not_searched']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('error', [{'timed_out': True}, {'warnings': ['partial']}, {'errors': ['bad query']}])
|
||||
def test_failed_search_is_not_reported_as_no_match(tmp_path, error):
|
||||
from ocado_grocy.openfoodfacts import OpenFoodFacts
|
||||
off = OpenFoodFacts(tmp_path, 'test@example.com')
|
||||
off.fetch = lambda *args, **kwargs: error
|
||||
with pytest.raises(ValueError, match='Incomplete product search'):
|
||||
off.search(product(), metadata())
|
||||
|
||||
|
||||
def test_ms_food_brand_and_missing_barcode_recovery():
|
||||
|
|
@ -116,7 +151,7 @@ def test_established_barcode_match_survives_a_missing_catalog_entry(tmp_path, mo
|
|||
p['description'] = replace_metadata(p['description'], m)
|
||||
class OFF:
|
||||
def __init__(self, *a): pass
|
||||
def catalog(self, *a): return []
|
||||
def search(self, *a): pytest.fail("Established match must not trigger search")
|
||||
def product(self, code, offline):
|
||||
assert code == candidate()['code']
|
||||
return candidate()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue