mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-26 14:21:25 +00:00
- Add pyproject.toml with uv and ruff configuration - Pin Python version to 3.13 via .python-version - Add Makefile commands: lint, format, fix - Apply ruff formatting to entire codebase - Remove unused imports (base64 in yookassa/simple_subscription) - Update .gitignore for new config files
214 lines
6.7 KiB
Python
214 lines
6.7 KiB
Python
import json
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
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,
|
|
resolve_yookassa_ip,
|
|
)
|
|
|
|
|
|
ALLOWED_IP = '185.71.76.10'
|
|
|
|
|
|
class DummyDB:
|
|
async def close(self) -> None: # pragma: no cover - simple stub
|
|
pass
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def configure_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(settings, 'YOOKASSA_ENABLED', True, raising=False)
|
|
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',
|
|
'X-Forwarded-For': ALLOWED_IP,
|
|
'Cf-Connecting-Ip': ALLOWED_IP,
|
|
}
|
|
headers.update(overrides)
|
|
return headers
|
|
|
|
|
|
@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=body.encode('utf-8'),
|
|
headers=_build_headers(**headers),
|
|
)
|
|
|
|
|
|
def _patch_get_db(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
async def fake_get_db():
|
|
yield DummyDB()
|
|
|
|
monkeypatch.setattr('app.external.yookassa_webhook.get_db', fake_get_db)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_webhook_success(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:
|
|
payload = {'event': 'payment.succeeded'}
|
|
body = json.dumps(payload, ensure_ascii=False)
|
|
response = await client.post(
|
|
settings.YOOKASSA_WEBHOOK_PATH,
|
|
data=body.encode('utf-8'),
|
|
headers=_build_headers(),
|
|
)
|
|
status = response.status
|
|
text = await response.text()
|
|
|
|
assert status == 400
|
|
assert text == 'No payment id'
|
|
process_mock.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_webhook_trusts_cf_connecting_ip(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:
|
|
payload = {'event': 'payment.succeeded'}
|
|
body = json.dumps(payload, ensure_ascii=False)
|
|
headers = _build_headers()
|
|
headers.pop('X-Forwarded-For')
|
|
response = await client.post(
|
|
settings.YOOKASSA_WEBHOOK_PATH,
|
|
data=body.encode('utf-8'),
|
|
headers=headers,
|
|
)
|
|
status = response.status
|
|
text = await response.text()
|
|
|
|
assert status == 400
|
|
assert text == 'No payment id'
|
|
process_mock.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_webhook_with_optional_signature(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:
|
|
payload = {'event': 'payment.succeeded'}
|
|
body = json.dumps(payload, ensure_ascii=False)
|
|
response = await client.post(
|
|
settings.YOOKASSA_WEBHOOK_PATH,
|
|
data=body.encode('utf-8'),
|
|
headers=_build_headers(Signature='test-signature'),
|
|
)
|
|
status = response.status
|
|
text = await response.text()
|
|
|
|
assert status == 400
|
|
assert text == 'No payment id'
|
|
process_mock.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_webhook_accepts_canceled_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:
|
|
payload = {'event': 'payment.canceled', 'object': {'id': 'yk_1'}}
|
|
response = await client.post(
|
|
settings.YOOKASSA_WEBHOOK_PATH,
|
|
data=json.dumps(payload).encode('utf-8'),
|
|
headers=_build_headers(),
|
|
)
|
|
|
|
status = response.status
|
|
|
|
assert status == 200
|
|
process_mock.assert_awaited_once()
|