fix: harden Polygon operation validation

This commit is contained in:
Repinoid
2026-10-25 14:07:13 +03:00
parent 3df89ec9e7
commit e3a031ce50
18 changed files with 803 additions and 346 deletions
+91 -52
View File
@@ -1,13 +1,26 @@
#!/usr/bin/env python3
"""fuzz_test.py — фаззинг Polygon: злонамеренные и экстремальные сценарии × 3 стенда."""
"""fuzz_test.py — локальные злонамеренные и экстремальные сценарии по стендам."""
if __name__ != "__main__":
import pytest
pytest.skip("standalone local fuzz scenarios; run explicitly with python", allow_module_level=True)
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "site"))
from app import app
import json
import config.loader as config
client = app.test_client()
H = {"Content-Type": "application/json"}
MH = {**H, "X-Mock-Auth": "test-token-123"}
stands = ["dev", "test", "prod"]
AUTH = os.getenv("MOCK_AUTH_TOKEN", "")
MH = {**H, **({"X-Mock-Auth": AUTH} if AUTH else {})}
stands = config.list_stands()
passed = 0
failed = 0
errors = []
@@ -89,7 +102,7 @@ for sid in stands:
json={"serviceId": 0, "displayName": "x"}, headers=H), 400))
t("displayName=число", lambda: st(client.post(f"{base}/api/v1/svc/instances",
json={"serviceId": 1, "displayName": 12345}, headers=H), 201))
json={"serviceId": 1, "displayName": 12345}, headers=H), 400))
t("paramId строка", lambda: st(client.post(f"{base}/api/v1/svc/instanceOperationCfsParams",
json={"instanceOperationUid": "00000000-0000-0000-0000-000000000000",
@@ -98,9 +111,10 @@ for sid in stands:
# ═══ 3. PAGINATION ═══
print("\n── Пагинация ──")
t("pageSize=999999", lambda: (
d := js(client.get(f"{base}/api/v1/svc/instances?pageSize=999999")),
d["pageSize"] <= 200 or True))
def test_page_size_upper_bound():
d = js(client.get(f"{base}/api/v1/svc/instances?pageSize=999999"))
assert d["pageSize"] == 200
t("pageSize=999999", test_page_size_upper_bound)
t("pageSize=0", lambda: st(client.get(f"{base}/api/v1/svc/instances?pageSize=0"), 200))
@@ -108,9 +122,10 @@ for sid in stands:
t("pageSize=''", lambda: st(client.get(f"{base}/api/v1/svc/instances?pageSize="), 200))
t("page=-100", lambda: (
d := js(client.get(f"{base}/api/v1/svc/instances?page=-100")),
d["page"] >= 1 or True))
def test_page_lower_bound():
d = js(client.get(f"{base}/api/v1/svc/instances?page=-100"))
assert d["page"] == 1
t("page=-100", test_page_lower_bound)
# ═══ 4. INVALID UUID ═══
print("\n── Невалидные UUID ──")
@@ -190,20 +205,28 @@ for sid in stands:
# ═══ 9. PATH TRAVERSAL ═══
print("\n── Path traversal ──")
t("GET /../services", lambda: st(client.get(f"{base}/api/v1/svc/../services"), 308))
t("GET двойной слеш", lambda: st(client.get(f"{base}//api/v1/svc/services"), 308))
t("GET /../services → 404", lambda: st(client.get(f"{base}/api/v1/svc/../services"), 404))
def test_double_slash_is_normalized():
response = client.get(f"{base}//api/v1/svc/services")
st(response, 200)
assert "results" in js(response)
t("GET двойной слеш → нормализованный список", test_double_slash_is_normalized)
# ═══ 10. DELAY ═══
print("\n── Задержка ──")
t("delay 999", lambda: st(client.post(f"{base}/api/v1/svc/_mock/delay/999", headers=MH), 400))
t("delay -1", lambda: st(client.post(f"{base}/api/v1/svc/_mock/delay/-1", headers=MH), 400))
t("delay 0", lambda: (
d := js(client.post(f"{base}/api/v1/svc/_mock/delay/0", headers=MH)),
d["delay"] == 0 or True))
t("delay 5.0", lambda: (
d := js(client.post(f"{base}/api/v1/svc/_mock/delay/5", headers=MH)),
d["delay"] == 5.0 or True))
def test_zero_delay():
d = js(client.post(f"{base}/api/v1/svc/_mock/delay/0", headers=MH))
assert d["delay"] == 0
t("delay 0", test_zero_delay)
def test_max_delay():
d = js(client.post(f"{base}/api/v1/svc/_mock/delay/5", headers=MH))
assert d["delay"] == 5.0
t("delay 5.0", test_max_delay)
client.post(f"{base}/api/v1/svc/_mock/delay/0.1", headers=MH)
# ═══ 11. FIELDS ═══
@@ -225,13 +248,9 @@ for sid in stands:
# ═══ 13. PAGES ═══
print("\n── Страницы ──")
t("GET /{sid}/ → HTML", lambda: (
r := client.get(f"{base}/"), st(r, 200),
b"Polygon" in r.data or True))
t("GET /{sid}/ → HTML", lambda: st(client.get(f"{base}/"), 200))
t("GET /swagger → HTML", lambda: (
r := client.get("/swagger"), st(r, 200),
b"swagger" in r.data.lower() or True))
t("GET /swagger → HTML", lambda: st(client.get("/swagger"), 200))
t("GET /health → OK", lambda: (
r := client.get("/health"), st(r, 200), r.data == b"OK"))
@@ -261,37 +280,57 @@ for sid in stands:
assert inst["instance"]["displayName"] == f"only-on-{sid}"
t(f"{sid}: только свой инстанс", check_isolated)
# Cross-stand: touch instance from wrong stand
t("чужая instance → 404", lambda: (
st(client.get(f"/dev/api/v1/svc/instances/{inst_by_stand['test']}"), 404)))
# Cross-stand checks apply only when at least two stands are configured.
if len(stands) >= 2:
first_stand, second_stand = stands[:2]
t(
"чужая instance → 404",
lambda: st(
client.get(
f"/{first_stand}/api/v1/svc/instances/{inst_by_stand[second_stand]}"
),
404,
),
)
t("чужая instance → 404 (prod→dev)", lambda: (
st(client.get(f"/prod/api/v1/svc/instances/{inst_by_stand['dev']}"), 404)))
client.post(f"/{first_stand}/api/v1/svc/_mock/reset", headers=MH)
client.post(f"/{second_stand}/api/v1/svc/_mock/reset", headers=MH)
client.post(f"/{first_stand}/api/v1/svc/_mock/fail-next", headers=MH)
# fail-next isolation
client.post(f"/dev/api/v1/svc/_mock/reset", headers=MH)
client.post(f"/test/api/v1/svc/_mock/reset", headers=MH)
client.post(f"/dev/api/v1/svc/_mock/fail-next", headers=MH)
def test_fail_isolation():
first_instance = js(client.post(
f"/{first_stand}/api/v1/svc/instances",
json={"serviceId": 1, "displayName": "fail-isolation"}, headers=H,
))["instanceUid"]
first_operation = js(client.post(
f"/{first_stand}/api/v1/svc/instanceOperations",
json={"instanceUid": first_instance, "operation": "create", "svcOperationId": 18},
headers=H,
))["instanceOperationUid"]
first_result = js(client.post(
f"/{first_stand}/api/v1/svc/instanceOperations/{first_operation}/run",
headers=H,
))
assert first_result["ok"] is False
def test_fail_isolation():
# dev: fail-next active
r = client.post(f"/dev/api/v1/svc/instances", json={"serviceId": 1, "displayName": "f"}, headers=H)
fi = js(r)["instanceUid"]
r = client.post(f"/dev/api/v1/svc/instanceOperations",
json={"instanceUid": fi, "operation": "create", "svcOperationId": 18}, headers=H)
fo = js(r)["instanceOperationUid"]
d = js(client.post(f"/dev/api/v1/svc/instanceOperations/{fo}/run", headers=H))
assert d["ok"] is False, "dev должен упасть"
second_instance = js(client.post(
f"/{second_stand}/api/v1/svc/instances",
json={"serviceId": 1, "displayName": "normal"}, headers=H,
))["instanceUid"]
second_operation = js(client.post(
f"/{second_stand}/api/v1/svc/instanceOperations",
json={"instanceUid": second_instance, "operation": "create", "svcOperationId": 18},
headers=H,
))["instanceOperationUid"]
second_result = js(client.post(
f"/{second_stand}/api/v1/svc/instanceOperations/{second_operation}/run",
headers=H,
))
assert second_result["ok"] is True
# test: без fail-next, должен работать
r = client.post(f"/test/api/v1/svc/instances", json={"serviceId": 1, "displayName": "ok"}, headers=H)
ti = js(r)["instanceUid"]
r = client.post(f"/test/api/v1/svc/instanceOperations",
json={"instanceUid": ti, "operation": "create", "svcOperationId": 18}, headers=H)
to = js(r)["instanceOperationUid"]
d = js(client.post(f"/test/api/v1/svc/instanceOperations/{to}/run", headers=H))
assert d["ok"] is True, "test должен работать"
t("fail-next изоляция: dev падает, test работает", test_fail_isolation)
t("fail-next изоляция между стендами", test_fail_isolation)
else:
print(" Cross-stand isolation skipped: fewer than two stands configured")
# ═══════ SUMMARY ═══════
@@ -303,5 +342,5 @@ if failed:
for e in errors:
print(f" {e}")
else:
print(f" 🎉 ВСЕ ТЕСТЫ ПРОЙДЕНЫ — {total} тестов × 3 стенда!")
print(f" 🎉 ВСЕ ТЕСТЫ ПРОЙДЕНЫ — {total} проверок, {len(stands)} стенд(а)")
print(f"{'='*55}")