"""Серверная защита HTML страниц зоотехника (сессия + блокировка /static/*.html)."""
import os
import tempfile
import unittest
from app import create_app, db
from app.services.setup_state import mark_setup_complete
from config import TestingConfig
class ZootechHtmlGuardTestConfig(TestingConfig):
_TMP_DIR = tempfile.mkdtemp(prefix="wesp-zootech-html-tests-")
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(_TMP_DIR, 'recipes_test.db')}"
SQLALCHEMY_BINDS = {"reports": f"sqlite:///{os.path.join(_TMP_DIR, 'reports_test.db')}"}
AUTH_LOGIN = "zootech-guard-admin"
AUTH_PASSWORD = "zootech-guard-secret"
def _drain_response(resp) -> None:
"""Дочитать тело ответа и закрыть поток (избегает ResourceWarning от send_from_directory)."""
try:
resp.get_data()
finally:
close = getattr(resp, "close", None)
if callable(close):
close()
def _dispose_db_engines() -> None:
"""Закрыть пулы SQLite после теста (убирает ResourceWarning unclosed database)."""
try:
for eng in db.engines.values():
eng.dispose()
except Exception:
try:
db.engine.dispose()
except Exception:
pass
class ZootechHtmlGuardTests(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app(ZootechHtmlGuardTestConfig)
self.client = self.app.test_client()
self.ctx = self.app.app_context()
self.ctx.push()
db.create_all()
mark_setup_complete(self.app)
def tearDown(self) -> None:
db.session.remove()
db.drop_all()
_dispose_db_engines()
self.ctx.pop()
def test_recipes_redirects_to_login_without_session(self) -> None:
r = self.client.get("/recipes", follow_redirects=False)
self.assertEqual(r.status_code, 302)
loc = r.headers.get("Location", "")
self.assertIn("/login", loc)
self.assertIn("next=", loc)
_drain_response(r)
def _login(self) -> None:
login = self.client.post(
"/api/auth/login",
json={
"login": "zootech-guard-admin",
"password": "zootech-guard-secret",
},
)
self.assertEqual(login.status_code, 200)
_drain_response(login)
def test_recipes_ok_after_login(self) -> None:
self._login()
r = self.client.get("/recipes")
self.assertEqual(r.status_code, 200)
self.assertIn("text/html", r.content_type or "")
self.assertIn(b"html", r.data.lower())
_drain_response(r)
def test_static_recipes_html_blocked_without_named_route(self) -> None:
r = self.client.get("/static/recipes.html")
self.assertEqual(r.status_code, 404)
_drain_response(r)
def test_static_recipes_selection_allowlisted_for_kiosk(self) -> None:
r = self.client.get("/static/recipes_selection.html")
self.assertEqual(r.status_code, 200)
_drain_response(r)
def test_static_login_html_allowlisted(self) -> None:
r = self.client.get("/static/login.html")
self.assertEqual(r.status_code, 200)
_drain_response(r)
def test_login_page_public(self) -> None:
r = self.client.get("/login")
self.assertEqual(r.status_code, 200)
_drain_response(r)
def test_zootech_pages_ok_after_login(self) -> None:
self._login()
for path in (
"/feed_dispensers",
"/components",
"/reports",
"/feed_consumption",
):
with self.subTest(path=path):
r = self.client.get(path)
self.assertEqual(r.status_code, 200, path)
self.assertIn(b"html", r.data.lower())
_drain_response(r)
def test_zootech_static_html_blocked_without_named_route(self) -> None:
for name in (
"feed_dispensers.html",
"components.html",
"reports.html",
"consumption.html",
):
with self.subTest(static=name):
r = self.client.get(f"/static/{name}")
self.assertEqual(r.status_code, 404)
_drain_response(r)
def test_recipes_wibor_public_for_kiosk(self) -> None:
"""Выбор рецепта с киоска без логина зоотехника (/scales → /api/recipes_wibor)."""
r = self.client.get("/api/recipes_wibor")
self.assertEqual(r.status_code, 200)
self.assertIn("text/html", r.content_type or "")
_drain_response(r)
if __name__ == "__main__":
unittest.main()