@@ -0,0 +1,204 @@
|
||||
"""Setup wizard: guard, API, completion."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from app import create_app, db
|
||||
from app.models import WebUser
|
||||
from app.services.setup_state import is_setup_completed, read_install_state
|
||||
from config import ProductionConfig
|
||||
from tests.kiosk_test_env import kiosk_client_kwargs
|
||||
|
||||
|
||||
class SetupWizardTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.mkdtemp(prefix="wesp-setup-wizard-")
|
||||
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 SetupConfig(ProductionConfig):
|
||||
BASE_DIR = str(self._base)
|
||||
DATA_DIR = str(data)
|
||||
SQLALCHEMY_DATABASE_URI = f"sqlite:///{recipes}"
|
||||
SQLALCHEMY_BINDS = {"reports": f"sqlite:///{reports}"}
|
||||
SECRET_KEY = "setup-wizard-test-secret"
|
||||
TESTING = True
|
||||
WESP_TESTING_BYPASS_SETUP_GUARD = False
|
||||
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
|
||||
|
||||
self.app = create_app(SetupConfig, run_migrations=True)
|
||||
self.client = self.app.test_client()
|
||||
self._ctx = self.app.app_context()
|
||||
self._ctx.push()
|
||||
db.create_all()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._ctx.pop()
|
||||
shutil.rmtree(self._tmp, ignore_errors=True)
|
||||
|
||||
def test_fresh_install_redirects_to_setup(self) -> None:
|
||||
self.assertFalse(is_setup_completed(self.app))
|
||||
resp = self.client.get("/")
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertIn("/setup", resp.headers.get("Location", ""))
|
||||
|
||||
def test_setup_page_ok(self) -> None:
|
||||
resp = self.client.get("/setup")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn(b"setup-wizard.js", resp.data)
|
||||
|
||||
def test_setup_status_defaults(self) -> None:
|
||||
resp = self.client.get("/api/setup/status")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
body = resp.get_json()
|
||||
self.assertEqual(body.get("status"), "success")
|
||||
self.assertFalse(body.get("setup", {}).get("setup_completed"))
|
||||
|
||||
def test_setup_api_accessible_from_lan_while_incomplete(self) -> None:
|
||||
self.app.config["TESTING"] = False
|
||||
try:
|
||||
resp = self.client.get("/api/setup/status", **kiosk_client_kwargs())
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(resp.get_json().get("setup", {}).get("setup_completed"))
|
||||
|
||||
post = self.client.post(
|
||||
"/api/setup/device-role",
|
||||
json={"device_role": "server"},
|
||||
**kiosk_client_kwargs(),
|
||||
)
|
||||
self.assertEqual(post.status_code, 200)
|
||||
finally:
|
||||
self.app.config["TESTING"] = True
|
||||
|
||||
def test_setup_status_from_lan_blocked_after_complete(self) -> None:
|
||||
self.client.post("/api/setup/device-role", json={"device_role": "client"})
|
||||
self.client.post(
|
||||
"/api/setup/sync",
|
||||
json={"server_url": "http://127.0.0.1", "client_name": "c1"},
|
||||
)
|
||||
self.client.post("/api/setup/complete")
|
||||
self.assertTrue(is_setup_completed(self.app))
|
||||
|
||||
self.app.config["TESTING"] = False
|
||||
try:
|
||||
resp = self.client.get("/api/setup/status", **kiosk_client_kwargs())
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
finally:
|
||||
self.app.config["TESTING"] = True
|
||||
|
||||
def test_server_flow_complete(self) -> None:
|
||||
r1 = self.client.post(
|
||||
"/api/setup/device-role",
|
||||
json={"device_role": "server"},
|
||||
)
|
||||
self.assertEqual(r1.status_code, 200)
|
||||
|
||||
r2 = self.client.post(
|
||||
"/api/setup/network",
|
||||
json={
|
||||
"local_hostname": "komton_test_1",
|
||||
},
|
||||
)
|
||||
self.assertEqual(r2.status_code, 200)
|
||||
net = r2.get_json().get("network") or {}
|
||||
self.assertEqual(net.get("local_hostname"), "komton_test_1.local")
|
||||
self.assertTrue(str(net.get("public_base_url") or "").startswith("http://komton_test_1.local"))
|
||||
|
||||
r3 = self.client.post(
|
||||
"/api/setup/users",
|
||||
json={
|
||||
"zootech": {
|
||||
"login": "setup_zootech",
|
||||
"password": "ZootechPass1",
|
||||
"confirm": "ZootechPass1",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual(r3.status_code, 201 if r3.status_code != 200 else 200)
|
||||
self.assertEqual(r3.get_json().get("status"), "success")
|
||||
|
||||
users = db.session.query(WebUser).all()
|
||||
self.assertEqual(len(users), 2)
|
||||
supers = [u for u in users if u.is_superuser]
|
||||
self.assertEqual(len(supers), 1)
|
||||
self.assertEqual(supers[0].login, "admin")
|
||||
zootechs = [u for u in users if not u.is_superuser]
|
||||
self.assertEqual(len(zootechs), 1)
|
||||
self.assertEqual(zootechs[0].login, "setup_zootech")
|
||||
|
||||
r4 = self.client.post("/api/setup/complete")
|
||||
self.assertEqual(r4.status_code, 200)
|
||||
self.assertTrue(is_setup_completed(self.app))
|
||||
|
||||
r5 = self.client.get("/")
|
||||
self.assertNotEqual(r5.status_code, 302)
|
||||
|
||||
def test_setup_mutation_blocked_after_complete(self) -> None:
|
||||
self.client.post("/api/setup/device-role", json={"device_role": "client"})
|
||||
self.client.post(
|
||||
"/api/setup/sync",
|
||||
json={"server_url": "http://127.0.0.1", "client_name": "c1"},
|
||||
)
|
||||
self.client.post("/api/setup/complete")
|
||||
self.assertTrue(is_setup_completed(self.app))
|
||||
|
||||
resp = self.client.post("/api/setup/device-role", json={"device_role": "server"})
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_client_sync_progress(self) -> None:
|
||||
self.client.post("/api/setup/device-role", json={"device_role": "client"})
|
||||
resp = self.client.get("/api/setup/sync/progress")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
body = resp.get_json()
|
||||
self.assertIn("first_bootstrap_done", body)
|
||||
self.assertIn("initial_sync_progress", body)
|
||||
|
||||
def test_setup_users_update_existing_zootech(self) -> None:
|
||||
self.client.post("/api/setup/device-role", json={"device_role": "server"})
|
||||
payload = {
|
||||
"zootech": {
|
||||
"login": "setup_zootech",
|
||||
"password": "ZootechPass1",
|
||||
"confirm": "ZootechPass1",
|
||||
},
|
||||
}
|
||||
r1 = self.client.post("/api/setup/users", json=payload)
|
||||
self.assertEqual(r1.status_code, 200)
|
||||
self.assertEqual(r1.get_json().get("action"), "created")
|
||||
|
||||
payload["zootech"]["password"] = "ZootechPass2"
|
||||
payload["zootech"]["confirm"] = "ZootechPass2"
|
||||
r2 = self.client.post("/api/setup/users", json=payload)
|
||||
self.assertEqual(r2.status_code, 200)
|
||||
self.assertEqual(r2.get_json().get("action"), "updated")
|
||||
self.assertEqual(db.session.query(WebUser).filter_by(is_superuser=False).count(), 1)
|
||||
|
||||
payload["zootech"]["login"] = "setup_zootech_2"
|
||||
payload["zootech"]["password"] = "ZootechPass3"
|
||||
payload["zootech"]["confirm"] = "ZootechPass3"
|
||||
r3 = self.client.post("/api/setup/users", json=payload)
|
||||
self.assertEqual(r3.status_code, 200)
|
||||
self.assertEqual(r3.get_json().get("action"), "created")
|
||||
self.assertEqual(db.session.query(WebUser).filter_by(is_superuser=False).count(), 2)
|
||||
|
||||
def test_install_state_has_first_launch(self) -> None:
|
||||
state = read_install_state(self.app)
|
||||
self.assertIn("first_launch_at", state)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user