"""HTTP-интеграция sync: register → push master → pull → confirm.""" from __future__ import annotations import os import tempfile import unittest import uuid from datetime import datetime from sqlalchemy import select from app import create_app, db from app.models import Component, Recipe, SyncClient, SyncEngineState, SyncQueue from app.services.sync_manager import enqueue_sync_queue_task from app.services.sync_record_data import get_record_data_for_sync from config import TestingConfig class SyncIntegrationConfig(TestingConfig): _TMP_DIR = tempfile.mkdtemp(prefix="wesp-sync-integration-") 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')}", } AUTH_LOGIN = "sync-int-admin" AUTH_PASSWORD = "sync-int-secret" class SyncIntegrationTests(unittest.TestCase): def setUp(self) -> None: self.app = create_app(SyncIntegrationConfig) self.client = self.app.test_client() self.ctx = self.app.app_context() self.ctx.push() db.create_all() self.node_id = "mill-integration-1" def tearDown(self) -> None: db.session.remove() db.drop_all() self.ctx.pop() def _register_client(self) -> None: resp = self.client.post( "/api/sync/register", json={"client_id": self.node_id, "client_name": "Mill integration"}, ) self.assertEqual(resp.status_code, 200) self.assertTrue(resp.get_json().get("success")) def _mark_bootstrap_ready(self) -> None: state = db.session.get(SyncEngineState, 1) if state is None: state = SyncEngineState(id=1) db.session.add(state) state.universal_bootstrap_completed_at = datetime.now() state.universal_bootstrap_cursor = 999 sc = db.session.execute( select(SyncClient).where(SyncClient.node_id == self.node_id) ).scalar_one() sc.personal_snapshot_cursor = 999 sc.personal_snapshot_completed_at = datetime.now() db.session.commit() def test_push_recipe_then_pull_component_confirm(self) -> None: self._register_client() self._mark_bootstrap_ready() recipe_id = str(uuid.uuid4()) db.session.add( Recipe( id=recipe_id, name="Mill integration recipe", heads_per_trip=5, mixing_time=3, trip_percent=100.0, ) ) db.session.commit() recipe_data = get_record_data_for_sync("recipe", recipe_id) self.assertIsNotNone(recipe_data) push_resp = self.client.post( "/api/sync/push", json={ "client_id": self.node_id, "changes": [ { "table_name": "recipe", "record_id": recipe_id, "action": "create", "data": recipe_data, } ], }, ) self.assertEqual(push_resp.status_code, 200) push_body = push_resp.get_json() self.assertTrue(push_body.get("success")) self.assertGreaterEqual(int(push_body.get("total_applied", 0)), 1) saved = db.session.get(Recipe, recipe_id) self.assertIsNotNone(saved) self.assertEqual(saved.name, "Mill integration recipe") cmp_id = str(uuid.uuid4()) db.session.add( Component( id=cmp_id, name="Server component", type="grain", is_active=True, dry_matter=1.0, protein=0.0, energy=0.0, price=0.0, ) ) db.session.commit() enqueue_sync_queue_task("component", cmp_id, "create", priority=2, target_node_id=None) db.session.commit() pull_resp = self.client.post( "/api/sync/pull", json={"client_id": self.node_id, "limit": 50}, ) self.assertIn(pull_resp.status_code, (200, 202)) if pull_resp.status_code == 202: pull_resp = self.client.post( "/api/sync/pull", json={"client_id": self.node_id, "limit": 50}, ) self.assertEqual(pull_resp.status_code, 200) pull_body = pull_resp.get_json() changes = pull_body.get("changes") or [] cmp_changes = [c for c in changes if c.get("table_name") == "component"] self.assertTrue(cmp_changes, f"expected component in pull, got {changes!r}") task_ids = [c.get("id") or c.get("task_id") for c in cmp_changes] task_ids = [t for t in task_ids if t] confirm_resp = self.client.post( "/api/sync/confirm", json={"client_id": self.node_id, "task_ids": task_ids}, ) self.assertEqual(confirm_resp.status_code, 200) self.assertTrue(confirm_resp.get_json().get("success")) task_row = db.session.get(SyncQueue, task_ids[0]) self.assertIsNotNone(task_row) self.assertEqual(task_row.status, "completed") if __name__ == "__main__": unittest.main()