mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Revert "Dev4"
This commit is contained in:
159
tests/external/test_yookassa_webhook.py
vendored
159
tests/external/test_yookassa_webhook.py
vendored
@@ -7,7 +7,13 @@ from aiohttp import web
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
|
||||
from app.config import settings
|
||||
from app.external.yookassa_webhook import create_yookassa_webhook_app
|
||||
from app.external.yookassa_webhook import (
|
||||
create_yookassa_webhook_app,
|
||||
resolve_yookassa_ip,
|
||||
)
|
||||
|
||||
|
||||
ALLOWED_IP = "185.71.76.10"
|
||||
|
||||
|
||||
class DummyDB:
|
||||
@@ -19,22 +25,84 @@ def configure_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_SHOP_ID", "shop", raising=False)
|
||||
monkeypatch.setattr(settings, "YOOKASSA_SECRET_KEY", "key", raising=False)
|
||||
monkeypatch.setattr(settings, "YOOKASSA_WEBHOOK_PATH", "/yookassa-webhook", raising=False)
|
||||
monkeypatch.setattr(settings, "YOOKASSA_TRUSTED_PROXY_NETWORKS", "", raising=False)
|
||||
|
||||
|
||||
def _build_headers(**overrides: str) -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Forwarded-For": ALLOWED_IP,
|
||||
}
|
||||
headers.update(overrides)
|
||||
return headers
|
||||
|
||||
|
||||
def _json_bytes(payload: dict) -> bytes:
|
||||
return json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
@pytest.mark.parametrize(
|
||||
("remote", "expected"),
|
||||
(
|
||||
("185.71.76.10", "185.71.76.10"),
|
||||
("8.8.8.8", "8.8.8.8"),
|
||||
("10.0.0.5", "185.71.76.10"),
|
||||
(None, "185.71.76.10"),
|
||||
),
|
||||
)
|
||||
def test_resolve_yookassa_ip_trust_rules(remote: str | None, expected: str) -> None:
|
||||
candidates = [ALLOWED_IP]
|
||||
ip_object = resolve_yookassa_ip(candidates, remote=remote)
|
||||
|
||||
assert ip_object is not None
|
||||
assert str(ip_object) == expected
|
||||
|
||||
|
||||
def test_resolve_yookassa_ip_prefers_last_forwarded_candidate() -> None:
|
||||
candidates = ["185.71.76.10", "8.8.8.8"]
|
||||
|
||||
ip_object = resolve_yookassa_ip(candidates, remote="10.0.0.5")
|
||||
|
||||
assert ip_object is not None
|
||||
assert str(ip_object) == "8.8.8.8"
|
||||
|
||||
|
||||
def test_resolve_yookassa_ip_accepts_allowed_last_forwarded_candidate() -> None:
|
||||
candidates = ["8.8.8.8", ALLOWED_IP]
|
||||
|
||||
ip_object = resolve_yookassa_ip(candidates, remote="10.0.0.5")
|
||||
|
||||
assert ip_object is not None
|
||||
assert str(ip_object) == ALLOWED_IP
|
||||
|
||||
|
||||
def test_resolve_yookassa_ip_skips_trusted_proxy_hops(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_TRUSTED_PROXY_NETWORKS", "203.0.113.0/24", raising=False)
|
||||
|
||||
candidates = [ALLOWED_IP, "203.0.113.10"]
|
||||
|
||||
ip_object = resolve_yookassa_ip(candidates, remote="10.0.0.5")
|
||||
|
||||
assert ip_object is not None
|
||||
assert str(ip_object) == ALLOWED_IP
|
||||
|
||||
|
||||
def test_resolve_yookassa_ip_trusted_public_proxy(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_TRUSTED_PROXY_NETWORKS", "198.51.100.0/24", raising=False)
|
||||
|
||||
candidates = [ALLOWED_IP, "198.51.100.10"]
|
||||
|
||||
ip_object = resolve_yookassa_ip(candidates, remote="198.51.100.20")
|
||||
|
||||
assert ip_object is not None
|
||||
assert str(ip_object) == ALLOWED_IP
|
||||
|
||||
|
||||
def test_resolve_yookassa_ip_returns_none_when_no_candidates() -> None:
|
||||
assert resolve_yookassa_ip([], remote=None) is None
|
||||
|
||||
|
||||
async def _post_webhook(client: TestClient, payload: dict, **headers: str) -> web.Response:
|
||||
body = json.dumps(payload, ensure_ascii=False)
|
||||
return await client.post(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
data=_json_bytes(payload),
|
||||
data=body.encode("utf-8"),
|
||||
headers=_build_headers(**headers),
|
||||
)
|
||||
|
||||
@@ -106,7 +174,7 @@ async def test_handle_webhook_accepts_canceled_event(monkeypatch: pytest.MonkeyP
|
||||
payload = {"event": "payment.canceled", "object": {"id": "yk_1"}}
|
||||
response = await client.post(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
data=_json_bytes(payload),
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers=_build_headers(),
|
||||
)
|
||||
|
||||
@@ -114,82 +182,3 @@ async def test_handle_webhook_accepts_canceled_event(monkeypatch: pytest.MonkeyP
|
||||
|
||||
assert status == 200
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_webhook_rejects_empty_body(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_patch_get_db(monkeypatch)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
app = create_yookassa_webhook_app(service)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
response = await client.post(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
data=b"",
|
||||
headers=_build_headers(),
|
||||
)
|
||||
|
||||
assert response.status == 400
|
||||
process_mock.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_webhook_rejects_invalid_json(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_patch_get_db(monkeypatch)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
app = create_yookassa_webhook_app(service)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
response = await client.post(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
data=b"{not-json}",
|
||||
headers=_build_headers(),
|
||||
)
|
||||
|
||||
assert response.status == 400
|
||||
process_mock.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_webhook_requires_event(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_patch_get_db(monkeypatch)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
app = create_yookassa_webhook_app(service)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
response = await client.post(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
data=_json_bytes({}),
|
||||
headers=_build_headers(),
|
||||
)
|
||||
|
||||
assert response.status == 400
|
||||
process_mock.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_webhook_ignores_unhandled_event(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_patch_get_db(monkeypatch)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
app = create_yookassa_webhook_app(service)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
response = await client.post(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
data=_json_bytes({"event": "unknown.event"}),
|
||||
headers=_build_headers(),
|
||||
)
|
||||
status = response.status
|
||||
text = await response.text()
|
||||
|
||||
assert status == 200
|
||||
assert text == "OK"
|
||||
process_mock.assert_not_called()
|
||||
|
||||
@@ -26,6 +26,7 @@ def reset_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_WEBHOOK_PATH", "/yookassa", raising=False)
|
||||
monkeypatch.setattr(settings, "YOOKASSA_SHOP_ID", "shop", raising=False)
|
||||
monkeypatch.setattr(settings, "YOOKASSA_SECRET_KEY", "key", raising=False)
|
||||
monkeypatch.setattr(settings, "YOOKASSA_TRUSTED_PROXY_NETWORKS", "", raising=False)
|
||||
monkeypatch.setattr(settings, "WEBHOOK_URL", "http://test", raising=False)
|
||||
|
||||
|
||||
@@ -40,6 +41,7 @@ def _build_request(
|
||||
path: str,
|
||||
body: bytes,
|
||||
headers: dict[str, str],
|
||||
client_ip: str | None = "185.71.76.1",
|
||||
) -> Request:
|
||||
scope = {
|
||||
"type": "http",
|
||||
@@ -49,6 +51,9 @@ def _build_request(
|
||||
"headers": [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in headers.items()],
|
||||
}
|
||||
|
||||
if client_ip is not None:
|
||||
scope["client"] = (client_ip, 12345)
|
||||
|
||||
async def receive() -> dict:
|
||||
return {"type": "http.request", "body": body, "more_body": False}
|
||||
|
||||
@@ -93,6 +98,235 @@ async def test_tribute_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_unknown_ip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
service = SimpleNamespace(process_yookassa_webhook=AsyncMock())
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={},
|
||||
client_ip=None,
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 403
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["reason"] == "unknown_ip"
|
||||
service.process_yookassa_webhook.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_forbidden_ip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
service = SimpleNamespace(process_yookassa_webhook=AsyncMock())
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={},
|
||||
client_ip="8.8.8.8",
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 403
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["reason"] == "forbidden_ip"
|
||||
assert payload["ip"] == "8.8.8.8"
|
||||
service.process_yookassa_webhook.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_forbidden_ip_ignores_spoofed_header(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
service = SimpleNamespace(process_yookassa_webhook=AsyncMock())
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={"X-Forwarded-For": "185.71.76.10"},
|
||||
client_ip="8.8.8.8",
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 403
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["reason"] == "forbidden_ip"
|
||||
assert payload["ip"] == "8.8.8.8"
|
||||
service.process_yookassa_webhook.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_forbidden_ip_ignores_spoofed_forwarded_chain(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
service = SimpleNamespace(process_yookassa_webhook=AsyncMock())
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={"X-Forwarded-For": "185.71.76.10, 8.8.8.8"},
|
||||
client_ip="10.0.0.5",
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 403
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["reason"] == "forbidden_ip"
|
||||
assert payload["ip"] == "8.8.8.8"
|
||||
service.process_yookassa_webhook.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_allowed_ip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
async def fake_get_db():
|
||||
yield SimpleNamespace()
|
||||
|
||||
monkeypatch.setattr("app.webserver.payments.get_db", fake_get_db)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={},
|
||||
client_ip="185.71.76.10",
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["status"] == "ok"
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_allowed_via_forwarded_header_when_proxy(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
async def fake_get_db():
|
||||
yield SimpleNamespace()
|
||||
|
||||
monkeypatch.setattr("app.webserver.payments.get_db", fake_get_db)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={"X-Forwarded-For": "185.71.76.10"},
|
||||
client_ip="10.0.0.5",
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["status"] == "ok"
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_allowed_via_trusted_forwarded_chain(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
monkeypatch.setattr(settings, "YOOKASSA_TRUSTED_PROXY_NETWORKS", "203.0.113.0/24", raising=False)
|
||||
|
||||
async def fake_get_db():
|
||||
yield SimpleNamespace()
|
||||
|
||||
monkeypatch.setattr("app.webserver.payments.get_db", fake_get_db)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={"X-Forwarded-For": "185.71.76.10, 203.0.113.10"},
|
||||
client_ip="10.0.0.5",
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["status"] == "ok"
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_allowed_via_trusted_public_proxy(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
monkeypatch.setattr(settings, "YOOKASSA_TRUSTED_PROXY_NETWORKS", "198.51.100.0/24", raising=False)
|
||||
|
||||
async def fake_get_db():
|
||||
yield SimpleNamespace()
|
||||
|
||||
monkeypatch.setattr("app.webserver.payments.get_db", fake_get_db)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={"X-Forwarded-For": "185.71.76.10, 198.51.100.10"},
|
||||
client_ip="198.51.100.20",
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["status"] == "ok"
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
Reference in New Issue
Block a user