Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 71284aa

Browse files
This PR has been updated to add tests for #1253
### Commits: - Add tests for invoice payment succeeded functionality This commit adds comprehensive unit tests for the invoice_payment_succeeded method in the StripeWebhookHandler class, covering: - Setting delinquent status to false when payment succeeds - Sending notification emails to owners and admins - Handling cases with no matching owners - Handling cases with multiple owners sharing same subscription These tests ensure that the invoice payment success handling works correctly after recent modifications. - Add tests for invoice payment failure functionality This commit adds comprehensive unit tests for the invoice_payment_failed method in the StripeWebhookHandler class, covering: - Skipping delinquency for payments requiring microdeposit verification - Setting delinquent status to true for normal payment failures - Sending notification emails to owners and admin users These tests ensure that the recent changes to invoice payment failure handling work correctly. - Add tests for Stripe webhook request handling This commit adds unit tests for the webhook handling functionality in StripeWebhookHandler, including: - Validation of webhook signatures - Handling of supported and unsupported event types - Error handling for missing configuration These tests ensure that the webhook endpoint properly validates and routes Stripe events to the appropriate handler methods.
1 parent e770760 commit 71284aa

File tree

3 files changed

+439
-0
lines changed

3 files changed

+439
-0
lines changed

billing/tests/test_invoice.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import unittest
2+
from datetime import datetime
3+
from unittest.mock import Mock, call, patch
4+
5+
import stripe
6+
from django.test import TestCase
7+
from rest_framework.test import APITestCase
8+
from shared.django_apps.core.tests.factories import OwnerFactory
9+
10+
from billing.views import StripeWebhookHandler
11+
12+
13+
class MockCard(object):
14+
def __init__(self):
15+
self.brand = "visa"
16+
self.last4 = "1234"
17+
18+
def __getitem__(self, key):
19+
return getattr(self, key)
20+
21+
22+
class MockPaymentMethod(object):
23+
def __init__(self, noCard=False):
24+
if noCard:
25+
self.card = None
26+
return
27+
28+
self.card = MockCard()
29+
30+
def __getitem__(self, key):
31+
return getattr(self, key)
32+
33+
34+
class MockPaymentIntent(object):
35+
def __init__(self, noCard=False):
36+
self.payment_method = MockPaymentMethod(noCard)
37+
self.status = "succeeded"
38+
39+
def __getitem__(self, key):
40+
return getattr(self, key)
41+
42+
def get(self, key, default=None):
43+
return getattr(self, key, default)
44+
45+
46+
class InvoiceTests(TestCase):
47+
def setUp(self):
48+
self.owner = OwnerFactory(
49+
stripe_customer_id="cus_123",
50+
stripe_subscription_id="sub_123",
51+
delinquent=True,
52+
)
53+
self.handler = StripeWebhookHandler()
54+
55+
@patch("services.task.TaskService.send_email")
56+
def test_invoice_payment_succeeded_sets_delinquent_false(self, mocked_send_email):
57+
"""Test that invoice_payment_succeeded correctly updates delinquent status"""
58+
# Create a mock invoice object
59+
mock_invoice = Mock()
60+
mock_invoice.customer = self.owner.stripe_customer_id
61+
mock_invoice.subscription = self.owner.stripe_subscription_id
62+
mock_invoice.total = 24000 # $240.00
63+
mock_invoice.hosted_invoice_url = "https://stripe.com/invoice"
64+
65+
# Call the method being tested
66+
self.handler.invoice_payment_succeeded(mock_invoice)
67+
68+
# Verify owner was updated
69+
self.owner.refresh_from_db()
70+
self.assertFalse(self.owner.delinquent)
71+
72+
# Verify email was sent to owner
73+
mocked_send_email.assert_called_once_with(
74+
to_addr=self.owner.email,
75+
subject="You're all set",
76+
template_name="success-after-failed-payment",
77+
amount=240,
78+
cta_link="https://stripe.com/invoice",
79+
date=datetime.now().strftime("%B %-d, %Y"),
80+
)
81+
82+
@patch("services.task.TaskService.send_email")
83+
def test_invoice_payment_succeeded_with_multiple_admins(self, mocked_send_email):
84+
"""Test invoice_payment_succeeded sends emails to all admins"""
85+
# Create admin owners
86+
admin_1 = OwnerFactory(email="[email protected]")
87+
admin_2 = OwnerFactory(email="[email protected]")
88+
89+
# Set admins for the owner
90+
self.owner.admins = [admin_1.ownerid, admin_2.ownerid]
91+
self.owner.save()
92+
93+
# Create a mock invoice object
94+
mock_invoice = Mock()
95+
mock_invoice.customer = self.owner.stripe_customer_id
96+
mock_invoice.subscription = self.owner.stripe_subscription_id
97+
mock_invoice.total = 24000 # $240.00
98+
mock_invoice.hosted_invoice_url = "https://stripe.com/invoice"
99+
100+
# Call the method being tested
101+
self.handler.invoice_payment_succeeded(mock_invoice)
102+
103+
# Verify owner was updated
104+
self.owner.refresh_from_db()
105+
self.assertFalse(self.owner.delinquent)
106+
107+
# Verify emails were sent to owner and admins
108+
expected_calls = [
109+
call(
110+
to_addr=self.owner.email,
111+
subject="You're all set",
112+
template_name="success-after-failed-payment",
113+
amount=240,
114+
cta_link="https://stripe.com/invoice",
115+
date=datetime.now().strftime("%B %-d, %Y"),
116+
),
117+
call(
118+
to_addr=admin_1.email,
119+
subject="You're all set",
120+
template_name="success-after-failed-payment",
121+
amount=240,
122+
cta_link="https://stripe.com/invoice",
123+
date=datetime.now().strftime("%B %-d, %Y"),
124+
),
125+
call(
126+
to_addr=admin_2.email,
127+
subject="You're all set",
128+
template_name="success-after-failed-payment",
129+
amount=240,
130+
cta_link="https://stripe.com/invoice",
131+
date=datetime.now().strftime("%B %-d, %Y"),
132+
),
133+
]
134+
mocked_send_email.assert_has_calls(expected_calls, any_order=True)
135+
136+
def test_invoice_payment_succeeded_no_matching_owners(self):
137+
"""Test handling when no matching owners are found"""
138+
# Create a mock invoice with non-matching customer/subscription
139+
mock_invoice = Mock()
140+
mock_invoice.customer = "cus_nonexistent"
141+
mock_invoice.subscription = "sub_nonexistent"
142+
143+
# This should run without error, just return without action
144+
self.handler.invoice_payment_succeeded(mock_invoice)
145+
146+
# Verify owner was not updated
147+
self.owner.refresh_from_db()
148+
self.assertTrue(self.owner.delinquent)
149+
150+
def test_invoice_payment_succeeded_multiple_owners(self):
151+
"""Test handling when multiple owners match the customer/subscription"""
152+
# Create another owner with the same customer/subscription
153+
other_owner = OwnerFactory(
154+
stripe_customer_id=self.owner.stripe_customer_id,
155+
stripe_subscription_id=self.owner.stripe_subscription_id,
156+
delinquent=True,
157+
)
158+
159+
# Create a mock invoice object
160+
mock_invoice = Mock()
161+
mock_invoice.customer = self.owner.stripe_customer_id
162+
mock_invoice.subscription = self.owner.stripe_subscription_id
163+
mock_invoice.total = 24000
164+
mock_invoice.hosted_invoice_url = "https://stripe.com/invoice"
165+
166+
# Call the method being tested
167+
self.handler.invoice_payment_succeeded(mock_invoice)
168+
169+
# Verify both owners were updated
170+
self.owner.refresh_from_db()
171+
other_owner.refresh_from_db()
172+
self.assertFalse(self.owner.delinquent)
173+
self.assertFalse(other_owner.delinquent)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import unittest
2+
from datetime import datetime
3+
from unittest.mock import Mock, call, patch
4+
5+
import stripe
6+
from django.test import TestCase
7+
from rest_framework.test import APITestCase
8+
from shared.django_apps.core.tests.factories import OwnerFactory
9+
10+
from billing.views import StripeWebhookHandler
11+
12+
13+
class MockCard(object):
14+
def __init__(self):
15+
self.brand = "visa"
16+
self.last4 = "1234"
17+
18+
def __getitem__(self, key):
19+
return getattr(self, key)
20+
21+
22+
class MockPaymentMethod(object):
23+
def __init__(self, noCard=False):
24+
if noCard:
25+
self.card = None
26+
return
27+
28+
self.card = MockCard()
29+
30+
def __getitem__(self, key):
31+
return getattr(self, key)
32+
33+
34+
class MockPaymentIntent(object):
35+
def __init__(self, noCard=False, status="succeeded", next_action=None):
36+
self.payment_method = MockPaymentMethod(noCard)
37+
self.status = status
38+
self.next_action = next_action or {}
39+
40+
def __getitem__(self, key):
41+
return getattr(self, key)
42+
43+
def get(self, key, default=None):
44+
return getattr(self, key, default)
45+
46+
47+
class InvoiceFailureTests(TestCase):
48+
def setUp(self):
49+
self.owner = OwnerFactory(
50+
stripe_customer_id="cus_123",
51+
stripe_subscription_id="sub_123",
52+
delinquent=False,
53+
)
54+
self.handler = StripeWebhookHandler()
55+
56+
@patch("stripe.PaymentIntent.retrieve")
57+
def test_invoice_payment_failed_skips_delinquency_for_microdeposit_verification(self, mock_retrieve):
58+
"""Test that delinquency is skipped when waiting for microdeposit verification"""
59+
# Set up the payment intent to indicate microdeposit verification
60+
mock_retrieve.return_value = MockPaymentIntent(
61+
status="requires_action",
62+
next_action={"type": "verify_with_microdeposits"}
63+
)
64+
65+
# Create a mock invoice
66+
mock_invoice = Mock()
67+
mock_invoice.customer = self.owner.stripe_customer_id
68+
mock_invoice.subscription = self.owner.stripe_subscription_id
69+
mock_invoice.default_payment_method = None
70+
mock_invoice.payment_intent = "pi_123"
71+
72+
# Call the method being tested
73+
self.handler.invoice_payment_failed(mock_invoice)
74+
75+
# Verify owner delinquent status was not changed
76+
self.owner.refresh_from_db()
77+
self.assertFalse(self.owner.delinquent)
78+
79+
# Verify payment intent was retrieved
80+
mock_retrieve.assert_called_once_with("pi_123")
81+
82+
@patch("stripe.PaymentIntent.retrieve")
83+
@patch("services.task.TaskService.send_email")
84+
def test_invoice_payment_failed_sets_delinquent_true(self, mock_send_email, mock_retrieve):
85+
"""Test that invoice_payment_failed sets delinquent status to True"""
86+
# Setup payment intent with a normal failure (not microdeposits)
87+
mock_retrieve.return_value = MockPaymentIntent()
88+
89+
# Create a mock invoice
90+
mock_invoice = Mock()
91+
mock_invoice.customer = self.owner.stripe_customer_id
92+
mock_invoice.subscription = self.owner.stripe_subscription_id
93+
mock_invoice.total = 24000
94+
mock_invoice.hosted_invoice_url = "https://stripe.com/invoice"
95+
mock_invoice.default_payment_method = {}
96+
mock_invoice.payment_intent = "pi_123"
97+
mock_invoice.__getitem__ = lambda self, key: getattr(self, key)
98+
99+
# Call the method being tested
100+
self.handler.invoice_payment_failed(mock_invoice)
101+
102+
# Verify owner delinquent status was changed
103+
self.owner.refresh_from_db()
104+
self.assertTrue(self.owner.delinquent)
105+
106+
# Verify email was sent to owner
107+
mock_send_email.assert_called_with(
108+
to_addr=self.owner.email,
109+
subject="Your Codecov payment failed",
110+
template_name="failed-payment",
111+
name=self.owner.username,
112+
amount=240,
113+
card_type="visa",
114+
last_four="1234",
115+
cta_link="https://stripe.com/invoice",
116+
date=datetime.now().strftime("%B %-d, %Y"),
117+
)
118+
119+
@patch("stripe.PaymentIntent.retrieve")
120+
@patch("services.task.TaskService.send_email")
121+
def test_invoice_payment_failed_with_multiple_admins(self, mock_send_email, mock_retrieve):
122+
"""Test that invoice_payment_failed sends emails to all admins"""
123+
# Create admin owners
124+
admin_1 = OwnerFactory(email="[email protected]", username="admin1")
125+
admin_2 = OwnerFactory(email="[email protected]", username="admin2")
126+
127+
# Set admins for the owner
128+
self.owner.admins = [admin_1.ownerid, admin_2.ownerid]
129+
self.owner.save()
130+
131+
# Setup payment intent with normal failure
132+
mock_retrieve.return_value = MockPaymentIntent()
133+
134+
# Create a mock invoice
135+
mock_invoice = Mock()
136+
mock_invoice.customer = self.owner.stripe_customer_id
137+
mock_invoice.subscription = self.owner.stripe_subscription_id
138+
mock_invoice.total = 24000
139+
mock_invoice.hosted_invoice_url = "https://stripe.com/invoice"
140+
mock_invoice.default_payment_method = {}
141+
mock_invoice.payment_intent = "pi_123"
142+
mock_invoice.__getitem__ = lambda self, key: getattr(self, key)
143+
144+
# Call the method being tested
145+
self.handler.invoice_payment_failed(mock_invoice)
146+
147+
# Verify owner delinquent status was changed
148+
self.owner.refresh_from_db()
149+
self.assertTrue(self.owner.delinquent)
150+
151+
# Verify emails were sent to owner and all admins
152+
self.assertEqual(mock_send_email.call_count, 3)
153+
mock_send_email.assert_any_call(
154+
to_addr=admin_1.email,
155+
subject="Your Codecov payment failed",
156+
template_name="failed-payment",
157+
name=admin_1.username,
158+
amount=240,
159+
card_type="visa",
160+
last_four="1234",
161+
cta_link="https://stripe.com/invoice",
162+
date=datetime.now().strftime("%B %-d, %Y"),
163+
)

0 commit comments

Comments
 (0)