mitai-jinkendo/backend/tests/test_food_mapping.py
Lars b37005be01
All checks were successful
Deploy Development / deploy (push) Successful in 59s
Build Test / pytest-backend (push) Successful in 5s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 22s
feat: Listen-Tab und sichtbare Zutatenlisten-Verwaltung
Listen anlegen, bearbeiten und importieren liegt unter Ernährung.
Tagebuchzeilen werden auch mit Mengen-Prefix verknüpft.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 12:40:53 +02:00

163 lines
5.7 KiB
Python

from datetime import date
from csv_parser.executor import guess_nutrition_item_fields
from data_layer.food_mapping import (
fetch_unmapped_rows,
filter_unmapped_since,
list_quantity_units,
list_unmapped_payload,
merge_unmapped_rows,
names_match_list,
normalize_food_name,
parse_quantity,
parse_quantity_g,
primary_search_query,
resolve_quantity,
)
from data_layer.nutrition_items import macros_differ
def test_primary_query_keeps_decimal_comma():
assert primary_search_query("Joghurt 9,5%") == "Joghurt 9,5%"
assert primary_search_query("Joghurt, aus Kuhmilch") == "Joghurt"
def test_normalize_strips_leading_quantity():
assert normalize_food_name("50 g Hähnchen") == "hähnchen"
assert normalize_food_name(" Vollmilch 3,5% ") == "vollmilch 3.5%"
assert normalize_food_name("1 ml Olivenöl") == normalize_food_name("2 ml Olivenöl") == "olivenöl"
def test_merge_unmapped_collapses_quantity_variants():
rows = merge_unmapped_rows([
{"source_name_raw": "1 ml Olivenöl", "source_name_normalized": "1 ml olivenöl", "count": 3, "kind": "diary"},
{"source_name_raw": "2 ml Olivenöl", "source_name_normalized": "2 ml olivenöl", "count": 1, "kind": "diary"},
])
assert len(rows) == 1
assert rows[0]["source_name_normalized"] == "olivenöl"
assert rows[0]["count"] == 4
assert rows[0]["source_name_raw"].lower() == "olivenöl"
def test_parse_quantity_g():
assert parse_quantity_g("150 g") == 150.0
assert parse_quantity_g("150") == 150.0
assert parse_quantity_g("1 kg") == 1000.0
assert parse_quantity_g("5 ml") == 5.0
assert parse_quantity_g("1 Stück") is None
assert parse_quantity_g("1 Stück", 60) == 60.0
assert parse_quantity_g("2 EL") == 30.0
assert parse_quantity("1 Scheibe")["needs_unit_map"] is True
assert parse_quantity("2 EL")["unit"] == "el"
def test_resolve_quantity_household_units():
two_tl = resolve_quantity(quantity_amount=2, source_unit="tl")
assert two_tl["quantity_g"] == 10.0
assert two_tl["quantity_raw"] == "2 TL"
pinch = resolve_quantity(quantity_amount=1, source_unit="prise")
assert pinch["quantity_g"] == 0.3
slice_ = resolve_quantity(quantity_amount=1, source_unit="stück")
assert slice_["quantity_g"] is None
assert slice_["needs_unit_map"] is True
assert resolve_quantity(quantity_amount=1, source_unit="stück", grams_per_unit=60)["quantity_g"] == 60.0
ids = [u["id"] for u in list_quantity_units()]
assert "tl" in ids and "prise" in ids
def test_guess_fddb_bezeichnung_without_template_mapping():
name, qty = guess_nutrition_item_fields({
"bezeichnung": "50 g Hähnchen",
"menge": "50 g",
"kj": "800",
})
assert name == "50 g Hähnchen"
assert qty == "50 g"
def test_recent_window_drops_old_and_dateless_foods():
today = date(2026, 9, 12)
rows = [
{"source_name_raw": "Haferflocken", "last_date": "2026-09-10", "count": 2},
{"source_name_raw": "Weißbrot", "last_date": "2026-01-02", "count": 40},
{"source_name_raw": "Altes Rezept-Salz", "last_date": None, "count": 1},
]
recent = filter_unmapped_since(rows, 28, today=today)
assert [r["source_name_raw"] for r in recent] == ["Haferflocken"]
assert len(filter_unmapped_since(rows, 0, today=today)) == 3
class _FakeCur:
def __init__(self, batches):
self.batches = list(batches)
self._i = 0
self._rows = []
def execute(self, sql, params=None):
self._rows = self.batches[self._i]
self._i += 1
def fetchall(self):
return self._rows
def test_fetch_unmapped_keeps_ingredient_dates_and_skips_mapped():
cur = _FakeCur([
[{"source_name_normalized": "haferflocken"}],
[],
[],
[
{
"source_name_raw": "Salz",
"source_name_normalized": "salz",
"count": 2,
"first_date": date(2026, 9, 10),
"last_date": date(2026, 9, 11),
"sample_quantity_raw": "1 g",
},
{
"source_name_raw": "Haferflocken",
"source_name_normalized": "haferflocken",
"count": 1,
"first_date": date(2026, 9, 1),
"last_date": date(2026, 9, 8),
"sample_quantity_raw": "40 g",
},
],
])
rows = fetch_unmapped_rows(cur, "p")
assert [r["source_name_normalized"] for r in rows] == ["salz"]
assert rows[0]["last_date"] == "2026-09-11"
assert rows[0]["kind"] == "recipe_ingredient"
def test_list_name_matches_despite_quantity_prefix():
assert names_match_list("50 g Müsli Mix", "50 g müsli mix", "müsli mix")
assert names_match_list("Müsli Mix", "müsli mix", "müsli mix")
assert not names_match_list("Haferflocken", "haferflocken", "müsli mix")
def test_unmapped_meta_includes_recent_ingredients():
batches = [
[],
[],
[],
[{
"source_name_raw": "Salz",
"source_name_normalized": "salz",
"count": 1,
"first_date": date(2026, 9, 10),
"last_date": date(2026, 9, 11),
"sample_quantity_raw": "1 g",
}],
]
payload = list_unmapped_payload(_FakeCur(batches), "p", since_days=0, meta=True)
assert payload["total"] == 1
assert payload["count"] == 1
assert payload["items"][0]["source_name_normalized"] == "salz"
def test_macros_differ_rounds():
assert not macros_differ({"kcal": 1.04, "protein_g": 0, "fat_g": 0, "carbs_g": 0}, {"kcal": 1.0, "protein_g": 0, "fat_g": 0, "carbs_g": 0})
assert macros_differ({"kcal": 10, "protein_g": 0, "fat_g": 0, "carbs_g": 0}, {"kcal": 11, "protein_g": 0, "fat_g": 0, "carbs_g": 0})