145 lines
5.3 KiB
Python
145 lines
5.3 KiB
Python
import json
|
|
import uuid
|
|
import pytest
|
|
from sqlalchemy import delete, select
|
|
from unittest.mock import patch
|
|
from app.models.subscription import Subscription
|
|
|
|
|
|
def _make_event(event_id, event_type, obj):
|
|
return {
|
|
"id": event_id,
|
|
"type": event_type,
|
|
"data": {"object": obj},
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_checkout_completed_activates_subscription(
|
|
client, test_db, test_user, auth_headers, monkeypatch
|
|
):
|
|
from app.core.config import settings
|
|
monkeypatch.setattr(settings, "STRIPE_SECRET_KEY", "sk_test_dummy")
|
|
monkeypatch.setattr(settings, "STRIPE_WEBHOOK_SECRET", "whsec_dummy")
|
|
|
|
account_id = uuid.UUID(test_user["user_data"]["account_id"])
|
|
# Replace seeded sub with trialing + stripe_customer_id linkage
|
|
from app.models.account import Account
|
|
account = (await test_db.execute(select(Account).where(Account.id == account_id))).scalar_one()
|
|
account.stripe_customer_id = "cus_xxx"
|
|
await test_db.execute(delete(Subscription).where(Subscription.account_id == account_id))
|
|
test_db.add(Subscription(account_id=account_id, plan="pro", status="trialing"))
|
|
await test_db.commit()
|
|
|
|
event = _make_event("evt_co_1", "checkout.session.completed", {
|
|
"id": "cs_xxx",
|
|
"customer": "cus_xxx",
|
|
"subscription": "sub_xxx",
|
|
})
|
|
|
|
with patch("stripe.Subscription.retrieve", return_value={
|
|
"id": "sub_xxx",
|
|
"status": "active",
|
|
"current_period_start": 1714521600,
|
|
"current_period_end": 1717113600,
|
|
"items": {"data": [{
|
|
"price": {"id": "price_test_monthly"},
|
|
"quantity": 5,
|
|
}]},
|
|
"cancel_at_period_end": False,
|
|
}), patch("stripe.Webhook.construct_event", return_value=event):
|
|
response = await client.post(
|
|
"/api/v1/webhooks/stripe",
|
|
content=json.dumps(event),
|
|
headers={"stripe-signature": "fake-sig"},
|
|
)
|
|
assert response.status_code == 200, response.json()
|
|
|
|
sub = (await test_db.execute(
|
|
select(Subscription).where(Subscription.account_id == account_id)
|
|
)).scalar_one()
|
|
assert sub.status == "active"
|
|
assert sub.stripe_subscription_id == "sub_xxx"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subscription_deleted_cancels_account(
|
|
client, test_db, test_user, auth_headers, monkeypatch
|
|
):
|
|
from app.core.config import settings
|
|
monkeypatch.setattr(settings, "STRIPE_SECRET_KEY", "sk_test_dummy")
|
|
monkeypatch.setattr(settings, "STRIPE_WEBHOOK_SECRET", "whsec_dummy")
|
|
|
|
account_id = uuid.UUID(test_user["user_data"]["account_id"])
|
|
await test_db.execute(delete(Subscription).where(Subscription.account_id == account_id))
|
|
test_db.add(Subscription(
|
|
account_id=account_id, plan="pro", status="active",
|
|
stripe_subscription_id="sub_xxx",
|
|
))
|
|
await test_db.commit()
|
|
|
|
event = _make_event("evt_del_1", "customer.subscription.deleted", {
|
|
"id": "sub_xxx",
|
|
"current_period_start": 1714521600,
|
|
"current_period_end": 1717113600,
|
|
"items": {"data": [{"quantity": 1}]},
|
|
})
|
|
|
|
with patch("stripe.Webhook.construct_event", return_value=event):
|
|
response = await client.post(
|
|
"/api/v1/webhooks/stripe",
|
|
content=json.dumps(event),
|
|
headers={"stripe-signature": "fake-sig"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
sub = (await test_db.execute(
|
|
select(Subscription).where(Subscription.account_id == account_id)
|
|
)).scalar_one()
|
|
assert sub.status == "canceled"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_webhook_signature_failure_returns_400(client, monkeypatch):
|
|
from app.core.config import settings
|
|
monkeypatch.setattr(settings, "STRIPE_SECRET_KEY", "sk_test_dummy")
|
|
monkeypatch.setattr(settings, "STRIPE_WEBHOOK_SECRET", "whsec_dummy")
|
|
|
|
with patch("stripe.Webhook.construct_event", side_effect=ValueError("bad sig")):
|
|
response = await client.post(
|
|
"/api/v1/webhooks/stripe",
|
|
content=b"{}",
|
|
headers={"stripe-signature": "fake-sig"},
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_webhook_idempotency(
|
|
client, test_db, test_user, auth_headers, monkeypatch
|
|
):
|
|
from app.core.config import settings
|
|
monkeypatch.setattr(settings, "STRIPE_SECRET_KEY", "sk_test_dummy")
|
|
monkeypatch.setattr(settings, "STRIPE_WEBHOOK_SECRET", "whsec_dummy")
|
|
|
|
account_id = uuid.UUID(test_user["user_data"]["account_id"])
|
|
await test_db.execute(delete(Subscription).where(Subscription.account_id == account_id))
|
|
test_db.add(Subscription(account_id=account_id, plan="pro", status="trialing"))
|
|
await test_db.commit()
|
|
|
|
event = _make_event("evt_dup_1", "customer.subscription.updated", {
|
|
"id": "sub_yyy",
|
|
"status": "active",
|
|
"current_period_start": 1714521600,
|
|
"current_period_end": 1717113600,
|
|
"items": {"data": [{"quantity": 1}]},
|
|
"cancel_at_period_end": False,
|
|
})
|
|
with patch("stripe.Webhook.construct_event", return_value=event):
|
|
r1 = await client.post("/api/v1/webhooks/stripe", content=json.dumps(event), headers={"stripe-signature": "x"})
|
|
r2 = await client.post("/api/v1/webhooks/stripe", content=json.dumps(event), headers={"stripe-signature": "x"})
|
|
assert r1.status_code == 200
|
|
assert r2.status_code == 200
|
|
assert r1.json()["applied"] is True
|
|
assert r2.json()["applied"] is False
|