diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index a55bfa86e3..4096363be3 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -25,6 +25,7 @@ from corporate.lib.stripe import ( DEFAULT_INVOICE_DAYS_UNTIL_DUE, MAX_INVOICED_LICENSES, MIN_INVOICED_LICENSES, + STRIPE_API_VERSION, BillingError, InvalidBillingSchedule, InvalidTier, @@ -496,6 +497,7 @@ class StripeTestCase(ZulipTestCase): "object": "event", "data": {"object": stripe_session_dict}, "type": "checkout.session.completed", + "api_version": STRIPE_API_VERSION, } response = self.client_post( @@ -3846,9 +3848,27 @@ class StripeWebhookEndpointTest(ZulipTestCase): ) self.assertEqual(result.status_code, 400) + def test_stripe_webhook_endpoint_invalid_api_version(self) -> None: + event_data = { + "id": "stripe_event_id", + "api_version": "1991-02-20", + "type": "event_type", + "data": {"object": {"object": "checkout.session", "id": "stripe_session_id"}}, + } + + expected_error_message = fr"Mismatch between billing system Stripe API version({STRIPE_API_VERSION}) and Stripe webhook event API version(1991-02-20)." + with self.assertLogs("corporate.stripe", "ERROR") as error_log: + self.client_post( + "/stripe/webhook/", + event_data, + content_type="application/json", + ) + self.assertEqual(error_log.output, [f"ERROR:corporate.stripe:{expected_error_message}"]) + def test_stripe_webhook_for_session_completed_event(self) -> None: valid_session_event_data = { "id": "stripe_event_id", + "api_version": STRIPE_API_VERSION, "type": "checkout.session.completed", "data": {"object": {"object": "checkout.session", "id": "stripe_session_id"}}, } @@ -3906,6 +3926,7 @@ class StripeWebhookEndpointTest(ZulipTestCase): valid_session_event_data = { "id": stripe_event_id, "type": event_type, + "api_version": STRIPE_API_VERSION, "data": {"object": {"object": "payment_intent", "id": stripe_payment_intent_id}}, } diff --git a/corporate/views/webhook.py b/corporate/views/webhook.py index 1eed6e1039..d1a098d8ee 100644 --- a/corporate/views/webhook.py +++ b/corporate/views/webhook.py @@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType from django.http import HttpRequest, HttpResponse from django.views.decorators.csrf import csrf_exempt +from corporate.lib.stripe import STRIPE_API_VERSION from corporate.lib.stripe_event_handler import ( handle_checkout_session_completed_event, handle_payment_intent_payment_failed_event, @@ -42,6 +43,11 @@ def stripe_webhook(request: HttpRequest) -> HttpResponse: except Exception: return HttpResponse(status=400) + if stripe_event.api_version != STRIPE_API_VERSION: + error_message = f"Mismatch between billing system Stripe API version({STRIPE_API_VERSION}) and Stripe webhook event API version({stripe_event.api_version})." + billing_logger.error(error_message) + return HttpResponse(status=400) + if stripe_event.type not in [ "checkout.session.completed", "payment_intent.succeeded", diff --git a/stubs/stripe/__init__.pyi b/stubs/stripe/__init__.pyi index bc5b2704a1..625ac04659 100644 --- a/stubs/stripe/__init__.pyi +++ b/stubs/stripe/__init__.pyi @@ -320,6 +320,7 @@ class EventData: class Event: id: str + api_version: str type: EventTypes data: EventData @staticmethod