fix webhook crashes on null custom_data, migrate to SDK Verifier
Paddle sandbox sends lifecycle events (subscription.updated, etc.) with
"custom_data": null. The .get("custom_data", {}) default only applies
when the key is missing, not when the value is explicitly null, causing
AttributeError on the next .get() call. Also guarded subscription.activated
to skip when user_id is absent (was inserting user_id=0 → FK violation).
Replaced manual HMAC verification with paddle_billing.Notifications.Verifier
via a lightweight _WebhookRequest wrapper satisfying the SDK's Request Protocol.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,72 @@ class TestWebhookSignature:
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
async def test_null_custom_data_does_not_crash(self, client, db):
|
||||
"""Paddle sends null custom_data on lifecycle events like subscription.updated."""
|
||||
payload = {
|
||||
"event_type": "subscription.updated",
|
||||
"data": {
|
||||
"id": "sub_test456",
|
||||
"status": "active",
|
||||
"customer_id": "ctm_test123",
|
||||
"custom_data": None,
|
||||
"current_billing_period": {
|
||||
"starts_at": "2025-02-01T00:00:00.000000Z",
|
||||
"ends_at": "2025-03-01T00:00:00.000000Z",
|
||||
},
|
||||
},
|
||||
}
|
||||
payload_bytes = json.dumps(payload).encode()
|
||||
sig = sign_payload(payload_bytes)
|
||||
|
||||
response = await client.post(
|
||||
WEBHOOK_PATH,
|
||||
data=payload_bytes,
|
||||
headers={SIG_HEADER: sig, "Content-Type": "application/json"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_null_custom_data_activated_does_not_crash(self, client, db):
|
||||
"""subscription.activated with null custom_data must not FK-violate on user_id=0."""
|
||||
payload = {
|
||||
"event_type": "subscription.activated",
|
||||
"data": {
|
||||
"id": "sub_test456",
|
||||
"status": "active",
|
||||
"customer_id": "ctm_test123",
|
||||
"custom_data": None,
|
||||
"current_billing_period": {
|
||||
"starts_at": "2025-02-01T00:00:00.000000Z",
|
||||
"ends_at": "2025-03-01T00:00:00.000000Z",
|
||||
},
|
||||
},
|
||||
}
|
||||
payload_bytes = json.dumps(payload).encode()
|
||||
sig = sign_payload(payload_bytes)
|
||||
|
||||
response = await client.post(
|
||||
WEBHOOK_PATH,
|
||||
data=payload_bytes,
|
||||
headers={SIG_HEADER: sig, "Content-Type": "application/json"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_null_data_does_not_crash(self, client, db):
|
||||
"""Guard against data being null in the event payload."""
|
||||
payload = {
|
||||
"event_type": "subscription.updated",
|
||||
"data": None,
|
||||
}
|
||||
payload_bytes = json.dumps(payload).encode()
|
||||
sig = sign_payload(payload_bytes)
|
||||
|
||||
response = await client.post(
|
||||
WEBHOOK_PATH,
|
||||
data=payload_bytes,
|
||||
headers={SIG_HEADER: sig, "Content-Type": "application/json"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# Subscription Lifecycle Events
|
||||
|
||||
Reference in New Issue
Block a user