Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
+354
View File
@@ -0,0 +1,354 @@
"""Постановка задач 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()