import json import os import tempfile import unittest from unittest.mock import patch from werkzeug.security import generate_password_hash from app import create_app, db from app.models import WebUser from config import TestingConfig class NetworkSettingsApiTests(unittest.TestCase): def setUp(self) -> None: self._tmp = tempfile.mkdtemp(prefix="wesp-network-settings-") self._net_path = os.path.join(self._tmp, "wesp_network_settings.json") self._sec_path = os.path.join(self._tmp, "wesp_security_settings.json") self._periph_path = os.path.join(self._tmp, "wesp-peripherals.jsonl") os.environ["WESP_NETWORK_SETTINGS_PATH"] = self._net_path os.environ["WESP_SECURITY_SETTINGS_PATH"] = self._sec_path os.environ["WESP_ADMIN_PERIPHERAL_LOG_PATH"] = self._periph_path self._prev_hostname = os.environ.pop("WESP_NETWORK_LOCAL_HOSTNAME", None) self._prev_pub = os.environ.pop("WESP_KIOSK_PUBLIC_BASE_URL", None) self._prev_mdns = os.environ.pop("WESP_MDNS_ENABLED", None) class C(TestingConfig): BASE_DIR = self._tmp DATA_DIR = os.path.join(self._tmp, "data") SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(self._tmp, 'recipes_test.db')}" SQLALCHEMY_BINDS = {"reports": f"sqlite:///{os.path.join(self._tmp, 'reports_test.db')}"} AUTH_LOGIN = "admin" AUTH_PASSWORD = "admin-secret" SECRET_KEY = "unit-test-secret-key-not-default-32chars" self.app = create_app(C) self.app.config["WESP_ADMIN_PERIPHERAL_LOG_PATH"] = self._periph_path self.client = self.app.test_client() self.ctx = self.app.app_context() self.ctx.push() os.makedirs(C.DATA_DIR, exist_ok=True) db.create_all() db.session.add( WebUser( login="admin", password_hash=generate_password_hash("admin-secret"), is_superuser=True, ) ) db.session.commit() self._login() def tearDown(self) -> None: db.session.remove() db.drop_all() self.ctx.pop() os.environ.pop("WESP_NETWORK_SETTINGS_PATH", None) os.environ.pop("WESP_SECURITY_SETTINGS_PATH", None) os.environ.pop("WESP_ADMIN_PERIPHERAL_LOG_PATH", None) if self._prev_hostname is not None: os.environ["WESP_NETWORK_LOCAL_HOSTNAME"] = self._prev_hostname else: os.environ.pop("WESP_NETWORK_LOCAL_HOSTNAME", None) if self._prev_pub is not None: os.environ["WESP_KIOSK_PUBLIC_BASE_URL"] = self._prev_pub else: os.environ.pop("WESP_KIOSK_PUBLIC_BASE_URL", None) if self._prev_mdns is not None: os.environ["WESP_MDNS_ENABLED"] = self._prev_mdns else: os.environ.pop("WESP_MDNS_ENABLED", None) def _login(self) -> None: r = self.client.post("/api/auth/login", json={"login": "admin", "password": "admin-secret"}) self.assertEqual(r.status_code, 200, r.get_data(as_text=True)) def test_get_network_defaults(self) -> None: r = self.client.get("/api/admin/network-settings") self.assertEqual(r.status_code, 200, r.get_data(as_text=True)) body = r.get_json() or {} net = body.get("network") or {} self.assertEqual(net.get("default_local_hostname"), "komton_srv_1.local") self.assertIn("komton_srv_1.local", net.get("local_hostname") or "") self.assertIn("detected", net) @patch("app.routes.admin.reload_mdns") def test_patch_network_settings_persists_and_applies(self, mock_reload) -> None: r = self.client.patch( "/api/admin/network-settings", json={ "local_hostname": "komton_srv_2.local", "public_base_url": "http://komton_srv_2.local", "mdns_enabled": True, }, ) self.assertEqual(r.status_code, 200, r.get_data(as_text=True)) self.assertEqual(self.app.config.get("KIOSK_PUBLIC_BASE_URL"), "http://komton_srv_2.local") self.assertEqual(self.app.config.get("WESP_NETWORK_LOCAL_HOSTNAME"), "komton_srv_2.local") self.assertTrue(self.app.config.get("WESP_MDNS_ENABLED")) mock_reload.assert_called_once() with open(self._net_path, encoding="utf-8") as f: stored = json.load(f) self.assertEqual(stored.get("local_hostname"), "komton_srv_2.local") @patch("app.routes.admin.reload_mdns") def test_migrate_public_url_from_security_file(self, _mock_reload) -> None: with open(self._sec_path, "w", encoding="utf-8") as f: json.dump({"kiosk_public_base_url": "http://legacy.local:5000"}, f) r = self.client.get("/api/admin/network-settings") net = (r.get_json() or {}).get("network") or {} self.assertEqual(net.get("public_base_url"), "http://legacy.local:5000") @patch("app.routes.admin.reload_mdns") def test_patch_writes_network_journal(self, mock_reload) -> None: r = self.client.patch( "/api/admin/network-settings", json={"local_hostname": "komton_srv_1.local", "mdns_enabled": True}, ) self.assertEqual(r.status_code, 200, r.get_data(as_text=True)) self.assertTrue(os.path.isfile(self._periph_path)) with open(self._periph_path, encoding="utf-8") as f: body = f.read() self.assertIn("network_settings_saved", body) ev = self.client.get("/api/admin/peripheral-events?component=network") self.assertEqual(ev.status_code, 200) events = (ev.get_json() or {}).get("events") or [] self.assertTrue(any(e.get("code") == "network_settings_saved" for e in events)) @patch("app.routes.admin.reload_mdns") def test_invalid_hostname_rejected(self, _mock_reload) -> None: r = self.client.patch( "/api/admin/network-settings", json={"local_hostname": "1"}, ) self.assertEqual(r.status_code, 400) self.assertIn("цифр", (r.get_json() or {}).get("message", "")) @patch("app.routes.admin.reload_mdns") def test_patch_public_url_only_skips_mdns_reload(self, mock_reload) -> None: r = self.client.patch( "/api/admin/network-settings", json={"public_base_url": "http://komton_srv_1.local"}, ) self.assertEqual(r.status_code, 200, r.get_data(as_text=True)) mock_reload.assert_not_called() @patch("app.routes.admin.reload_mdns") def test_clear_network_journal_only(self, _mock_reload) -> None: from app.services.admin_peripheral_monitor import append_peripheral_event append_peripheral_event( self.app, component="network", code="mdns_started", message="net event", force=True, ) append_peripheral_event( self.app, component="hx711", code="hx711_read_error", message="hw event", force=True, ) r = self.client.delete("/api/admin/peripheral-events?component=network") self.assertEqual(r.status_code, 200, r.get_data(as_text=True)) ev = self.client.get("/api/admin/peripheral-events?component=network") self.assertEqual(ev.status_code, 200) net_events = (ev.get_json() or {}).get("events") or [] self.assertEqual(net_events, []) all_ev = self.client.get("/api/admin/peripheral-events?limit=20") codes = [e.get("code") for e in (all_ev.get_json() or {}).get("events") or []] self.assertIn("hx711_read_error", codes) def test_env_lock_blocks_patch(self) -> None: os.environ["WESP_KIOSK_PUBLIC_BASE_URL"] = "http://env.local:5000" r = self.client.patch( "/api/admin/network-settings", json={"public_base_url": "http://other.local:5000"}, ) self.assertEqual(r.status_code, 400) if __name__ == "__main__": unittest.main()