This commit is contained in:
Deeman
2026-02-19 19:16:23 +01:00
parent 781281f9bc
commit b108a53ef3
57 changed files with 1203 additions and 1048 deletions

View File

@@ -12,6 +12,7 @@ from padelnomics.billing.routes import (
get_subscription_by_provider_id,
is_within_limits,
update_subscription_status,
upsert_billing_customer,
upsert_subscription,
)
from padelnomics.core import config
@@ -40,11 +41,11 @@ class TestGetSubscription:
class TestUpsertSubscription:
async def test_insert_new_subscription(self, db, test_user):
await upsert_billing_customer(test_user["id"], "cust_abc")
sub_id = await upsert_subscription(
user_id=test_user["id"],
plan="pro",
status="active",
provider_customer_id="cust_abc",
provider_subscription_id="sub_xyz",
current_period_end="2025-06-01T00:00:00Z",
)
@@ -52,33 +53,30 @@ class TestUpsertSubscription:
row = await get_subscription(test_user["id"])
assert row["plan"] == "pro"
assert row["status"] == "active"
assert row["paddle_customer_id"] == "cust_abc"
assert row["paddle_subscription_id"] == "sub_xyz"
assert row["provider_subscription_id"] == "sub_xyz"
assert row["current_period_end"] == "2025-06-01T00:00:00Z"
async def test_update_existing_subscription(self, db, test_user, create_subscription):
original_id = await create_subscription(
test_user["id"], plan="starter", status="active",
paddle_subscription_id="sub_old",
provider_subscription_id="sub_old",
)
returned_id = await upsert_subscription(
user_id=test_user["id"],
plan="pro",
status="active",
provider_customer_id="cust_new",
provider_subscription_id="sub_new",
provider_subscription_id="sub_old",
)
assert returned_id == original_id
row = await get_subscription(test_user["id"])
assert row["plan"] == "pro"
assert row["paddle_subscription_id"] == "sub_new"
assert row["provider_subscription_id"] == "sub_old"
async def test_upsert_with_none_period_end(self, db, test_user):
await upsert_subscription(
user_id=test_user["id"],
plan="pro",
status="active",
provider_customer_id="cust_1",
provider_subscription_id="sub_1",
current_period_end=None,
)
@@ -95,8 +93,8 @@ class TestGetSubscriptionByProviderId:
result = await get_subscription_by_provider_id("nonexistent")
assert result is None
async def test_finds_by_paddle_subscription_id(self, db, test_user, create_subscription):
await create_subscription(test_user["id"], paddle_subscription_id="sub_findme")
async def test_finds_by_provider_subscription_id(self, db, test_user, create_subscription):
await create_subscription(test_user["id"], provider_subscription_id="sub_findme")
result = await get_subscription_by_provider_id("sub_findme")
assert result is not None
assert result["user_id"] == test_user["id"]
@@ -108,14 +106,14 @@ class TestGetSubscriptionByProviderId:
class TestUpdateSubscriptionStatus:
async def test_updates_status(self, db, test_user, create_subscription):
await create_subscription(test_user["id"], status="active", paddle_subscription_id="sub_upd")
await create_subscription(test_user["id"], status="active", provider_subscription_id="sub_upd")
await update_subscription_status("sub_upd", status="cancelled")
row = await get_subscription(test_user["id"])
assert row["status"] == "cancelled"
assert row["updated_at"] is not None
async def test_updates_extra_fields(self, db, test_user, create_subscription):
await create_subscription(test_user["id"], paddle_subscription_id="sub_extra")
await create_subscription(test_user["id"], provider_subscription_id="sub_extra")
await update_subscription_status(
"sub_extra",
status="active",
@@ -128,7 +126,7 @@ class TestUpdateSubscriptionStatus:
assert row["current_period_end"] == "2026-01-01T00:00:00Z"
async def test_noop_for_unknown_provider_id(self, db, test_user, create_subscription):
await create_subscription(test_user["id"], paddle_subscription_id="sub_known", status="active")
await create_subscription(test_user["id"], provider_subscription_id="sub_known", status="active")
await update_subscription_status("sub_unknown", status="expired")
row = await get_subscription(test_user["id"])
assert row["status"] == "active" # unchanged
@@ -301,7 +299,7 @@ class TestLimitsHypothesis:
# Use upsert to avoid duplicate inserts across Hypothesis examples
await upsert_subscription(
user_id=test_user["id"], plan="pro", status="active",
provider_customer_id="cust_hyp", provider_subscription_id="sub_hyp",
provider_subscription_id="sub_hyp",
)
result = await is_within_limits(test_user["id"], "items", count)
assert result is True

View File

@@ -82,7 +82,7 @@ class TestManageRoute:
assert response.status_code in (302, 303, 307)
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")
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"
@@ -108,7 +108,7 @@ class TestCancelRoute:
assert response.status_code in (302, 303, 307)
async def test_cancels_subscription(self, auth_client, db, test_user, create_subscription):
await create_subscription(test_user["id"], paddle_subscription_id="sub_test")
await create_subscription(test_user["id"], provider_subscription_id="sub_test")
mock_client = MagicMock()
with patch("padelnomics.billing.routes._paddle_client", return_value=mock_client):
@@ -123,7 +123,7 @@ class TestCancelRoute:
from quart import Blueprint # noqa: E402
from padelnomics.billing.routes import subscription_required # noqa: E402
from padelnomics.auth.routes import subscription_required # noqa: E402
test_bp = Blueprint("test", __name__)

View File

@@ -174,7 +174,7 @@ class TestWebhookSubscriptionActivated:
class TestWebhookSubscriptionUpdated:
async def test_updates_subscription_status(self, client, db, test_user, create_subscription):
await create_subscription(test_user["id"], status="active", paddle_subscription_id="sub_test456")
await create_subscription(test_user["id"], status="active", provider_subscription_id="sub_test456")
payload = make_webhook_payload(
"subscription.updated",
@@ -197,7 +197,7 @@ class TestWebhookSubscriptionUpdated:
class TestWebhookSubscriptionCanceled:
async def test_marks_subscription_cancelled(self, client, db, test_user, create_subscription):
await create_subscription(test_user["id"], status="active", paddle_subscription_id="sub_test456")
await create_subscription(test_user["id"], status="active", provider_subscription_id="sub_test456")
payload = make_webhook_payload(
"subscription.canceled",
@@ -219,7 +219,7 @@ class TestWebhookSubscriptionCanceled:
class TestWebhookSubscriptionPastDue:
async def test_marks_subscription_past_due(self, client, db, test_user, create_subscription):
await create_subscription(test_user["id"], status="active", paddle_subscription_id="sub_test456")
await create_subscription(test_user["id"], status="active", provider_subscription_id="sub_test456")
payload = make_webhook_payload(
"subscription.past_due",
@@ -251,7 +251,7 @@ class TestWebhookSubscriptionPastDue:
])
async def test_event_status_transitions(client, db, test_user, create_subscription, event_type, expected_status):
if event_type != "subscription.activated":
await create_subscription(test_user["id"], paddle_subscription_id="sub_test456")
await create_subscription(test_user["id"], provider_subscription_id="sub_test456")
payload = make_webhook_payload(event_type, user_id=str(test_user["id"]))
payload_bytes = json.dumps(payload).encode()

View File

@@ -744,11 +744,23 @@ class TestRouteRegistration:
# ════════════════════════════════════════════════════════════
@pytest.fixture
async def admin_client(app):
"""Test client with admin session."""
async def admin_client(app, db):
"""Test client with admin user (has admin role)."""
from datetime import datetime
now = datetime.utcnow().isoformat()
async with db.execute(
"INSERT INTO users (email, name, created_at) VALUES (?, ?, ?)",
("admin@test.com", "Admin", now),
) as cursor:
admin_id = cursor.lastrowid
await db.execute(
"INSERT INTO user_roles (user_id, role) VALUES (?, 'admin')", (admin_id,)
)
await db.commit()
async with app.test_client() as c:
async with c.session_transaction() as sess:
sess["is_admin"] = True
sess["user_id"] = admin_id
yield c

View File

@@ -314,6 +314,7 @@ class TestQuoteRequest:
"stakeholder_type": "entrepreneur",
"contact_name": "Test User",
"contact_email": "test@example.com",
"contact_phone": "+491234567890",
"csrf_token": csrf,
},
)
@@ -345,6 +346,7 @@ class TestQuoteRequest:
"stakeholder_type": "entrepreneur",
"contact_name": "Guest",
"contact_email": "guest@example.com",
"contact_phone": "+491234567890",
"csrf_token": csrf,
},
)
@@ -374,6 +376,7 @@ class TestQuoteRequest:
"stakeholder_type": "entrepreneur",
"contact_name": "Auth User",
"contact_email": "test@example.com", # matches test_user email
"contact_phone": "+491234567890",
"csrf_token": csrf,
},
)
@@ -404,6 +407,7 @@ class TestQuoteRequest:
"stakeholder_type": "developer",
"contact_name": "Venue Search",
"contact_email": "venue@example.com",
"contact_phone": "+491234567890",
"csrf_token": csrf,
},
)
@@ -432,6 +436,7 @@ class TestQuoteRequest:
"stakeholder_type": "tennis_club",
"contact_name": "Club Owner",
"contact_email": "club@example.com",
"contact_phone": "+491234567890",
"csrf_token": csrf,
},
)
@@ -460,6 +465,7 @@ class TestQuoteRequest:
"stakeholder_type": "entrepreneur",
"contact_name": "Context Test",
"contact_email": "ctx@example.com",
"contact_phone": "+491234567890",
"csrf_token": csrf,
},
)
@@ -487,7 +493,7 @@ class TestQuoteRequest:
assert resp.status_code == 422
data = await resp.get_json()
assert data["ok"] is False
assert len(data["errors"]) >= 3 # country, timeline, stakeholder_type + name + email
assert len(data["errors"]) >= 3 # country, timeline, stakeholder_type + name + email + phone
# ════════════════════════════════════════════════════════════
@@ -505,6 +511,7 @@ class TestQuoteVerification:
"stakeholder_type": "entrepreneur",
"contact_name": "Verify Test",
"contact_email": "verify@example.com",
"contact_phone": "+491234567890",
}
async def _submit_guest_quote(self, client, db, email="verify@example.com"):