Заглушка: случайная страница при установке вместо девяти фиксированных

stub_site собирает страницу из частей: 20 тем (кафе, пекарня, фотограф, заметки,
мастерские, документация и т. п.) × 5 вёрсток = 100 вариантов, плюс случайное
название, подзаголовок, 3 пункта из 5, адрес, палитра, шрифт и скругление.
Без внешних ресурсов, UTF-8. Вымышленный бренд из старых шаблонов убран, в том числе
из запасной страницы kit net masq. Страница сохраняется в /var/www/kit/index.html
в обоих режимах: её показывают nginx и Hysteria2, её берёт kit net masq, её
возвращает --restore. Тест: tools/test/stub_test.py.
This commit is contained in:
maxdmitrov
2026-10-06 15:54:00 +03:00
parent da07108a83
commit 888a4cb46b
5 changed files with 167 additions and 49 deletions
+73
View File
@@ -0,0 +1,73 @@
"""Проверки сайта-заглушки (stub_site в 3x-ui.sh): python3 tools/test/stub_test.py"""
import html.parser, os, re, subprocess, unittest
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
src = open(os.path.join(root, "scripts", "3x-ui.sh"), encoding="utf-8").read()
func = re.search(r"^stub_site\(\) \{.*?^\}\n", src, re.S | re.M).group(0)
N = 300
# rnd на сервере – через shuf, здесь – через /dev/urandom (на Mac нет shuf; $RANDOM в подоболочках повторяется).
script = ('set -Eeuo pipefail\nrnd() { echo $(( $1 + $(od -An -N2 -tu2 /dev/urandom) % ($2 - $1 + 1) )); }\n' + func
+ 'for i in $(seq %d); do stub_site; echo "@@END@@"; done\n' % N)
raw = subprocess.run(["bash", "-c", script], capture_output=True, check=True).stdout
PAGES = [p.strip() for p in raw.decode("utf-8").split("@@END@@") if p.strip()] # strict: только UTF-8
forbidden = []
fp = os.path.join(root, ".git", "hooks", "forbidden.txt")
if os.path.exists(fp):
forbidden = [w.strip().lower() for w in open(fp, encoding="utf-8") if w.strip() and not w.startswith("#")]
PUBLIC = re.compile(r"vpn|proxy|xray|x-ui|cumulo|hysteria|reality|v2ray|clash|\bkit\b|tunnel|censor|block|bypass|server", re.I)
EXTERNAL = re.compile(r"https?:|//|\bsrc=|\bhref=|<script|<link|<iframe|<img|@import|url\(|\bon[a-z]+=", re.I)
VOID = {"meta", "br", "hr", "input", "img"}
class Balance(html.parser.HTMLParser):
def __init__(self):
super().__init__(); self.stack = []; self.bad = []
def handle_starttag(self, tag, attrs):
if tag not in VOID:
self.stack.append(tag)
def handle_endtag(self, tag):
if not self.stack or self.stack.pop() != tag:
self.bad.append(tag)
class Stub(unittest.TestCase):
def test_count(self):
self.assertEqual(len(PAGES), N)
def test_valid_html(self):
for p in PAGES:
self.assertTrue(p.startswith('<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'), p[:80])
self.assertTrue(p.endswith("</body></html>"), p[-40:])
b = Balance(); b.feed(p); b.close()
self.assertEqual((b.stack, b.bad), ([], []), p)
self.assertEqual(p.count("<title>"), 1)
def test_no_external_and_no_leftovers(self):
for p in PAGES:
self.assertIsNone(EXTERNAL.search(p), p)
text = re.sub(r"<style>.*?</style>", "", p)
self.assertIsNone(re.search(r"[$|^~`\\]", text), p)
self.assertNotIn("\n", p)
def test_words(self):
for p in PAGES:
low = p.lower()
self.assertIsNone(PUBLIC.search(re.sub(r"<style>.*?</style>", "", p)), p) # в стилях есть display:block
for w in forbidden:
self.assertNotIn(w, low)
def test_variety(self):
titles = {re.search(r"<title>(.*?)</title>", p).group(1) for p in PAGES}
self.assertGreater(len(titles), N * 0.8)
layouts = {k for p in PAGES for k, m in (("hero", "place-items:center;text-align"), ("cards", "header{background"),
("nav", "nav{width"), ("notes", "article{"), ("band", ".t{background")) if m in p}
self.assertEqual(len(layouts), 5)
themes = {re.search(r"<title>.*? (\S+)</title>", p).group(1) for p in PAGES}
self.assertGreaterEqual(len(themes), 25) # последние слова названий: Coffee, Bakery, Photography…
if __name__ == "__main__":
unittest.main()