"""Чистая установка: пустой data/, Alembic, bootstrap, health.""" from __future__ import annotations import os import shutil import tempfile import unittest from pathlib import Path from app import create_app, db from app.models.auto_update_settings import AutoUpdateSettings from app.services.update_health import migrations_ok_for_app from config import Config, ProductionConfig class FreshInstallTest(unittest.TestCase): def setUp(self) -> None: self._tmp = tempfile.mkdtemp(prefix="wesp-fresh-install-") self._base = Path(self._tmp) / "wesp_home" self._base.mkdir() data = self._base / "data" data.mkdir() recipes = data / "recipes.db" reports = data / "reports.db" class FreshConfig(ProductionConfig): BASE_DIR = str(self._base) DATA_DIR = str(data) SQLALCHEMY_DATABASE_URI = f"sqlite:///{recipes}" SQLALCHEMY_BINDS = {"reports": f"sqlite:///{reports}"} SECRET_KEY = "fresh-install-test-secret" AUTH_LOGIN = "install-admin" AUTH_PASSWORD = "install-secret" SYNC_BACKGROUND_REQUEUE = False SYNC_CLIENT_AUTOSTART = False WESP_ADMIN_LOG_PATH = "" WESP_ADMIN_HARDWARE_LOG_PATH = "" WESP_ADMIN_UI_ACTIVITY_LOG_PATH = "" WESP_LLM_AUTOSTART = False WESP_BACKGROUND_STARTUP = False self.app = create_app(FreshConfig, run_migrations=True) self.client = self.app.test_client() self._ctx = self.app.app_context() self._ctx.push() def tearDown(self) -> None: self._ctx.pop() shutil.rmtree(self._tmp, ignore_errors=True) def test_ensure_auto_update_row_idempotent(self) -> None: from sqlalchemy import func, select from app.services.auto_update_settings_service import ensure_auto_update_row ensure_auto_update_row() ensure_auto_update_row() n = db.session.scalar(select(func.count()).select_from(AutoUpdateSettings)) self.assertEqual(int(n or 0), 1) def test_databases_and_auto_update_bootstrap(self) -> None: from app.services.auto_update_settings_service import bootstrap_update_environment recipes = Path(self.app.config["DATA_DIR"]) / "recipes.db" reports = Path(self.app.config["DATA_DIR"]) / "reports.db" self.assertTrue(recipes.is_file()) self.assertTrue(reports.is_file()) # Как при prod-старте, но синхронно: отложенный поток под нагрузкой pytest может не успеть за 5 с. bootstrap_update_environment(self.app) row = db.session.get(AutoUpdateSettings, 1) self.assertIsNotNone(row) config_json = Path(self.app.config["DATA_DIR"]) / "config.json" self.assertTrue(config_json.is_file()) def test_migrations_at_head(self) -> None: self.assertTrue(migrations_ok_for_app(self.app)) def test_health_after_fresh_install(self) -> None: r = self.client.get("/api/health") self.assertEqual(r.status_code, 200) body = r.get_json() self.assertTrue(body.get("ok")) self.assertTrue(body.get("db_ok")) self.assertTrue(body.get("migrations_ok")) self.assertEqual(body.get("migration_head"), body.get("migration_revision")) def test_setup_not_completed_by_default(self) -> None: from app.services.setup_state import is_setup_completed self.assertFalse(is_setup_completed(self.app)) def test_alembic_env_does_not_bootstrap_before_upgrade(self) -> None: """create_app(run_migrations=False) не обращается к таблицам до upgrade (как migrations/env.py).""" empty = Path(self._tmp) / "empty" empty.mkdir() data = empty / "data" data.mkdir() recipes = data / "recipes.db" reports = data / "reports.db" class EmptyConfig(Config): BASE_DIR = str(empty) DATA_DIR = str(data) SQLALCHEMY_DATABASE_URI = f"sqlite:///{recipes}" SQLALCHEMY_BINDS = {"reports": f"sqlite:///{reports}"} SECRET_KEY = "empty" SYNC_BACKGROUND_REQUEUE = False WESP_ADMIN_LOG_PATH = "" os.environ["WESP_ALEMBIC"] = "1" try: app = create_app(EmptyConfig, run_migrations=False) self.assertIsNotNone(app) finally: os.environ.pop("WESP_ALEMBIC", None) if __name__ == "__main__": unittest.main()