@@ -0,0 +1,139 @@
|
||||
"""Серверная защита 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()
|
||||
Reference in New Issue
Block a user