333 lines
11 KiB
Python
333 lines
11 KiB
Python
import asyncio
|
|
|
|
import httpx
|
|
|
|
from app.controllers.v1.delivery import get_aggregator_service
|
|
from app.main import create_app
|
|
from app.schemas.payment import InitPaymentRequest, InitPaymentResponse
|
|
from app.services.aggregator import (
|
|
InitPaymentUnavailableError,
|
|
InvalidInitPaymentRequestError,
|
|
)
|
|
from tests.payment_fixtures import make_init_payment_payload
|
|
|
|
|
|
class StubAggregatorService:
|
|
def __init__(self, *, response: object, error: Exception | None = None) -> None:
|
|
self._response = response
|
|
self._error = error
|
|
self.calls: list[InitPaymentRequest] = []
|
|
|
|
async def init_payment(self, request: InitPaymentRequest) -> object:
|
|
self.calls.append(request)
|
|
if self._error is not None:
|
|
raise self._error
|
|
return self._response
|
|
|
|
|
|
def _install_service_override(app, service: StubAggregatorService) -> None:
|
|
async def override_service() -> StubAggregatorService:
|
|
return service
|
|
|
|
app.dependency_overrides[get_aggregator_service] = override_service
|
|
|
|
|
|
def _post(app, payload: dict[str, object]) -> httpx.Response:
|
|
async def run_request() -> httpx.Response:
|
|
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
|
async with httpx.AsyncClient(
|
|
transport=transport,
|
|
base_url="http://testserver",
|
|
) as client:
|
|
return await client.post("/api/v1/delivery/order", json=payload)
|
|
|
|
return asyncio.run(run_request())
|
|
|
|
|
|
def test_post_init_payment_returns_response_and_delegates_to_service() -> None:
|
|
expected_response = InitPaymentResponse(payment_url="https://pay.test/payment/1")
|
|
service = StubAggregatorService(response=expected_response)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
payload = make_init_payment_payload()
|
|
|
|
response = _post(app, payload)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == expected_response.model_dump(mode="json")
|
|
assert service.calls == [InitPaymentRequest.model_validate(payload)]
|
|
|
|
|
|
def test_post_init_payment_rejects_invalid_tariff_code() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["systemData"]["tariff"]["tariffCode"] = ""
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_snake_case_top_level_field() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["account_email"] = invalid_payload.pop("accountEmail")
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_invalid_account_email() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["accountEmail"] = "not-an-email"
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_snake_case_nested_address_field() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
sender_address = invalid_payload["senderAddress"]
|
|
sender_address["city_id"] = sender_address.pop("cityId")
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_snake_case_nested_tariff_field() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
tariff = invalid_payload["systemData"]["tariff"]
|
|
tariff["tariff_code"] = tariff.pop("tariffCode")
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_missing_price() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
del invalid_payload["systemData"]["tariff"]["price"]
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_non_positive_price() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["systemData"]["tariff"]["price"] = 0
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_non_integer_price() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["systemData"]["tariff"]["price"] = 125000.5
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_missing_declared_value() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
del invalid_payload["content"]["declared_value"]
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_non_integer_declared_value() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["content"]["declared_value"] = "50000"
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_camel_case_declared_value() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["content"]["declaredValue"] = invalid_payload["content"].pop(
|
|
"declared_value"
|
|
)
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_requires_company_fields_when_is_company_true() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["senderContact"]["isCompany"] = True
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_accepts_company_with_full_requisites() -> None:
|
|
expected_response = InitPaymentResponse(payment_url="https://pay.test/payment/1")
|
|
service = StubAggregatorService(response=expected_response)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
payload = make_init_payment_payload()
|
|
payload["senderContact"].update(
|
|
{
|
|
"isCompany": True,
|
|
"companyName": "Romashka LLC",
|
|
"inn": "7707083893",
|
|
"kpp": "770701001",
|
|
}
|
|
)
|
|
|
|
response = _post(app, payload)
|
|
|
|
assert response.status_code == 200
|
|
assert service.calls == [InitPaymentRequest.model_validate(payload)]
|
|
|
|
|
|
def test_post_init_payment_rejects_non_numeric_weight() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["systemData"]["weight"] = "string"
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_non_numeric_dimension() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["systemData"]["dimensions"]["length"] = "string"
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_rejects_parcel_without_dimensions() -> None:
|
|
service = StubAggregatorService(response=None)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
invalid_payload = make_init_payment_payload()
|
|
invalid_payload["systemData"]["dimensions"] = None
|
|
|
|
response = _post(app, invalid_payload)
|
|
|
|
assert response.status_code == 422
|
|
assert service.calls == []
|
|
|
|
|
|
def test_post_init_payment_accepts_doc_without_dimensions() -> None:
|
|
expected_response = InitPaymentResponse(payment_url="https://pay.test/payment/1")
|
|
service = StubAggregatorService(response=expected_response)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
payload = make_init_payment_payload()
|
|
payload["systemData"]["parcelType"] = "doc"
|
|
payload["systemData"]["docPackaging"] = "envelope"
|
|
payload["systemData"]["dimensions"] = None
|
|
|
|
response = _post(app, payload)
|
|
|
|
assert response.status_code == 200
|
|
assert service.calls == [InitPaymentRequest.model_validate(payload)]
|
|
|
|
|
|
def test_post_init_payment_maps_invalid_request_to_400() -> None:
|
|
service = StubAggregatorService(
|
|
response=None,
|
|
error=InvalidInitPaymentRequestError("invalid payload"),
|
|
)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
|
|
response = _post(app, make_init_payment_payload())
|
|
|
|
assert response.status_code == 400
|
|
assert response.json() == {
|
|
"detail": {
|
|
"code": "invalid_init_payment_request",
|
|
"message": "Payment request contains invalid or unsupported TBank data.",
|
|
}
|
|
}
|
|
|
|
|
|
def test_post_init_payment_maps_service_exception_to_503() -> None:
|
|
service = StubAggregatorService(
|
|
response=None,
|
|
error=InitPaymentUnavailableError("service unavailable"),
|
|
)
|
|
app = create_app()
|
|
_install_service_override(app, service)
|
|
|
|
response = _post(app, make_init_payment_payload())
|
|
|
|
assert response.status_code == 503
|
|
assert response.json() == {
|
|
"detail": {
|
|
"code": "init_payment_unavailable",
|
|
"message": "Payment initialization is temporarily unavailable.",
|
|
}
|
|
}
|