FunkyJuiceRecipes/tests/test_models_daos.py

182 lines
5.7 KiB
Python
Raw Normal View History

2025-12-18 20:30:09 +00:00
from __future__ import annotations
import sqlite3
from pathlib import Path
from types import SimpleNamespace
import pytest
from peewee import IntegrityError, DoesNotExist
from funkyjuicerecipes.data.db import init_database, DB_FILENAME
from funkyjuicerecipes.data.dao.flavors import FlavorRepository
from funkyjuicerecipes.data.dao.recipes import RecipeRepository
from funkyjuicerecipes.data.models.flavor import Flavor
from funkyjuicerecipes.data.models.recipe_flavor import RecipeFlavor
class DummyPaths(SimpleNamespace):
@property
def data(self) -> Path: # type: ignore[override]
return self._data
@data.setter
def data(self, value: Path) -> None:
self._data = Path(value)
class DummyApp(SimpleNamespace):
def __init__(self, data_dir: Path) -> None:
super().__init__()
self.paths = DummyPaths()
self.paths.data = data_dir
def _init_db(tmp_path: Path):
app = DummyApp(tmp_path / "data")
db = init_database(app)
return app, db
def test_flavor_uniqueness_nocase(tmp_path: Path):
app, db = _init_db(tmp_path)
flavors = FlavorRepository()
f1 = flavors.create(name="Strawberry", company="TPA", base="PG")
assert f1.id is not None
with pytest.raises(IntegrityError):
flavors.create(name="strawberry", company="tpa", base="PG")
def test_flavor_soft_delete_and_listing_filters(tmp_path: Path):
app, db = _init_db(tmp_path)
flavors = FlavorRepository()
f1 = flavors.create(name="Vanilla", company="CAP", base="PG")
f2 = flavors.create(name="Cookie", company="FA", base="PG")
# default list excludes deleted
rows = flavors.list()
assert [f.name for f in rows] == ["Cookie", "Vanilla"] or [f.name for f in rows] == ["Vanilla", "Cookie"]
# soft delete f2
assert flavors.soft_delete(f2.id) is True
rows_default = flavors.list()
assert all(not r.is_deleted for r in rows_default)
assert {r.id for r in rows_default} == {f1.id}
rows_only_deleted = flavors.list(only_deleted=True)
assert {r.id for r in rows_only_deleted} == {f2.id}
rows_include_deleted = flavors.list(include_deleted=True)
assert {r.id for r in rows_include_deleted} == {f1.id, f2.id}
# undelete brings it back to default list
assert flavors.undelete(f2.id) is True
rows_default2 = flavors.list()
assert {r.id for r in rows_default2} == {f1.id, f2.id}
# hard delete safety: referenced flavor cannot be deleted
recipes = RecipeRepository()
r = recipes.create(
name="Test Mix",
size_ml=30,
base_pg_pct=50,
base_vg_pct=50,
nic_pct=0,
nic_base="PG",
ingredients=[(f1.id, 5.0)],
)
# now f1 is referenced; hard delete should be blocked
assert flavors.hard_delete_when_safe(f1.id) is False
# f2 is not referenced; can be hard-deleted
assert flavors.hard_delete_when_safe(f2.id) is True
with pytest.raises(DoesNotExist):
Flavor.get_by_id(f2.id)
def test_recipe_creation_and_get_with_ingredients_includes_soft_deleted_flavors(tmp_path: Path):
app, db = _init_db(tmp_path)
flavors = FlavorRepository()
recipes = RecipeRepository()
fa = flavors.create(name="Apple", company="FA", base="PG")
fb = flavors.create(name="Berry", company="CAP", base="VG")
# Soft delete one flavor
flavors.soft_delete(fb.id)
r = recipes.create(
name="Fruit",
size_ml=60,
base_pg_pct=70,
base_vg_pct=30,
nic_pct=3,
nic_base="PG",
ingredients=[(fa.id, 4.0), (fb.id, 2.0)],
)
r2, pairs = recipes.get_with_ingredients(r.id)
assert r2.id == r.id
# Both flavors should be present, including the soft-deleted one
got = {fl.id: pct for (fl, pct) in pairs}
assert got == {fa.id: 4.0, fb.id: 2.0}
# Verify the deleted flag is visible on the soft-deleted flavor
deleted_flavor = next(fl for (fl, pct) in pairs if fl.id == fb.id)
assert deleted_flavor.is_deleted is True
def test_replace_ingredients_diff_behavior(tmp_path: Path):
app, db = _init_db(tmp_path)
flavors = FlavorRepository()
recipes = RecipeRepository()
f1 = flavors.create(name="Lemon", company="FA", base="PG")
f2 = flavors.create(name="Lime", company="FA", base="PG")
f3 = flavors.create(name="Orange", company="FA", base="PG")
r = recipes.create(
name="Citrus",
size_ml=30,
base_pg_pct=50,
base_vg_pct=50,
nic_pct=0,
nic_base="PG",
ingredients=[(f1.id, 3.0), (f2.id, 2.0)],
)
# Capture initial rows
rf_rows = list(
RecipeFlavor.select().where(RecipeFlavor.recipe == r.id).order_by(RecipeFlavor.id)
)
assert len(rf_rows) == 2
rf_f1 = next(x for x in rf_rows if x.flavor_id == f1.id)
rf_f2 = next(x for x in rf_rows if x.flavor_id == f2.id)
# Now: remove f1, update f2 pct, add f3
recipes.replace_ingredients(r.id, [(f2.id, 5.5), (f3.id, 1.0)])
rf_rows_after = list(
RecipeFlavor.select().where(RecipeFlavor.recipe == r.id).order_by(RecipeFlavor.id)
)
assert len(rf_rows_after) == 2
flavors_after = {x.flavor_id for x in rf_rows_after}
assert flavors_after == {f2.id, f3.id}
# f2 row should be updated in-place (same id) with new pct
rf_f2_after = next(x for x in rf_rows_after if x.flavor_id == f2.id)
assert rf_f2_after.id == rf_f2.id
assert pytest.approx(rf_f2_after.pct, rel=1e-9) == 5.5
# f1 row should be gone
assert not RecipeFlavor.select().where(
(RecipeFlavor.recipe == r.id) & (RecipeFlavor.flavor == f1.id)
).exists()
# f3 should be newly present
assert RecipeFlavor.select().where(
(RecipeFlavor.recipe == r.id) & (RecipeFlavor.flavor == f3.id)
).exists()