158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
"""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()
|