233 lines
7.9 KiB
Python
233 lines
7.9 KiB
Python
"""Smoke tests for Cache-Control on /control/static/* responses.
|
|
|
|
The Control UI serves vendored JS/CSS through a `_CachedStaticFiles` subclass
|
|
(see ``opensquilla.gateway.control_ui``). These tests pin the header semantics
|
|
so a refactor that drops the subclass — or breaks the env-rollback knob —
|
|
shows up immediately.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
from starlette.applications import Starlette
|
|
from starlette.testclient import TestClient
|
|
|
|
from opensquilla.gateway import control_ui
|
|
from opensquilla.gateway.config import ControlUiConfig, GatewayConfig
|
|
from opensquilla.gateway.control_ui import create_control_ui_routes
|
|
|
|
|
|
@pytest.fixture
|
|
def _app(monkeypatch: pytest.MonkeyPatch) -> Starlette:
|
|
monkeypatch.delenv("OPENSQUILLA_STATIC_NO_CACHE", raising=False)
|
|
config = GatewayConfig()
|
|
config.control_ui.enabled = True
|
|
routes = create_control_ui_routes(config)
|
|
return Starlette(routes=routes)
|
|
|
|
|
|
def test_static_asset_carries_long_cache_control(_app: Starlette) -> None:
|
|
client = TestClient(_app)
|
|
response = client.get("/control/static/js/app.js")
|
|
assert response.status_code == 200, response.text
|
|
cache = response.headers.get("Cache-Control", "")
|
|
assert "max-age=2592000" in cache, cache
|
|
assert "public" in cache, cache
|
|
|
|
|
|
def test_control_ui_bootstrap_includes_config_path(tmp_path) -> None:
|
|
config = GatewayConfig()
|
|
config.config_path = str(tmp_path / "OpenSquilla Config.toml")
|
|
config.control_ui.enabled = True
|
|
app = Starlette(routes=create_control_ui_routes(config))
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/control/")
|
|
|
|
assert response.status_code == 200
|
|
assert 'data-config-path="' in response.text
|
|
assert str(config.config_path) in response.text
|
|
|
|
|
|
def test_control_ui_vite_asset_urls_use_configured_base_path(
|
|
tmp_path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
(tmp_path / "index.html").write_text(
|
|
'<script type="module" crossorigin src="./assets/index.js"></script>'
|
|
'<link rel="stylesheet" crossorigin href="./assets/index.css">',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr(control_ui, "_DIST_DIR", tmp_path)
|
|
|
|
js_url, css_urls = control_ui._read_vite_assets("/ops")
|
|
|
|
assert js_url == "/ops/static/dist/assets/index.js"
|
|
assert css_urls == ["/ops/static/dist/assets/index.css"]
|
|
|
|
|
|
def test_read_vite_assets_extracts_every_stylesheet(
|
|
tmp_path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
# Vite can emit more than one entry stylesheet (e.g. a shared Icon chunk
|
|
# before the main bundle); all must be returned in document order, else the
|
|
# page renders unstyled.
|
|
(tmp_path / "index.html").write_text(
|
|
'<script type="module" crossorigin src="./assets/index.js"></script>'
|
|
'<link rel="stylesheet" crossorigin href="./assets/Icon-abc.css">'
|
|
'<link rel="stylesheet" crossorigin href="./assets/index-def.css">',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr(control_ui, "_DIST_DIR", tmp_path)
|
|
|
|
js_url, css_urls = control_ui._read_vite_assets("/ops")
|
|
|
|
assert js_url == "/ops/static/dist/assets/index.js"
|
|
assert css_urls == [
|
|
"/ops/static/dist/assets/Icon-abc.css",
|
|
"/ops/static/dist/assets/index-def.css",
|
|
]
|
|
|
|
|
|
def test_control_ui_rebases_hard_coded_vite_base_path(
|
|
tmp_path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
(tmp_path / "index.html").write_text(
|
|
'<script type="module" src="/control/static/dist/assets/index.js"></script>'
|
|
'<link rel="stylesheet" href="/control/static/dist/assets/index.css">',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr(control_ui, "_DIST_DIR", tmp_path)
|
|
|
|
js_url, css_urls = control_ui._read_vite_assets("/custom")
|
|
|
|
assert js_url == "/custom/static/dist/assets/index.js"
|
|
assert css_urls == ["/custom/static/dist/assets/index.css"]
|
|
|
|
|
|
def test_control_ui_defaults_to_vue_bootstrap(
|
|
tmp_path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
(tmp_path / "index.html").write_text(
|
|
'<script type="module" crossorigin src="./assets/index.js"></script>'
|
|
'<link rel="stylesheet" crossorigin href="./assets/index.css">',
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.setattr(control_ui, "_DIST_DIR", tmp_path)
|
|
config = GatewayConfig()
|
|
config.control_ui.enabled = True
|
|
app = Starlette(routes=create_control_ui_routes(config))
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/control/")
|
|
|
|
assert response.status_code == 200
|
|
assert '/control/static/dist/assets/index.js' in response.text
|
|
assert '/control/static/js/app.js' not in response.text
|
|
|
|
|
|
def test_control_ui_legacy_frontend_uses_static_bootstrap() -> None:
|
|
config = GatewayConfig()
|
|
config.control_ui.enabled = True
|
|
config.control_ui.frontend = "legacy"
|
|
app = Starlette(routes=create_control_ui_routes(config))
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/control/")
|
|
|
|
assert response.status_code == 200
|
|
assert '/control/static/js/app.js' in response.text
|
|
assert '/control/static/dist/assets/' not in response.text
|
|
|
|
|
|
def test_control_ui_frontend_reads_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("OPENSQUILLA_CONTROL_UI_FRONTEND", "legacy")
|
|
|
|
config = GatewayConfig()
|
|
|
|
assert config.control_ui.frontend == "legacy"
|
|
|
|
|
|
def test_control_ui_frontend_reads_toml_config(tmp_path) -> None:
|
|
config_path = tmp_path / "opensquilla.toml"
|
|
config_path.write_text(
|
|
'[control_ui]\nfrontend = "legacy"\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
config = GatewayConfig.load_from_toml(config_path)
|
|
|
|
assert config.control_ui.frontend == "legacy"
|
|
|
|
|
|
def test_control_ui_frontend_rejects_invalid_value() -> None:
|
|
with pytest.raises(ValidationError):
|
|
ControlUiConfig(frontend="retro")
|
|
|
|
|
|
def test_control_ui_legacy_frontend_uses_configured_base_path() -> None:
|
|
config = GatewayConfig()
|
|
config.control_ui.enabled = True
|
|
config.control_ui.base_path = "/ops"
|
|
config.control_ui.frontend = "legacy"
|
|
app = Starlette(routes=create_control_ui_routes(config))
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/ops/")
|
|
|
|
assert response.status_code == 200
|
|
assert '/ops/static/js/app.js' in response.text
|
|
assert '/control/static/js/app.js' not in response.text
|
|
|
|
|
|
def test_control_ui_bootstrap_ws_url_uses_client_reachable_wildcard_host() -> None:
|
|
config = GatewayConfig()
|
|
config.host = "0.0.0.0"
|
|
config.port = 20002
|
|
config.control_ui.enabled = True
|
|
app = Starlette(routes=create_control_ui_routes(config))
|
|
client = TestClient(app)
|
|
|
|
response = client.get("/control/")
|
|
|
|
assert response.status_code == 200
|
|
assert 'data-ws-url="ws://127.0.0.1:20002/ws"' in response.text
|
|
assert 'data-ws-url="ws://0.0.0.0:20002/ws"' not in response.text
|
|
|
|
|
|
def test_env_rollback_disables_cache_control(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
# OPENSQUILLA_STATIC_NO_CACHE=1 must completely skip the Cache-Control
|
|
# header so a release with a static-cache problem can be defused without
|
|
# a redeploy.
|
|
monkeypatch.setenv("OPENSQUILLA_STATIC_NO_CACHE", "1")
|
|
config = GatewayConfig()
|
|
config.control_ui.enabled = True
|
|
app = Starlette(routes=create_control_ui_routes(config))
|
|
client = TestClient(app)
|
|
response = client.get("/control/static/js/app.js")
|
|
assert response.status_code == 200
|
|
# Either header is absent or it does not advertise our long max-age.
|
|
cache = response.headers.get("Cache-Control", "")
|
|
assert "max-age=2592000" not in cache
|
|
|
|
|
|
def test_nonexistent_path_does_not_add_header(_app: Starlette) -> None:
|
|
client = TestClient(_app)
|
|
response = client.get("/control/static/js/does-not-exist-12345.js")
|
|
# 404 must not be tagged with a long-cache header — clients would otherwise
|
|
# remember a "missing" asset for 30 days.
|
|
assert response.status_code == 404
|
|
assert "max-age=2592000" not in response.headers.get("Cache-Control", "")
|
|
|
|
|
|
def _cleanup_env() -> None:
|
|
os.environ.pop("OPENSQUILLA_STATIC_NO_CACHE", None)
|