145 lines
4.2 KiB
Python
145 lines
4.2 KiB
Python
"""Billing checkout integration tests (ticket 012).
|
|
|
|
Uses respx to mock ЮKassa; enables YOOKASSA_ENABLED in the test env.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
import httpx
|
|
import pytest
|
|
import respx
|
|
from sqlalchemy import text
|
|
|
|
from contract_check.core.db.session import create_session_factory
|
|
from tests.integration.conftest import user_token
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
def _unique_tg() -> int:
|
|
return abs(int(uuid.uuid4().int % 900000000000)) + 100000000000
|
|
|
|
|
|
async def _auth_env(client: httpx.AsyncClient, infra: dict[str, str]) -> tuple[int, str]:
|
|
tg = _unique_tg()
|
|
token = await user_token(client, infra, tg)
|
|
return tg, token
|
|
|
|
|
|
@pytest.fixture
|
|
async def yookassa_enabled():
|
|
"""Temporarily turn on YOOKASSA_ENABLED for tests."""
|
|
import os
|
|
|
|
old = os.environ.get("YOOKASSA_ENABLED")
|
|
os.environ["YOOKASSA_ENABLED"] = "true"
|
|
os.environ["YOOKASSA_SHOP_ID"] = "123456"
|
|
os.environ["YOOKASSA_SECRET_KEY"] = "test-secret"
|
|
os.environ["YOOKASSA_RETURN_BASE_URL"] = "https://example.com/pay/{invoice_id}"
|
|
os.environ["PRICE_PER_DOC_KOPECKS"] = "19900"
|
|
yield
|
|
if old is None:
|
|
os.environ.pop("YOOKASSA_ENABLED", None)
|
|
else:
|
|
os.environ["YOOKASSA_ENABLED"] = old
|
|
os.environ.pop("YOOKASSA_SHOP_ID", None)
|
|
os.environ.pop("YOOKASSA_SECRET_KEY", None)
|
|
os.environ.pop("YOOKASSA_RETURN_BASE_URL", None)
|
|
|
|
|
|
@respx.mock
|
|
async def test_checkout_topup_creates_invoice(
|
|
client: httpx.AsyncClient,
|
|
infra: dict[str, str],
|
|
yookassa_enabled,
|
|
) -> None:
|
|
get_settings.cache_clear()
|
|
_, token = await _auth_env(client, infra)
|
|
|
|
respx.post("https://api.yookassa.ru/v3/payments").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "pay-topup-1",
|
|
"status": "pending",
|
|
"confirmation": {"confirmation_url": "https://yoo.test/topup-1"},
|
|
},
|
|
)
|
|
)
|
|
|
|
r = await client.post(
|
|
"/api/v1/billing/checkout",
|
|
json={"kind": "topup", "credits": 5},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert r.status_code == 201, r.text
|
|
body = r.json()
|
|
assert body["invoice_id"]
|
|
assert body["confirmation_url"] == "https://yoo.test/topup-1"
|
|
|
|
# Invoice persisted as pending.
|
|
factory = create_session_factory()
|
|
async with factory() as s:
|
|
row = (
|
|
await s.execute(
|
|
text(
|
|
"SELECT amount, kind, credits_purchased, status, external_id, confirmation_url FROM invoices WHERE id = :id"
|
|
),
|
|
{"id": uuid.UUID(body["invoice_id"])},
|
|
)
|
|
).first()
|
|
assert row is not None
|
|
assert int(row[0]) == 5 * 19900
|
|
assert row[1] == "topup"
|
|
assert row[2] == 5
|
|
assert row[3] == "pending"
|
|
assert row[4] == "pay-topup-1"
|
|
assert row[5] == "https://yoo.test/topup-1"
|
|
|
|
|
|
async def test_checkout_payments_disabled_503(
|
|
client: httpx.AsyncClient, infra: dict[str, str]
|
|
) -> None:
|
|
_, token = await _auth_env(client, infra)
|
|
r = await client.post(
|
|
"/api/v1/billing/checkout",
|
|
json={"kind": "topup", "credits": 1},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert r.status_code == 503
|
|
|
|
|
|
async def test_checkout_invalid_pack_422(
|
|
client: httpx.AsyncClient,
|
|
infra: dict[str, str],
|
|
yookassa_enabled,
|
|
) -> None:
|
|
get_settings.cache_clear()
|
|
_, token = await _auth_env(client, infra)
|
|
r = await client.post(
|
|
"/api/v1/billing/checkout",
|
|
json={"kind": "topup", "credits": 7},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert r.status_code == 422
|
|
|
|
|
|
async def test_plans_catalog_visible_when_payments_off(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
r = await client.get("/api/v1/billing/plans")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert len(body["plans"]) == 4
|
|
assert body["price_per_doc_kopecks"] == 19900
|
|
|
|
|
|
async def test_invoices_requires_auth(client: httpx.AsyncClient) -> None:
|
|
r = await client.get("/api/v1/billing/invoices")
|
|
assert r.status_code == 401
|
|
|
|
|
|
# Import needed inside fixture to avoid circular import issues at collection time.
|
|
from contract_check.core.config import get_settings # noqa: E402
|