130 lines
5.2 KiB
Python
130 lines
5.2 KiB
Python
"""Раздача веб-приложения: 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_root_redirect(app_hub, path):
|
||
status, r, body = _get(app_hub, path)
|
||
assert status == 302
|
||
assert r.getheader("Location") == "/app/"
|
||
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
|