fix: harden Polygon operation validation
This commit is contained in:
+91
-52
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user