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