Parsing
This commit is contained in:
parent
7741df79e9
commit
5c70a9faf0
3 changed files with 5312 additions and 0 deletions
5121
tests/sample_files/recipes/cheese-omelette.html
Normal file
5121
tests/sample_files/recipes/cheese-omelette.html
Normal file
File diff suppressed because one or more lines are too long
72
tests/test_ingredients_parse_api_v2.py
Normal file
72
tests/test_ingredients_parse_api_v2.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import unittest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import main
|
||||
from db import connect, create
|
||||
|
||||
|
||||
class TestIngredientsParseApiV2(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.conn = await connect(":memory:")
|
||||
await create(self.conn)
|
||||
|
||||
async def override_get_db():
|
||||
try:
|
||||
yield self.conn
|
||||
finally:
|
||||
pass
|
||||
|
||||
main.app.dependency_overrides[main.get_db] = override_get_db
|
||||
self.client = TestClient(main.app)
|
||||
|
||||
async def asyncTearDown(self):
|
||||
await self.conn.close()
|
||||
main.app.dependency_overrides.clear()
|
||||
|
||||
def test_parse_multiple_ingredients(self):
|
||||
params = {
|
||||
"ingredients": [
|
||||
"14oz milk powder",
|
||||
"2 cups flour",
|
||||
"1 tsp salt",
|
||||
"egg", # defaults to 1 Items
|
||||
]
|
||||
}
|
||||
r = self.client.get("/api/v1/recipes/ingredients/parse", params=params)
|
||||
assert r.status_code == 200, r.text
|
||||
arr = r.json()
|
||||
assert isinstance(arr, list)
|
||||
assert len(arr) == 4
|
||||
|
||||
# Basic shape checks
|
||||
for item in arr:
|
||||
assert "name" in item and isinstance(item["name"], str)
|
||||
assert "line" in item and isinstance(item["line"], str)
|
||||
assert "quantity" in item
|
||||
assert "unit" in item and isinstance(item["unit"], str)
|
||||
# Quantity should be a positive number
|
||||
assert float(item["quantity"]) > 0
|
||||
# The original sentence should round-trip into line
|
||||
assert len(item["line"]) >= len(item["name"]) >= 1
|
||||
|
||||
# Spot checks for unit/quantity normalization
|
||||
# 14oz milk powder
|
||||
oz = arr[0]
|
||||
assert float(oz["quantity"]) == 14.0
|
||||
assert oz["unit"] == "Ounce"
|
||||
assert "milk" in oz["name"].lower()
|
||||
|
||||
cups = arr[1]
|
||||
assert float(cups["quantity"]) == 2.0
|
||||
assert cups["unit"] == "Cup"
|
||||
assert cups["name"].lower() == "flour"
|
||||
|
||||
tsp = arr[2]
|
||||
assert float(tsp["quantity"]) == 1.0
|
||||
assert tsp["unit"] == "Teaspoon"
|
||||
assert tsp["name"].lower() == "salt"
|
||||
|
||||
egg = arr[3]
|
||||
assert float(egg["quantity"]) == 1.0
|
||||
assert egg["unit"] == "Items"
|
||||
assert egg["name"].lower() == "egg"
|
||||
119
tests/test_recipes_parse_from_url_integration_v2.py
Normal file
119
tests/test_recipes_parse_from_url_integration_v2.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import unittest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import main
|
||||
from db import connect, create
|
||||
from scripts.migration_to_households import run_migration
|
||||
|
||||
|
||||
SAMPLE_URL = "https://www.allrecipes.com/recipe/262696/cheese-omelette/"
|
||||
with open("tests/sample_files/recipes/cheese-omelette.html", "r", encoding="utf-8") as f:
|
||||
SAMPLE_HTML = f.read()
|
||||
|
||||
|
||||
class TestRecipesParseFromUrlIntegrationV2(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.conn = await connect(":memory:")
|
||||
await create(self.conn)
|
||||
await run_migration(self.conn)
|
||||
|
||||
async def override_get_db():
|
||||
try:
|
||||
yield self.conn
|
||||
finally:
|
||||
pass
|
||||
|
||||
main.app.dependency_overrides[main.get_db] = override_get_db
|
||||
self.client = TestClient(main.app)
|
||||
|
||||
# Register user and create household
|
||||
r = self.client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={"email": "scrape@test.com", "password": "pw", "displayName": "Scrape"},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
token = r.json()["accessToken"]
|
||||
self.headers = {"Authorization": f"Bearer {token}"}
|
||||
r2 = self.client.post("/api/v1/households", headers=self.headers, json={"name": "H"})
|
||||
assert r2.status_code == 200, r2.text
|
||||
self.slug = r2.json()["slug"]
|
||||
|
||||
async def asyncTearDown(self):
|
||||
await self.conn.close()
|
||||
main.app.dependency_overrides.clear()
|
||||
|
||||
def test_parse_from_url_with_mocked_html(self):
|
||||
# Monkeypatch httpx.AsyncClient.get used in scraper
|
||||
import recipes.scraping as scraping
|
||||
|
||||
class DummyResp:
|
||||
def __init__(self, status_code=200, text=""):
|
||||
self.status_code = status_code
|
||||
self.text = text
|
||||
|
||||
class DummyClient:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def get(self, url, headers=None, follow_redirects=False):
|
||||
assert url == SAMPLE_URL
|
||||
return DummyResp(200, SAMPLE_HTML)
|
||||
|
||||
orig_client = scraping.httpx.AsyncClient
|
||||
scraping.httpx.AsyncClient = DummyClient
|
||||
try:
|
||||
r = self.client.post(
|
||||
f"/api/v1/households/{self.slug}/recipes/parse-from-url",
|
||||
headers=self.headers,
|
||||
json={"url": SAMPLE_URL},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
data = r.json()
|
||||
assert data.get("@type") in ("Recipe", ["Recipe"]) # accept single or list
|
||||
# JSON-LD should contain ingredients and instructions
|
||||
assert isinstance(data.get("recipeIngredient"), list)
|
||||
assert len(data["recipeIngredient"]) >= 1
|
||||
# The title should mention omelette
|
||||
assert "name" in data and "omelette" in data["name"].lower()
|
||||
# Optional: if author or image present, assert types
|
||||
if "image" in data:
|
||||
assert isinstance(data["image"], (str, list, dict))
|
||||
if "author" in data:
|
||||
assert isinstance(data["author"], (str, list, dict))
|
||||
finally:
|
||||
scraping.httpx.AsyncClient = orig_client
|
||||
|
||||
def test_parse_from_url_not_found(self):
|
||||
import recipes.scraping as scraping
|
||||
|
||||
class DummyResp:
|
||||
def __init__(self, status_code=404, text=""):
|
||||
self.status_code = status_code
|
||||
self.text = text
|
||||
|
||||
class DummyClient:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def get(self, url, headers=None, follow_redirects=False):
|
||||
return DummyResp(404, "")
|
||||
|
||||
orig_client = scraping.httpx.AsyncClient
|
||||
scraping.httpx.AsyncClient = DummyClient
|
||||
try:
|
||||
r = self.client.post(
|
||||
f"/api/v1/households/{self.slug}/recipes/parse-from-url",
|
||||
headers=self.headers,
|
||||
json={"url": SAMPLE_URL},
|
||||
)
|
||||
assert r.status_code == 404, r.text
|
||||
body = r.json()
|
||||
assert body.get("status") == 404
|
||||
finally:
|
||||
scraping.httpx.AsyncClient = orig_client
|
||||
Loading…
Reference in a new issue