From e7be50ba82b1c19d9cbd9b8ebdced22c303f7bc3 Mon Sep 17 00:00:00 2001 From: Spoofiecus Date: Wed, 16 Sep 2026 14:29:41 +0200 Subject: [PATCH] test(smoke): assert the login shell serves on a fresh install --- tests/test_smoke_install.py | 116 ++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/test_smoke_install.py diff --git a/tests/test_smoke_install.py b/tests/test_smoke_install.py new file mode 100644 index 0000000000..1dafe2dc7a --- /dev/null +++ b/tests/test_smoke_install.py @@ -0,0 +1,116 @@ +"""Fresh-install smoke tests (Phase 1): the floor must be green. + +Covers the cheapest release-confidence gate: +1. Python version requirement (3.11+). +2. Core dependency imports (fastapi, uvicorn, sqlalchemy, bcrypt, httpx, dotenv). +3. .env.example parses as KEY=VALUE lines (setup.py copies it verbatim). +4. internal_api_base() honors APP_PORT (never hardcode :7000). +5. Full app boots via TestClient and /api/health is healthy even with + optional services (Chroma/SearXNG/Ollama) unreachable. + +Pattern followed: tests/test_internal_api_base.py (env-scoped base helper + +no-hardcoded-loopback guard) and setup.py::check_deps (same module list). +""" +import pathlib +import sys + +import pytest + +REPO = pathlib.Path(__file__).resolve().parent.parent +CORE_DEPS = ["fastapi", "uvicorn", "sqlalchemy", "bcrypt", "httpx", "dotenv"] + + +def test_python_is_311_plus(): + assert sys.version_info >= (3, 11), f"need 3.11+, got {sys.version}" + + +@pytest.mark.parametrize("mod", CORE_DEPS) +def test_core_dependency_imports(mod): + __import__(mod) + + +def test_env_example_parses(): + example = REPO / ".env.example" + assert example.exists(), ".env.example must exist (setup.py copies it)" + bad = [] + for n, line in enumerate(example.read_text(encoding="utf-8").splitlines(), 1): + s = line.strip() + if not s or s.startswith("#"): + continue + if "=" not in s: + bad.append((n, line)) + assert not bad, f".env.example has non KEY=VALUE lines: {bad[:5]}" + + +def test_internal_api_base_honors_app_port(monkeypatch): + import core.constants as cc + + for k in ("ODYSSEUS_INTERNAL_BASE", "APP_PORT"): + monkeypatch.delenv(k, raising=False) + assert cc.internal_api_base() == "http://127.0.0.1:7000" + monkeypatch.setenv("APP_PORT", "7001") + assert cc.internal_api_base() == "http://127.0.0.1:7001" + + +def test_app_boots_and_health_is_healthy(monkeypatch): + """Full app boots; health is healthy with optional services unreachable.""" + import os + + # Point every optional-service probe at dead ports so the test proves + # graceful degradation instead of depending on the dev machine. + monkeypatch.setenv("DATABASE_URL", "sqlite:///:memory:") + monkeypatch.setenv("CHROMADB_HOST", "127.0.0.1") + monkeypatch.setenv("CHROMADB_PORT", "9") # discard port: nothing listens + monkeypatch.setenv("SEARXNG_INSTANCE", "http://127.0.0.1:9") + monkeypatch.setenv("OLLAMA_BASE_URL", "http://127.0.0.1:9/v1") + # Keep the embedding stack local-only (no HF download in CI). + monkeypatch.setenv("EMBEDDING_URL", "") + os.environ.pop("EMBEDDING_MODEL", None) + + from fastapi.testclient import TestClient + from app import app + + client = TestClient(app, raise_server_exceptions=False) + resp = client.get("/api/health") + assert resp.status_code == 200, resp.text[:500] + assert resp.json().get("status") == "healthy" + + +def test_root_serves_login_shell_html(monkeypatch): + """Fresh install must serve the UI shell, not just a healthy API. + + Smoke-contract item from #3968: "confirm the UI can become available + when applicable". Without users configured, the app's first-run path + redirects browser navigation to /login; assert that the shell HTML + actually comes back (status OK, HTML document, viewport meta) rather + than an error page or an empty body. + """ + import os + + monkeypatch.setenv("DATABASE_URL", "sqlite:///:memory:") + monkeypatch.setenv("CHROMADB_HOST", "127.0.0.1") + monkeypatch.setenv("CHROMADB_PORT", "9") + monkeypatch.setenv("SEARXNG_INSTANCE", "http://127.0.0.1:9") + monkeypatch.setenv("OLLAMA_BASE_URL", "http://127.0.0.1:9/v1") + monkeypatch.setenv("EMBEDDING_URL", "") + os.environ.pop("EMBEDDING_MODEL", None) + + from fastapi.testclient import TestClient + from app import app + + client = TestClient(app, raise_server_exceptions=False) + resp = client.get("/", follow_redirects=False) + # Either the login shell directly (200) or the first-run redirect to + # /login — both mean the UI layer is mounted and answering. What must + # NOT happen is a 5xx or an unexpected scheme. + assert resp.status_code in (200, 302, 307), ( + f"unexpected status {resp.status_code}: {resp.text[:300]}" + ) + if resp.status_code in (302, 307): + location = resp.headers.get("location", "") + assert "/login" in location, f"redirect did not target login: {location}" + resp = client.get(location or "/login") + body = resp.text + assert resp.status_code == 200, resp.text[:300] + assert "