sdlc-hub/tests/test_web_app.py
Andrey Epifancev a88bb1e6e8 refactor(sdlc-rename): пакет sdlc_hub, CLI sdlc-hub, каталог данных sdlc-hub
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:27:17 +04:00

130 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Раздача веб-приложения: GET /app… без токена (specs/hub-api «Раздача веб-приложения»)."""
import http.client
import threading
from urllib.parse import urlparse
import pytest
from sdlc_hub.core import api
def _get(base, path):
"""Сырой GET: http.client не нормализует путь (../ и %2e%2e доходят до хаба как есть)."""
u = urlparse(base)
c = http.client.HTTPConnection(u.hostname, u.port, timeout=10)
try:
c.request("GET", path)
r = c.getresponse()
return r.status, r, r.read()
finally:
c.close()
def _serve(tmp_path, web_dir):
h = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups", web_dir=web_dir)
srv = api.make_server(h, "127.0.0.1", 0)
th = threading.Thread(target=srv.serve_forever, daemon=True)
th.start()
return srv, th, f"http://127.0.0.1:{srv.server_port}"
@pytest.fixture
def app_hub(tmp_path):
web = tmp_path / "app"
(web / "assets").mkdir(parents=True)
(web / "index.html").write_text("<!doctype html><div id=root></div>", encoding="utf-8")
(web / "assets" / "index-abc123.js").write_text("console.log('ok')", encoding="utf-8")
(web / "assets" / "index-abc123.css").write_text("body{}", encoding="utf-8")
(web / "assets" / "font.bin").write_bytes(b"\x00\x01")
for name, data in (("logo.svg", b"<svg/>"), ("p.png", b"\x89PNG"), ("f.ico", b"\x00"), ("f.woff2", b"wOF2"),
("index-abc123.js.map", b"{}")):
(web / "assets" / name).write_bytes(data)
(tmp_path / "secret.txt").write_text("секрет", encoding="utf-8")
srv, th, base = _serve(tmp_path, web)
yield base
srv.shutdown()
srv.server_close()
th.join(5)
def test_asset_served_with_type_and_immutable_cache(app_hub):
status, r, body = _get(app_hub, "/app/assets/index-abc123.js")
assert status == 200
assert r.getheader("Content-Type") == "text/javascript; charset=utf-8"
assert r.getheader("Cache-Control") == "public, max-age=31536000, immutable"
assert body == b"console.log('ok')"
status, r, _ = _get(app_hub, "/app/assets/index-abc123.css")
assert r.getheader("Content-Type") == "text/css; charset=utf-8"
status, r, _ = _get(app_hub, "/app/assets/font.bin")
assert status == 200 and r.getheader("Content-Type") == "application/octet-stream"
@pytest.mark.parametrize("path", ["/app", "/app/", "/app/board", "/app/issues/AURORA-1620", "/app/issues?sprint=x"])
def test_app_paths_get_index_without_token(app_hub, path):
status, r, body = _get(app_hub, path)
assert status == 200
assert r.getheader("Content-Type") == "text/html; charset=utf-8"
assert r.getheader("Cache-Control") == "no-cache"
assert body == b"<!doctype html><div id=root></div>"
@pytest.mark.parametrize("path", ["/app/assets/../../secret.txt", "/app/assets/%2e%2e/%2e%2e/secret.txt",
"/app/assets/..%2F..%2Fsecret.txt", "/app/assets/missing.js", "/app/assets/"])
def test_asset_outside_or_missing_is_404(app_hub, path):
status, _, body = _get(app_hub, path)
assert status == 404
assert "секрет".encode("utf-8") not in body
def test_not_built_is_503_with_hint(tmp_path):
srv, th, base = _serve(tmp_path, tmp_path / "no-build")
try:
for path in ("/app/board", "/app/assets/index-abc123.js"):
status, r, body = _get(base, path)
assert status == 503
assert r.getheader("Content-Type") == "text/html; charset=utf-8"
assert "make web".encode("utf-8") in body
finally:
srv.shutdown()
srv.server_close()
th.join(5)
def test_other_paths_are_not_app(app_hub):
status, _, _ = _get(app_hub, "/application")
assert status == 404 # не префикс /app/, обычный 404 API
status, _, _ = _get(app_hub, "/board")
assert status == 401 # API по-прежнему под токеном
def test_default_web_dir_is_package_build(tmp_path):
h = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups")
assert h.web_dir == api.WEB_APP
assert api.WEB_APP.parts[-3:] == ("sdlc_hub", "web", "app")
@pytest.mark.parametrize("path", ["/", "/links"])
def test_old_board_addresses_redirect_to_app(app_hub, path):
status, r, body = _get(app_hub, path)
assert status == 302
assert r.getheader("Location") == "/app/board"
assert body == b""
def test_links_page_is_gone():
assert not hasattr(api, "PAGE")
assert not (api.WEB_APP.parent / "links.html").exists()
@pytest.mark.parametrize("name,ctype", [("logo.svg", "image/svg+xml"), ("p.png", "image/png"), ("f.ico", "image/x-icon"),
("f.woff2", "font/woff2"), ("index-abc123.js.map", "application/json")])
def test_asset_content_types(app_hub, name, ctype):
status, r, _ = _get(app_hub, "/app/assets/" + name)
assert status == 200 and r.getheader("Content-Type") == ctype
@pytest.mark.parametrize("path", ["/app/assets/%00", "/app/assets/a%00.js", "/app/assets/%00/../index-abc123.js"])
def test_nul_in_asset_path_is_404(app_hub, path):
status, _, _ = _get(app_hub, path)
assert status == 404