Files
site/WESP_REL/tests/test_recipe_sync_enqueue.py
2026-07-17 12:57:18 +03:00

355 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Постановка задач sync_queue при операциях с рецептами и helper enqueue_sync_queue_task."""
import os
import tempfile
import unittest
import uuid
from sqlalchemy import func, select
from app import create_app, db
from app.models import Component, Ingredient, Recipe, SyncQueue, UnloadingGroup
from app.services.recipe_update_service import RecipeUpdateError, update_recipe_from_payload
from app.services.sync_manager import enqueue_sync_queue_task
from config import TestingConfig
from tests.helpers.mill_recipe_fixtures import (
count_sync_tasks,
create_component,
create_recipe_with_children,
group_payload_from_row,
ingredient_payload_from_row,
recipe_update_payload,
sync_task_exists,
)
class RecipeSyncTestConfig(TestingConfig):
_TMP_DIR = tempfile.mkdtemp(prefix="wesp-recipe-sync-tests-")
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(_TMP_DIR, 'recipes_test.db')}"
SQLALCHEMY_BINDS = {"reports": f"sqlite:///{os.path.join(_TMP_DIR, 'reports_test.db')}"}
class RecipeSyncEnqueueTests(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app(RecipeSyncTestConfig)
self.ctx = self.app.app_context()
self.ctx.push()
db.create_all()
def tearDown(self) -> None:
db.session.remove()
db.drop_all()
self.ctx.pop()
def test_enqueue_sync_queue_task_creates_pending(self) -> None:
rid = str(uuid.uuid4())
enqueue_sync_queue_task("recipe", rid, "update", priority=2)
db.session.commit()
row = db.session.execute(
select(SyncQueue).where(
SyncQueue.table_name == "recipe",
SyncQueue.record_id == rid,
SyncQueue.action == "update",
)
).scalar_one()
self.assertEqual(row.status, "pending")
self.assertEqual(row.priority, 2)
def test_enqueue_idempotent_revives_pending(self) -> None:
rid = str(uuid.uuid4())
enqueue_sync_queue_task("recipe", rid, "update", priority=2)
db.session.commit()
enqueue_sync_queue_task("recipe", rid, "update", priority=3)
db.session.commit()
n = db.session.scalar(
select(func.count()).select_from(SyncQueue).where(SyncQueue.record_id == rid)
)
self.assertEqual(n, 1)
row = db.session.execute(
select(SyncQueue).where(SyncQueue.record_id == rid)
).scalar_one()
self.assertEqual(row.status, "pending")
self.assertEqual(row.priority, 3)
def test_update_recipe_from_payload_enqueues_sync_rows(self) -> None:
comp = Component(
id=str(uuid.uuid4()),
name="Корм А",
type="mix",
dry_matter=50.0,
protein=0.0,
energy=0.0,
price=0.0,
)
db.session.add(comp)
recipe = Recipe(
id=str(uuid.uuid4()),
name="Тест",
heads_per_trip=10,
mixing_time=5,
trip_percent=100.0,
)
db.session.add(recipe)
db.session.flush()
ing = Ingredient(
name=comp.name,
weight_per_head=2.0,
amount=20.0,
dry_matter=50.0,
component_id=comp.id,
order=1,
recipe_id=recipe.id,
created_by="system",
updated_by="system",
)
grp = UnloadingGroup(
name="Г1",
distribution_type="percent",
value=100.0,
weight=20.0,
order=1,
recipe_id=recipe.id,
created_by="system",
updated_by="system",
)
db.session.add_all([ing, grp])
db.session.commit()
payload = {
"name": "Тест 2",
"heads_count": 10,
"mixing_time": 5,
"trip_percent": 100.0,
"dry_matter_locked": False,
"unloading_link_broken": False,
"ingredients": [
{
"component_id": comp.id,
"weight_per_head": 2.0,
"amount": 20.0,
"dry_matter": 50.0,
"order": 1,
}
],
"unloading_groups": [
{
"name": "Г1",
"distribution_type": "percent",
"value": 100.0,
"weight": 20.0,
"order": 1,
}
],
}
update_recipe_from_payload(recipe.id, payload)
db.session.commit()
recipe_tasks = db.session.scalar(
select(func.count()).select_from(SyncQueue).where(
SyncQueue.table_name == "recipe",
SyncQueue.record_id == recipe.id,
SyncQueue.action == "update",
)
)
self.assertGreaterEqual(recipe_tasks, 1)
ing_row = db.session.execute(
select(Ingredient).where(
Ingredient.recipe_id == recipe.id,
Ingredient.is_deleted.is_(False),
)
).scalar_one()
ing_sync = db.session.scalar(
select(func.count()).select_from(SyncQueue).where(
SyncQueue.table_name == "ingredient",
SyncQueue.record_id == ing_row.id,
SyncQueue.action == "update",
)
)
self.assertGreaterEqual(ing_sync, 1)
grp_row = db.session.execute(
select(UnloadingGroup).where(
UnloadingGroup.recipe_id == recipe.id,
UnloadingGroup.is_deleted.is_(False),
)
).scalar_one()
grp_sync = db.session.scalar(
select(func.count()).select_from(SyncQueue).where(
SyncQueue.table_name == "unloading_group",
SyncQueue.record_id == grp_row.id,
SyncQueue.action == "update",
)
)
self.assertGreaterEqual(grp_sync, 1)
def test_delete_ingredient_by_explicit_ids(self) -> None:
recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False)
ing_a, ing_b = ing_ids[0], ing_ids[1]
recipe = db.session.get(Recipe, recipe_id)
ing_b_row = db.session.get(Ingredient, ing_b)
payload = recipe_update_payload(
recipe,
deleted_ingredient_ids=[ing_a],
ingredients=[ingredient_payload_from_row(ing_b_row, order=1)],
groups=[],
)
update_recipe_from_payload(recipe_id, payload)
db.session.commit()
self.assertTrue(db.session.get(Ingredient, ing_a).is_deleted)
self.assertFalse(db.session.get(Ingredient, ing_b).is_deleted)
self.assertTrue(sync_task_exists("ingredient", ing_a, "delete"))
def test_reorder_without_deleted_list_implicit_delete(self) -> None:
recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False)
recipe = db.session.get(Recipe, recipe_id)
keep = db.session.get(Ingredient, ing_ids[1])
payload = recipe_update_payload(
recipe,
ingredients=[ingredient_payload_from_row(keep, order=1)],
groups=[],
)
update_recipe_from_payload(recipe_id, payload)
db.session.commit()
self.assertTrue(db.session.get(Ingredient, ing_ids[0]).is_deleted)
self.assertTrue(sync_task_exists("ingredient", ing_ids[0], "delete"))
def test_match_by_id_not_order_regression(self) -> None:
recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False)
ing_a, ing_b = ing_ids[0], ing_ids[1]
recipe = db.session.get(Recipe, recipe_id)
ing_b_row = db.session.get(Ingredient, ing_b)
comp_b_before = str(ing_b_row.component_id)
payload = recipe_update_payload(
recipe,
deleted_ingredient_ids=[ing_a],
ingredients=[ingredient_payload_from_row(ing_b_row, order=1)],
groups=[],
)
update_recipe_from_payload(recipe_id, payload)
db.session.commit()
ing_b_after = db.session.get(Ingredient, ing_b)
self.assertTrue(db.session.get(Ingredient, ing_a).is_deleted)
self.assertEqual(str(ing_b_after.component_id), comp_b_before)
def test_save_enqueues_update_for_all_active_children(self) -> None:
recipe_id, ing_ids, grp_ids = create_recipe_with_children()
recipe = db.session.get(Recipe, recipe_id)
ing_a = db.session.get(Ingredient, ing_ids[0])
ing_b = db.session.get(Ingredient, ing_ids[1])
grp1 = db.session.get(UnloadingGroup, grp_ids[0])
update_recipe_from_payload(
recipe_id,
recipe_update_payload(
recipe,
ingredients=[
ingredient_payload_from_row(ing_b, order=1),
ingredient_payload_from_row(ing_a, order=2),
],
groups=[group_payload_from_row(grp1, order=1)],
),
)
db.session.commit()
for ing_id in ing_ids:
self.assertGreaterEqual(
count_sync_tasks("ingredient", ing_id, "update"),
1,
msg=f"ingredient {ing_id}",
)
self.assertGreaterEqual(
count_sync_tasks("unloading_group", grp_ids[0], "update"),
1,
)
def test_change_component_id_enqueues_ingredient_update(self) -> None:
recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False)
new_comp = create_component(name="Новый корм")
recipe = db.session.get(Recipe, recipe_id)
ing_row = db.session.get(Ingredient, ing_ids[0])
payload = recipe_update_payload(
recipe,
ingredients=[
{
**ingredient_payload_from_row(ing_row),
"component_id": new_comp,
}
],
groups=[],
)
update_recipe_from_payload(recipe_id, payload)
db.session.commit()
self.assertEqual(str(db.session.get(Ingredient, ing_ids[0]).component_id), new_comp)
self.assertGreaterEqual(count_sync_tasks("ingredient", ing_ids[0], "update"), 1)
def test_delete_unloading_group_by_explicit_ids(self) -> None:
recipe_id, ing_ids, grp_ids = create_recipe_with_children()
recipe = db.session.get(Recipe, recipe_id)
keep_grp = db.session.get(UnloadingGroup, grp_ids[1])
ings = [
ingredient_payload_from_row(db.session.get(Ingredient, iid))
for iid in ing_ids
]
payload = recipe_update_payload(
recipe,
ingredients=ings,
groups=[group_payload_from_row(keep_grp, order=1)],
deleted_group_ids=[grp_ids[0]],
)
update_recipe_from_payload(recipe_id, payload)
db.session.commit()
self.assertTrue(db.session.get(UnloadingGroup, grp_ids[0]).is_deleted)
self.assertTrue(sync_task_exists("unloading_group", grp_ids[0], "delete"))
def test_soft_deleted_ingredient_no_duplicate_update_task(self) -> None:
recipe_id, ing_ids, _ = create_recipe_with_children(with_groups=False)
recipe = db.session.get(Recipe, recipe_id)
remove_id = ing_ids[0]
keep = db.session.get(Ingredient, ing_ids[1])
payload = recipe_update_payload(
recipe,
deleted_ingredient_ids=[remove_id],
ingredients=[ingredient_payload_from_row(keep, order=1)],
groups=[],
)
update_recipe_from_payload(recipe_id, payload)
db.session.commit()
self.assertTrue(sync_task_exists("ingredient", remove_id, "delete"))
self.assertEqual(count_sync_tasks("ingredient", remove_id, "update"), 0)
def test_update_missing_required_field_raises(self) -> None:
recipe = Recipe(
id=str(uuid.uuid4()),
name="X",
heads_per_trip=1,
mixing_time=0,
trip_percent=100.0,
)
db.session.add(recipe)
db.session.commit()
with self.assertRaises(RecipeUpdateError) as ctx:
update_recipe_from_payload(recipe.id, {"name": "Y"})
self.assertIn("heads_count", ctx.exception.message)
if __name__ == "__main__":
unittest.main()