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

Commit e770760

Browse files
This PR has been updated to add tests for #1253
### Commits: - Add unit tests for modified billing webhook payment handlers This commit adds tests for the streamlined invoice payment handling in the billing/views.py file: - Tests for invoice_payment_succeeded function including delinquent status updates and email notifications - Tests for invoice_payment_failed function including conditional delinquent status handling - Tests for the payment intent verification flow These tests ensure proper coverage of the modified payment handling logic in the billing webhook handler. - Add unit tests for subscription handling in billing webhook This commit adds tests for the subscription handling functionality in billing/views.py: - Tests for customer_subscription_deleted including plan reset and repository deactivation - Tests for customer_subscription_created including ID setting and plan updates - Tests for the payment verification method used with asynchronous payment methods - Tests for handling repositories when subscriptions change These tests ensure the subscription lifecycle is properly handled in the Stripe webhook handler. - Add unit tests for payment method handling in webhook handler This commit adds tests for the payment method handling functionality in billing/views.py: - Tests for delayed notification handling with bank account verification - Tests for payment intent success handling - Tests for payment method attachment and default setting These tests ensure payment methods are properly handled for asynchronous payment flows.
1 parent 44a2c86 commit e770760

File tree

3 files changed

+388
-0
lines changed

3 files changed

+388
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import unittest
2+
from unittest.mock import Mock, patch
3+
4+
from django.test import TestCase
5+
6+
from billing.views import StripeWebhookHandler
7+
from codecov_auth.tests.factories import OwnerFactory
8+
9+
10+
class TestInvoicePaymentSucceeded(TestCase):
11+
def setUp(self):
12+
self.handler = StripeWebhookHandler()
13+
self.owner = OwnerFactory(
14+
stripe_customer_id="cus_123",
15+
stripe_subscription_id="sub_123",
16+
)
17+
18+
@patch("billing.views.Owner.objects.filter")
19+
def test_invoice_payment_succeeded_updates_delinquent_status(self, mock_filter):
20+
mock_owners = Mock()
21+
mock_filter.return_value = mock_owners
22+
23+
# Create a mock invoice object
24+
mock_invoice = Mock()
25+
mock_invoice.customer = self.owner.stripe_customer_id
26+
mock_invoice.subscription = self.owner.stripe_subscription_id
27+
mock_invoice.total = 24000
28+
mock_invoice.hosted_invoice_url = "https://stripe.com"
29+
30+
# Call the handler method
31+
self.handler.invoice_payment_succeeded(mock_invoice)
32+
33+
# Verify the owners were filtered and updated correctly
34+
mock_filter.assert_called_once_with(
35+
stripe_customer_id=self.owner.stripe_customer_id,
36+
)
37+
mock_owners.update.assert_called_once_with(delinquent=False)
38+
39+
@patch("billing.views.Owner.objects.filter")
40+
@patch("billing.views.TaskService")
41+
def test_invoice_payment_succeeded_sends_email_to_delinquent_owners(
42+
self, mock_task_service, mock_filter
43+
):
44+
# Setup mock owners with delinquent status
45+
mock_owners = Mock()
46+
mock_owners_list = [self.owner]
47+
mock_filter.return_value = mock_owners
48+
mock_owners.filter.return_value = mock_owners
49+
mock_owners.__iter__.return_value = mock_owners_list
50+
51+
# Setup delinquent status
52+
self.owner.delinquent = True
53+
self.owner.email = "[email protected]"
54+
55+
# Setup task service mock
56+
mock_task_service_instance = Mock()
57+
mock_task_service.return_value = mock_task_service_instance
58+
59+
# Create a mock invoice object
60+
mock_invoice = Mock()
61+
mock_invoice.customer = self.owner.stripe_customer_id
62+
mock_invoice.subscription = self.owner.stripe_subscription_id
63+
mock_invoice.total = 24000
64+
mock_invoice.hosted_invoice_url = "https://stripe.com"
65+
66+
# Call the handler method
67+
self.handler.invoice_payment_succeeded(mock_invoice)
68+
69+
# Verify email was sent to delinquent owner
70+
mock_task_service_instance.send_email.assert_called_with(
71+
to_addr=self.owner.email,
72+
subject="You're all set",
73+
template_name="success-after-failed-payment",
74+
amount=240,
75+
cta_link="https://stripe.com",
76+
date=unittest.mock.ANY,
77+
)
78+
79+
80+
class TestInvoicePaymentFailed(TestCase):
81+
def setUp(self):
82+
self.handler = StripeWebhookHandler()
83+
self.owner = OwnerFactory(
84+
stripe_customer_id="cus_123",
85+
stripe_subscription_id="sub_123",
86+
delinquent=False
87+
)
88+
89+
@patch("billing.views.stripe.PaymentIntent.retrieve")
90+
@patch("billing.views.Owner.objects.filter")
91+
def test_invoice_payment_failed_with_payment_method_sets_delinquent(
92+
self, mock_filter, mock_retrieve_payment_intent
93+
):
94+
# Setup mock owners
95+
mock_owners = Mock()
96+
mock_filter.return_value = mock_owners
97+
98+
# Create a mock invoice object with payment method
99+
mock_invoice = Mock()
100+
mock_invoice.customer = self.owner.stripe_customer_id
101+
mock_invoice.subscription = self.owner.stripe_subscription_id
102+
mock_invoice.total = 24000
103+
mock_invoice.hosted_invoice_url = "https://stripe.com"
104+
mock_invoice.payment_intent = "pi_123"
105+
mock_invoice.default_payment_method = {} # Not None
106+
107+
# Call the handler method
108+
self.handler.invoice_payment_failed(mock_invoice)
109+
110+
# Verify owners were set to delinquent
111+
mock_owners.update.assert_called_once_with(delinquent=True)
112+
113+
@patch("billing.views.stripe.PaymentIntent.retrieve")
114+
@patch("billing.views.Owner.objects.filter")
115+
def test_invoice_payment_failed_skips_delinquency_for_requires_action(
116+
self, mock_filter, mock_retrieve_payment_intent
117+
):
118+
# Setup payment intent that requires action
119+
mock_payment_intent = Mock()
120+
mock_payment_intent.status = "requires_action"
121+
mock_payment_intent.next_action = {"type": "verify_with_microdeposits"}
122+
mock_retrieve_payment_intent.return_value = mock_payment_intent
123+
124+
# Create a mock invoice object with no payment method
125+
mock_invoice = Mock()
126+
mock_invoice.payment_intent = "pi_123"
127+
mock_invoice.default_payment_method = None
128+
129+
# No delinquent status should be set for this case
130+
self.handler.invoice_payment_failed(mock_invoice)
131+
mock_filter.assert_not_called()
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from unittest.mock import Mock, patch
2+
3+
from django.test import TestCase
4+
5+
from billing.views import StripeWebhookHandler
6+
from codecov_auth.tests.factories import OwnerFactory
7+
8+
9+
class TestPaymentMethodHandlers(TestCase):
10+
def setUp(self):
11+
self.handler = StripeWebhookHandler()
12+
self.owner = OwnerFactory(
13+
stripe_customer_id="cus_123",
14+
stripe_subscription_id="sub_123",
15+
)
16+
17+
@patch("billing.views.stripe.PaymentMethod.attach")
18+
@patch("billing.views.stripe.Customer.modify")
19+
@patch("billing.views.stripe.Subscription.modify")
20+
@patch("billing.views.stripe.PaymentMethod.retrieve")
21+
@patch("billing.views.Owner.objects.filter")
22+
def test_check_and_handle_delayed_notification_payment_methods(
23+
self,
24+
mock_filter,
25+
mock_retrieve,
26+
mock_sub_modify,
27+
mock_customer_modify,
28+
mock_attach,
29+
):
30+
# Setup mock owners
31+
mock_owners = Mock()
32+
mock_filter.return_value = mock_owners
33+
mock_owners.values_list.return_value = [["sub_123"]]
34+
35+
# Setup mock payment method
36+
mock_payment_method = Mock()
37+
mock_payment_method.type = "us_bank_account"
38+
mock_payment_method.us_bank_account = {}
39+
mock_payment_method.id = "pm_123"
40+
mock_retrieve.return_value = mock_payment_method
41+
42+
# Call the handler method
43+
self.handler._check_and_handle_delayed_notification_payment_methods(
44+
"cus_123", "pm_123"
45+
)
46+
47+
# Verify payment method was attached to customer
48+
mock_attach.assert_called_once_with(mock_payment_method, customer="cus_123")
49+
50+
# Verify customer was updated with default payment method
51+
mock_customer_modify.assert_called_once_with(
52+
"cus_123",
53+
invoice_settings={"default_payment_method": mock_payment_method},
54+
)
55+
56+
# Verify subscription was updated with default payment method
57+
mock_sub_modify.assert_called_once_with(
58+
"sub_123",
59+
default_payment_method=mock_payment_method
60+
)
61+
62+
@patch("billing.views.logging.Logger.info")
63+
@patch("billing.views.StripeWebhookHandler._check_and_handle_delayed_notification_payment_methods")
64+
def test_payment_intent_succeeded(self, mock_check, mock_log_info):
65+
# Create mock payment intent
66+
mock_payment_intent = Mock()
67+
mock_payment_intent.id = "pi_123"
68+
mock_payment_intent.customer = "cus_123"
69+
mock_payment_intent.payment_method = "pm_123"
70+
71+
# Call the handler method
72+
self.handler.payment_intent_succeeded(mock_payment_intent)
73+
74+
# Verify delayed notification method was called
75+
mock_check.assert_called_once_with("cus_123", "pm_123")
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from unittest.mock import Mock, patch
2+
3+
from django.test import TestCase
4+
5+
from billing.constants import DEFAULT_FREE_PLAN
6+
from billing.models import Plan
7+
from billing.views import StripeWebhookHandler
8+
from codecov_auth.tests.factories import OwnerFactory
9+
from core.tests.factories import RepositoryFactory
10+
11+
12+
class TestSubscriptionHandlers(TestCase):
13+
def setUp(self):
14+
self.handler = StripeWebhookHandler()
15+
self.owner = OwnerFactory(
16+
stripe_customer_id="cus_123",
17+
stripe_subscription_id="sub_123",
18+
plan="users-pr-inappy",
19+
plan_user_count=10,
20+
plan_auto_activate=True
21+
)
22+
23+
@patch("billing.views.Owner.objects.filter")
24+
def test_customer_subscription_deleted_sets_free_plan(self, mock_filter):
25+
# Setup mock owners
26+
mock_owners = Mock()
27+
mock_filter.return_value = mock_owners
28+
29+
# Create mock subscription
30+
mock_subscription = Mock()
31+
mock_subscription.id = self.owner.stripe_subscription_id
32+
mock_subscription.customer = self.owner.stripe_customer_id
33+
mock_subscription.plan = {"name": self.owner.plan}
34+
mock_subscription.status = "active"
35+
36+
# Call the handler method
37+
self.handler.customer_subscription_deleted(mock_subscription)
38+
39+
# Verify owners were updated correctly
40+
mock_owners.update.assert_called_with(
41+
plan=DEFAULT_FREE_PLAN,
42+
plan_user_count=1,
43+
plan_activated_users=None,
44+
stripe_subscription_id=None
45+
)
46+
47+
@patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method")
48+
@patch("billing.views.Owner.objects.get")
49+
@patch("billing.views.PlanService")
50+
def test_customer_subscription_created_sets_subscription_ids(
51+
self, mock_plan_service, mock_get, mock_has_unverified
52+
):
53+
# Mock owner instance and plan service
54+
mock_owner = Mock()
55+
mock_get.return_value = mock_owner
56+
mock_plan_service_instance = Mock()
57+
mock_plan_service.return_value = mock_plan_service_instance
58+
mock_has_unverified.return_value = False
59+
60+
# Create mock subscription with metadata
61+
mock_subscription = Mock()
62+
mock_subscription.id = "new_sub_123"
63+
mock_subscription.customer = "new_cus_123"
64+
mock_subscription.plan.id = "plan_pro_yearly"
65+
mock_subscription.quantity = 5
66+
mock_subscription.metadata.get.return_value = self.owner.ownerid
67+
68+
# Call the handler method
69+
self.handler.customer_subscription_created(mock_subscription)
70+
71+
# Verify owner was retrieved and updated correctly
72+
mock_get.assert_called_with(ownerid=self.owner.ownerid)
73+
self.assertEqual(mock_owner.stripe_subscription_id, "new_sub_123")
74+
self.assertEqual(mock_owner.stripe_customer_id, "new_cus_123")
75+
mock_owner.save.assert_called_once()
76+
77+
# Verify plan was updated
78+
mock_plan_service.assert_called_with(current_org=mock_owner)
79+
mock_plan_service_instance.expire_trial_when_upgrading.assert_called_once()
80+
mock_plan_service_instance.update_plan.assert_called_once()
81+
82+
@patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method")
83+
@patch("billing.views.Owner.objects.get")
84+
@patch("billing.views.PlanService")
85+
def test_customer_subscription_created_early_returns_if_unverified(
86+
self, mock_plan_service, mock_get, mock_has_unverified
87+
):
88+
# Mock owner instance and unverified payment method
89+
mock_owner = Mock()
90+
mock_get.return_value = mock_owner
91+
mock_has_unverified.return_value = True
92+
93+
# Create mock subscription with metadata
94+
mock_subscription = Mock()
95+
mock_subscription.id = "new_sub_123"
96+
mock_subscription.customer = "new_cus_123"
97+
mock_subscription.plan.id = "plan_pro_yearly"
98+
mock_subscription.metadata.get.return_value = self.owner.ownerid
99+
100+
# Call the handler method
101+
self.handler.customer_subscription_created(mock_subscription)
102+
103+
# Verify owner was updated but plan was not
104+
mock_get.assert_called_with(ownerid=self.owner.ownerid)
105+
self.assertEqual(mock_owner.stripe_subscription_id, "new_sub_123")
106+
self.assertEqual(mock_owner.stripe_customer_id, "new_cus_123")
107+
mock_owner.save.assert_called_once()
108+
109+
# Verify plan service was not used to update plan
110+
mock_plan_service_instance = mock_plan_service.return_value
111+
mock_plan_service_instance.update_plan.assert_not_called()
112+
113+
@patch("billing.views.stripe.Invoice.retrieve")
114+
def test_has_unverified_initial_payment_method(self, mock_invoice_retrieve):
115+
# Mock subscription and payment intent requiring action
116+
mock_subscription = Mock()
117+
mock_subscription.latest_invoice = "inv_123"
118+
119+
mock_invoice = Mock()
120+
mock_invoice.payment_intent = "pi_123"
121+
mock_invoice_retrieve.return_value = mock_invoice
122+
123+
mock_payment_intent = Mock()
124+
mock_payment_intent.status = "requires_action"
125+
mock_payment_intent.next_action = {"type": "verify_with_microdeposits"}
126+
127+
with patch("billing.views.stripe.PaymentIntent.retrieve", return_value=mock_payment_intent):
128+
result = self.handler._has_unverified_initial_payment_method(mock_subscription)
129+
130+
self.assertTrue(result)
131+
mock_invoice_retrieve.assert_called_once_with("inv_123")
132+
133+
@patch("billing.views.stripe.Invoice.retrieve")
134+
def test_has_unverified_initial_payment_method_returns_false_when_succeeded(self, mock_invoice_retrieve):
135+
# Mock subscription and payment intent that succeeded
136+
mock_subscription = Mock()
137+
mock_subscription.latest_invoice = "inv_123"
138+
139+
mock_invoice = Mock()
140+
mock_invoice.payment_intent = "pi_123"
141+
mock_invoice_retrieve.return_value = mock_invoice
142+
143+
mock_payment_intent = Mock()
144+
mock_payment_intent.status = "succeeded"
145+
146+
with patch("billing.views.stripe.PaymentIntent.retrieve", return_value=mock_payment_intent):
147+
result = self.handler._has_unverified_initial_payment_method(mock_subscription)
148+
149+
self.assertFalse(result)
150+
mock_invoice_retrieve.assert_called_once_with("inv_123")
151+
152+
@patch("billing.views.Owner.objects.filter")
153+
@patch("billing.views.Repository.objects.filter")
154+
def test_customer_subscription_deleted_deactivates_repositories(
155+
self, mock_repo_filter, mock_owner_filter
156+
):
157+
# Create mock repositories
158+
mock_repos = Mock()
159+
mock_repo_filter.return_value = mock_repos
160+
161+
# Setup mock owners
162+
mock_owners = Mock()
163+
mock_owner_filter.return_value = mock_owners
164+
mock_owners.__iter__.return_value = [self.owner]
165+
166+
# Create test repositories
167+
RepositoryFactory(author=self.owner, activated=True, active=True)
168+
RepositoryFactory(author=self.owner, activated=True, active=True)
169+
170+
# Create mock subscription
171+
mock_subscription = Mock()
172+
mock_subscription.id = self.owner.stripe_subscription_id
173+
mock_subscription.customer = self.owner.stripe_customer_id
174+
mock_subscription.plan = {"name": self.owner.plan}
175+
mock_subscription.status = "active"
176+
177+
# Call the handler method
178+
self.handler.customer_subscription_deleted(mock_subscription)
179+
180+
# Verify repositories were deactivated
181+
mock_repo_filter.assert_called_with(author=self.owner)
182+
mock_repos.update.assert_called_with(activated=False, active=False)

0 commit comments

Comments
 (0)