Update from Copier template v0.4.0
- Accept RBAC system: user_roles table, role_required decorator, grant_role/revoke_role/ensure_admin_role functions - Accept improved billing architecture: billing_customers table separation, provider-agnostic naming - Accept enhanced user loading with subscription/roles eager loading in app.py - Accept improved email templates with branded styling - Accept new infrastructure: migration tracking, transaction logging, A/B testing - Accept template improvements: Resend SDK, Tailwind build stage, UMAMI analytics config - Keep beanflows-specific configs: BASE_URL 5001, coffee PLAN_FEATURES/PLAN_LIMITS - Keep beanflows analytics integration and DuckDB health check - Add new test files and utility scripts from template Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
"""
|
||||
Route integration tests for Paddle billing endpoints.
|
||||
External Paddle API calls mocked with respx.
|
||||
"""
|
||||
import json
|
||||
|
||||
import httpx
|
||||
Paddle SDK calls mocked via mock_paddle_client fixture.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
|
||||
CHECKOUT_METHOD = "POST"
|
||||
@@ -54,24 +57,16 @@ class TestCheckoutRoute:
|
||||
|
||||
assert response.status_code in (302, 303, 307)
|
||||
|
||||
@respx.mock
|
||||
async def test_creates_checkout_session(self, auth_client, db, test_user):
|
||||
|
||||
respx.post("https://api.paddle.com/transactions").mock(
|
||||
return_value=httpx.Response(200, json={
|
||||
"data": {
|
||||
"checkout": {
|
||||
"url": "https://checkout.paddle.com/test_123"
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
async def test_creates_checkout_session(self, auth_client, db, test_user, mock_paddle_client):
|
||||
mock_txn = MagicMock()
|
||||
mock_txn.checkout.url = "https://checkout.paddle.com/test_123"
|
||||
mock_paddle_client.transactions.create.return_value = mock_txn
|
||||
|
||||
response = await auth_client.post(f"/billing/checkout/{CHECKOUT_PLAN}", follow_redirects=False)
|
||||
|
||||
assert response.status_code in (302, 303, 307)
|
||||
mock_paddle_client.transactions.create.assert_called_once()
|
||||
|
||||
|
||||
async def test_invalid_plan_rejected(self, auth_client, db, test_user):
|
||||
|
||||
@@ -82,20 +77,13 @@ class TestCheckoutRoute:
|
||||
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_api_error_propagates(self, auth_client, db, test_user):
|
||||
|
||||
respx.post("https://api.paddle.com/transactions").mock(
|
||||
return_value=httpx.Response(500, json={"error": "server error"})
|
||||
)
|
||||
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
|
||||
async def test_api_error_propagates(self, auth_client, db, test_user, mock_paddle_client):
|
||||
mock_paddle_client.transactions.create.side_effect = Exception("API error")
|
||||
with pytest.raises(Exception, match="API error"):
|
||||
await auth_client.post(f"/billing/checkout/{CHECKOUT_PLAN}")
|
||||
|
||||
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# Manage subscription / Portal
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@@ -110,24 +98,18 @@ class TestManageRoute:
|
||||
response = await auth_client.post("/billing/manage", follow_redirects=False)
|
||||
assert response.status_code in (302, 303, 307)
|
||||
|
||||
@respx.mock
|
||||
async def test_redirects_to_portal(self, auth_client, db, test_user, create_subscription):
|
||||
|
||||
await create_subscription(test_user["id"], paddle_subscription_id="sub_test")
|
||||
|
||||
respx.get("https://api.paddle.com/subscriptions/sub_test").mock(
|
||||
return_value=httpx.Response(200, json={
|
||||
"data": {
|
||||
"management_urls": {
|
||||
"update_payment_method": "https://paddle.com/manage/test_123"
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
async def test_redirects_to_portal(self, auth_client, db, test_user, create_subscription, mock_paddle_client):
|
||||
await create_subscription(test_user["id"], provider_subscription_id="sub_test")
|
||||
|
||||
mock_sub = MagicMock()
|
||||
mock_sub.management_urls.update_payment_method = "https://paddle.com/manage/test_123"
|
||||
mock_paddle_client.subscriptions.get.return_value = mock_sub
|
||||
|
||||
response = await auth_client.post("/billing/manage", follow_redirects=False)
|
||||
assert response.status_code in (302, 303, 307)
|
||||
mock_paddle_client.subscriptions.get.assert_called_once_with("sub_test")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -145,18 +127,14 @@ class TestCancelRoute:
|
||||
response = await auth_client.post("/billing/cancel", follow_redirects=False)
|
||||
assert response.status_code in (302, 303, 307)
|
||||
|
||||
@respx.mock
|
||||
async def test_cancels_subscription(self, auth_client, db, test_user, create_subscription):
|
||||
|
||||
await create_subscription(test_user["id"], paddle_subscription_id="sub_test")
|
||||
|
||||
respx.post("https://api.paddle.com/subscriptions/sub_test/cancel").mock(
|
||||
return_value=httpx.Response(200, json={"data": {}})
|
||||
)
|
||||
|
||||
async def test_cancels_subscription(self, auth_client, db, test_user, create_subscription, mock_paddle_client):
|
||||
await create_subscription(test_user["id"], provider_subscription_id="sub_test")
|
||||
|
||||
response = await auth_client.post("/billing/cancel", follow_redirects=False)
|
||||
assert response.status_code in (302, 303, 307)
|
||||
mock_paddle_client.subscriptions.cancel.assert_called_once()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -167,8 +145,9 @@ class TestCancelRoute:
|
||||
# subscription_required decorator
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
from beanflows.billing.routes import subscription_required
|
||||
from quart import Blueprint
|
||||
from quart import Blueprint # noqa: E402
|
||||
|
||||
from beanflows.auth.routes import subscription_required # noqa: E402
|
||||
|
||||
test_bp = Blueprint("test", __name__)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user