Files
payment-processing-test/tests/integration/test_payments_api.py
T

123 lines
3.9 KiB
Python

import asyncio
import uuid
from decimal import Decimal
import httpx2
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import OutboxEvent, Payment, PaymentStatus
PAYMENT_BODY: dict[str, object] = {
"amount": "1250.50",
"currency": "RUB",
"description": "Integration payment",
"metadata": {"order_id": 123},
"webhook_url": "https://example.com/webhook",
}
AUTH_HEADERS = {"X-API-Key": "test-key"}
async def create_payment(
client: httpx2.AsyncClient,
idempotency_key: str,
body: dict[str, object] | None = None,
) -> httpx2.Response:
return await client.post(
"/api/v1/payments",
headers={**AUTH_HEADERS, "Idempotency-Key": idempotency_key},
json=body or PAYMENT_BODY,
)
async def test_create_payment_persists_payment_and_outbox_atomically(
api_client: httpx2.AsyncClient,
db_session: AsyncSession,
) -> None:
response = await create_payment(api_client, "order-123")
assert response.status_code == 202
body = response.json()
assert body["status"] == PaymentStatus.PENDING
payment_id = uuid.UUID(body["payment_id"])
payment = await db_session.get(Payment, payment_id)
event = await db_session.scalar(
select(OutboxEvent).where(OutboxEvent.aggregate_id == payment_id)
)
assert payment is not None
assert payment.amount == Decimal("1250.50")
assert payment.metadata_ == PAYMENT_BODY["metadata"]
assert event is not None
assert event.payload == {"payment_id": str(payment_id)}
assert event.published_at is None
async def test_get_returns_created_payment(
api_client: httpx2.AsyncClient,
) -> None:
created = await create_payment(api_client, "get-payment")
payment_id = created.json()["payment_id"]
response = await api_client.get(
f"/api/v1/payments/{payment_id}",
headers=AUTH_HEADERS,
)
assert response.status_code == 200
assert response.json() == {
"payment_id": payment_id,
"amount": "1250.50",
"currency": "RUB",
"description": "Integration payment",
"metadata": {"order_id": 123},
"status": "pending",
"idempotency_key": "get-payment",
"webhook_url": "https://example.com/webhook",
"created_at": created.json()["created_at"],
"processed_at": None,
}
async def test_get_unknown_payment_returns_404(api_client: httpx2.AsyncClient) -> None:
response = await api_client.get(
f"/api/v1/payments/{uuid.uuid4()}",
headers=AUTH_HEADERS,
)
assert response.status_code == 404
assert response.json() == {"detail": "Payment not found"}
async def test_repeated_idempotency_key_returns_original_payment(
api_client: httpx2.AsyncClient,
db_session: AsyncSession,
) -> None:
first = await create_payment(api_client, "same-key")
second_body: dict[str, object] = {
**PAYMENT_BODY,
"amount": "999.99",
"currency": "USD",
}
second = await create_payment(api_client, "same-key", second_body)
assert second.status_code == 202
assert second.json()["payment_id"] == first.json()["payment_id"]
assert await db_session.scalar(select(func.count()).select_from(Payment)) == 1
assert await db_session.scalar(select(func.count()).select_from(OutboxEvent)) == 1
async def test_concurrent_requests_create_one_payment_and_one_event(
api_client: httpx2.AsyncClient,
db_session: AsyncSession,
) -> None:
responses = await asyncio.gather(
create_payment(api_client, "concurrent-key"),
create_payment(api_client, "concurrent-key"),
)
assert {response.status_code for response in responses} == {202}
assert len({response.json()["payment_id"] for response in responses}) == 1
assert await db_session.scalar(select(func.count()).select_from(Payment)) == 1
assert await db_session.scalar(select(func.count()).select_from(OutboxEvent)) == 1