"""Постановка задач 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()