first commit
This commit is contained in:
commit
34ea15a54d
246 changed files with 18660 additions and 0 deletions
174
backend/tests/test_orders.py
Normal file
174
backend/tests/test_orders.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.session import SessionLocal
|
||||
from app.main import app
|
||||
from app.models.catalog import TicketType
|
||||
from app.models.enums import OrderStatus, TicketTypeStatus
|
||||
from app.models.user import User
|
||||
from app.schemas.orders import OrderCreate, OrderLineCreate
|
||||
from app.services.orders import InsufficientInventoryError, create_order
|
||||
from app.services.payments.gateway import MockPaymentGateway, get_payment_gateway
|
||||
from tests.helpers import (
|
||||
auth_headers,
|
||||
client,
|
||||
create_pending_order,
|
||||
first_published_event,
|
||||
pick_tier,
|
||||
register_user,
|
||||
)
|
||||
|
||||
|
||||
def _complete_mock_payment(token: str, checkout: dict) -> None:
|
||||
gateway = get_payment_gateway()
|
||||
assert isinstance(gateway, MockPaymentGateway)
|
||||
payment = checkout["payment"]
|
||||
payment_id = f"mock_pay_{checkout['id']}"
|
||||
signature = gateway.build_mock_checkout_signature(payment["provider_order_id"], payment_id)
|
||||
response = client.post(
|
||||
f"/api/orders/{checkout['id']}/payment/verify",
|
||||
headers=auth_headers(token),
|
||||
json={
|
||||
"provider_order_id": payment["provider_order_id"],
|
||||
"provider_payment_id": payment_id,
|
||||
"signature": signature,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_list_event_tickets() -> None:
|
||||
event = first_published_event()
|
||||
response = client.get(f"/api/events/{event['id']}/tickets")
|
||||
assert response.status_code == 200
|
||||
tiers = response.json()
|
||||
assert tiers
|
||||
assert tiers[0]["event_id"] == event["id"]
|
||||
assert "remaining_quantity" in tiers[0]
|
||||
assert "price_paise" in tiers[0]
|
||||
|
||||
|
||||
def test_create_order_pending_until_verified() -> None:
|
||||
token, checkout = create_pending_order()
|
||||
assert checkout["status"] == OrderStatus.pending.value
|
||||
assert checkout["payment"]["amount_paise"] == checkout["total_paise"]
|
||||
assert checkout["payment"]["provider_order_id"]
|
||||
|
||||
tickets_before = client.get("/api/tickets/my", headers=auth_headers(token)).json()
|
||||
assert not any(t["order_id"] == checkout["id"] for t in tickets_before)
|
||||
|
||||
_complete_mock_payment(token, checkout)
|
||||
|
||||
my_tickets = client.get("/api/tickets/my", headers=auth_headers(token))
|
||||
assert my_tickets.status_code == 200
|
||||
tickets = my_tickets.json()
|
||||
assert any(t["order_id"] == checkout["id"] for t in tickets)
|
||||
assert all(t["code"].startswith("DC-") for t in tickets)
|
||||
|
||||
|
||||
def test_order_requires_authentication() -> None:
|
||||
event = first_published_event()
|
||||
response = client.post(
|
||||
"/api/orders",
|
||||
json={"event_id": event["id"], "items": [{"ticket_type_id": 1, "quantity": 1}]},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_cannot_oversell_inventory() -> None:
|
||||
_, token = register_user()
|
||||
event = first_published_event()
|
||||
tiers = client.get(f"/api/events/{event['id']}/tickets").json()
|
||||
tier = pick_tier(tiers, 1)
|
||||
if not tier:
|
||||
pytest.skip("No ticket inventory available for test")
|
||||
if tier["remaining_quantity"] >= 20:
|
||||
pytest.skip("Quantity cap prevents oversell test for this tier")
|
||||
|
||||
response = client.post(
|
||||
"/api/orders",
|
||||
headers=auth_headers(token),
|
||||
json={
|
||||
"event_id": event["id"],
|
||||
"items": [{"ticket_type_id": tier["id"], "quantity": tier["remaining_quantity"] + 1}],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
def test_server_calculates_total_not_client() -> None:
|
||||
token, checkout = create_pending_order()
|
||||
tiers = client.get(f"/api/events/{checkout['event_id']}/tickets").json()
|
||||
tier = next(t for t in tiers if t["id"] == checkout["items"][0]["ticket_type_id"])
|
||||
qty = checkout["items"][0]["quantity"]
|
||||
assert checkout["total_paise"] == tier["price_paise"] * qty
|
||||
|
||||
|
||||
def test_concurrent_purchases_do_not_oversell() -> None:
|
||||
event = first_published_event()
|
||||
db = SessionLocal()
|
||||
try:
|
||||
tier = db.scalar(
|
||||
select(TicketType).where(TicketType.event_id == event["id"]).order_by(TicketType.id.asc())
|
||||
)
|
||||
assert tier is not None
|
||||
tier.quantity_sold = max(tier.quantity_total - 1, 0)
|
||||
tier.quantity_total = tier.quantity_sold + 1
|
||||
tier.status = TicketTypeStatus.active
|
||||
db.commit()
|
||||
ticket_type_id = tier.id
|
||||
event_id = event["id"]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
user_ids: list[int] = []
|
||||
for _ in range(2):
|
||||
email = f"race-{uuid.uuid4().hex}@example.com"
|
||||
reg = client.post(
|
||||
"/api/auth/register",
|
||||
json={"email": email, "password": "securepass1"},
|
||||
)
|
||||
assert reg.status_code == 201
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.scalar(select(User).where(User.email == email))
|
||||
assert user is not None
|
||||
user_ids.append(user.id)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def _buy(user_id: int) -> str:
|
||||
session = SessionLocal()
|
||||
try:
|
||||
user = session.get(User, user_id)
|
||||
create_order(
|
||||
session,
|
||||
user,
|
||||
OrderCreate(
|
||||
event_id=event_id,
|
||||
items=[OrderLineCreate(ticket_type_id=ticket_type_id, quantity=1)],
|
||||
),
|
||||
)
|
||||
return "success"
|
||||
except InsufficientInventoryError:
|
||||
session.rollback()
|
||||
return "conflict"
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
results = list(pool.map(_buy, user_ids))
|
||||
|
||||
assert results.count("success") == 1
|
||||
assert results.count("conflict") == 1
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
refreshed = db.get(TicketType, ticket_type_id)
|
||||
assert refreshed is not None
|
||||
assert refreshed.quantity_sold == refreshed.quantity_total
|
||||
finally:
|
||||
db.close()
|
||||
Loading…
Add table
Add a link
Reference in a new issue