Skip to content

Commit 81b1fc9

Browse files
authored
feat: update create billing portal session api (#860)
1 parent e87874e commit 81b1fc9

File tree

3 files changed

+474
-21
lines changed

3 files changed

+474
-21
lines changed
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
"""
2+
Tests for customer billing API endpoints.
3+
"""
4+
import uuid
5+
from datetime import timedelta
6+
from unittest import mock
7+
8+
import stripe
9+
from django.urls import reverse
10+
from django.utils import timezone
11+
from rest_framework import status
12+
13+
from enterprise_access.apps.core.constants import SYSTEM_ENTERPRISE_ADMIN_ROLE, SYSTEM_ENTERPRISE_LEARNER_ROLE
14+
from enterprise_access.apps.core.tests.factories import UserFactory
15+
from enterprise_access.apps.customer_billing.constants import CheckoutIntentState
16+
from enterprise_access.apps.customer_billing.models import CheckoutIntent
17+
from test_utils import APITest
18+
19+
20+
class CustomerBillingPortalSessionTests(APITest):
21+
"""
22+
Tests for CustomerBillingPortalSession endpoints.
23+
"""
24+
25+
def setUp(self):
26+
super().setUp()
27+
self.enterprise_uuid = str(uuid.uuid4())
28+
self.stripe_customer_id = 'cus_test_123'
29+
30+
# Create a checkout intent for testing
31+
self.checkout_intent = CheckoutIntent.objects.create(
32+
user=self.user,
33+
enterprise_uuid=self.enterprise_uuid,
34+
enterprise_name='Test Enterprise',
35+
enterprise_slug='test-enterprise',
36+
stripe_customer_id=self.stripe_customer_id,
37+
state=CheckoutIntentState.PAID,
38+
quantity=10,
39+
expires_at=timezone.now() + timedelta(hours=1),
40+
)
41+
42+
def tearDown(self):
43+
CheckoutIntent.objects.all().delete()
44+
super().tearDown()
45+
46+
def test_create_enterprise_admin_portal_session_success(self):
47+
"""
48+
Successful creation of enterprise admin portal session.
49+
"""
50+
self.set_jwt_cookie([{
51+
'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE,
52+
'context': self.enterprise_uuid, # implicit access to this enterprise
53+
}])
54+
55+
url = reverse('api:v1:customer-billing-create-enterprise-admin-portal-session')
56+
57+
mock_session = {
58+
'id': 'bps_test_123',
59+
'url': 'https://billing.stripe.com/session/test_123',
60+
'customer': self.stripe_customer_id,
61+
}
62+
63+
with mock.patch('stripe.billing_portal.Session.create') as mock_create:
64+
mock_create.return_value = mock_session
65+
66+
response = self.client.get(
67+
url,
68+
{'enterprise_customer_uuid': self.enterprise_uuid},
69+
HTTP_ORIGIN='https://admin.example.com'
70+
)
71+
72+
self.assertEqual(response.status_code, status.HTTP_200_OK)
73+
self.assertEqual(response.data, mock_session)
74+
75+
# Implementation uses /{enterprise_slug} for Admin portal return URL.
76+
mock_create.assert_called_once_with(
77+
customer=self.stripe_customer_id,
78+
return_url='https://admin.example.com/test-enterprise',
79+
)
80+
81+
def test_create_enterprise_admin_portal_session_missing_uuid(self):
82+
"""
83+
Without enterprise_customer_uuid, RBAC blocks at the decorator -> 403.
84+
"""
85+
self.set_jwt_cookie([{
86+
'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE,
87+
'context': self.enterprise_uuid,
88+
}])
89+
90+
url = reverse('api:v1:customer-billing-create-enterprise-admin-portal-session')
91+
92+
response = self.client.get(url)
93+
94+
# Permission layer rejects because fn(...) yields None context.
95+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
96+
97+
def test_create_enterprise_admin_portal_session_no_checkout_intent(self):
98+
"""
99+
RBAC passes (user has implicit access to provided UUID), view returns 404 when no intent exists.
100+
"""
101+
non_existent_uuid = str(uuid.uuid4())
102+
self.set_jwt_cookie([{
103+
'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE,
104+
'context': non_existent_uuid,
105+
}])
106+
107+
url = reverse('api:v1:customer-billing-create-enterprise-admin-portal-session')
108+
109+
response = self.client.get(
110+
url,
111+
{'enterprise_customer_uuid': non_existent_uuid}
112+
)
113+
114+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
115+
116+
def test_create_enterprise_admin_portal_session_no_stripe_customer(self):
117+
"""
118+
If the CheckoutIntent has no Stripe customer ID, Stripe call will error → 422.
119+
"""
120+
other_user = UserFactory()
121+
checkout_intent_no_stripe = CheckoutIntent.objects.create(
122+
user=other_user,
123+
enterprise_uuid=str(uuid.uuid4()),
124+
enterprise_name='Test Enterprise 2',
125+
enterprise_slug='test-enterprise-2',
126+
stripe_customer_id=None,
127+
state=CheckoutIntentState.CREATED,
128+
quantity=5,
129+
expires_at=timezone.now() + timedelta(hours=1),
130+
)
131+
132+
self.set_jwt_cookie([{
133+
'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE,
134+
'context': checkout_intent_no_stripe.enterprise_uuid,
135+
}])
136+
137+
url = reverse('api:v1:customer-billing-create-enterprise-admin-portal-session')
138+
139+
with mock.patch('stripe.billing_portal.Session.create') as mock_create:
140+
mock_create.side_effect = stripe.error.InvalidRequestError(
141+
'Customer does not exist',
142+
'customer'
143+
)
144+
response = self.client.get(
145+
url,
146+
{'enterprise_customer_uuid': checkout_intent_no_stripe.enterprise_uuid},
147+
HTTP_ORIGIN='https://admin.example.com'
148+
)
149+
150+
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
151+
152+
def test_create_enterprise_admin_portal_session_stripe_error(self):
153+
"""
154+
Stripe API returns an error → 422.
155+
"""
156+
self.set_jwt_cookie([{
157+
'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE,
158+
'context': self.enterprise_uuid,
159+
}])
160+
161+
url = reverse('api:v1:customer-billing-create-enterprise-admin-portal-session')
162+
163+
with mock.patch('stripe.billing_portal.Session.create') as mock_create:
164+
mock_create.side_effect = stripe.error.InvalidRequestError(
165+
'Customer does not exist',
166+
'customer'
167+
)
168+
169+
response = self.client.get(
170+
url,
171+
{'enterprise_customer_uuid': self.enterprise_uuid},
172+
HTTP_ORIGIN='https://admin.example.com'
173+
)
174+
175+
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
176+
177+
def test_create_enterprise_admin_portal_session_authentication_required(self):
178+
"""
179+
Authentication required for enterprise admin portal session.
180+
"""
181+
url = reverse('api:v1:customer-billing-create-enterprise-admin-portal-session')
182+
183+
response = self.client.get(
184+
url,
185+
{'enterprise_customer_uuid': self.enterprise_uuid}
186+
)
187+
188+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
189+
190+
def test_create_enterprise_admin_portal_session_permission_required(self):
191+
"""
192+
User with learner role only should be forbidden by RBAC.
193+
"""
194+
self.set_jwt_cookie([{
195+
'system_wide_role': SYSTEM_ENTERPRISE_LEARNER_ROLE,
196+
'context': self.enterprise_uuid,
197+
}])
198+
199+
url = reverse('api:v1:customer-billing-create-enterprise-admin-portal-session')
200+
201+
response = self.client.get(
202+
url,
203+
{'enterprise_customer_uuid': self.enterprise_uuid}
204+
)
205+
206+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
207+
208+
def test_create_checkout_portal_session_success(self):
209+
"""
210+
Successful creation of checkout portal session.
211+
"""
212+
self.set_jwt_cookie([{
213+
'system_wide_role': SYSTEM_ENTERPRISE_LEARNER_ROLE,
214+
'context': str(uuid.uuid4()),
215+
}])
216+
217+
url = reverse('api:v1:customer-billing-create-checkout-portal-session',
218+
kwargs={'pk': self.checkout_intent.id})
219+
220+
mock_session = {
221+
'id': 'bps_test_456',
222+
'url': 'https://billing.stripe.com/session/test_456',
223+
'customer': self.stripe_customer_id,
224+
}
225+
226+
with mock.patch('stripe.billing_portal.Session.create') as mock_create:
227+
mock_create.return_value = mock_session
228+
229+
response = self.client.get(
230+
url,
231+
HTTP_ORIGIN='https://checkout.example.com'
232+
)
233+
234+
self.assertEqual(response.status_code, status.HTTP_200_OK)
235+
self.assertEqual(response.data, mock_session)
236+
237+
mock_create.assert_called_once_with(
238+
customer=self.stripe_customer_id,
239+
return_url='https://checkout.example.com/billing-details/success',
240+
)
241+
242+
def test_create_checkout_portal_session_wrong_user(self):
243+
"""
244+
Wrong user (permission class denies) → 403.
245+
"""
246+
other_user = UserFactory()
247+
self.set_jwt_cookie([{
248+
'system_wide_role': SYSTEM_ENTERPRISE_LEARNER_ROLE,
249+
'context': str(uuid.uuid4()),
250+
}], user=other_user)
251+
252+
url = reverse('api:v1:customer-billing-create-checkout-portal-session',
253+
kwargs={'pk': self.checkout_intent.id})
254+
255+
response = self.client.get(url)
256+
257+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
258+
259+
def test_create_checkout_portal_session_nonexistent_intent(self):
260+
"""
261+
Permission class denies before view (no intent for pk) → 403.
262+
"""
263+
self.set_jwt_cookie([{
264+
'system_wide_role': SYSTEM_ENTERPRISE_LEARNER_ROLE,
265+
'context': str(uuid.uuid4()),
266+
}])
267+
268+
url = reverse('api:v1:customer-billing-create-checkout-portal-session',
269+
kwargs={'pk': 99999})
270+
271+
response = self.client.get(url)
272+
273+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
274+
275+
def test_create_checkout_portal_session_no_stripe_customer(self):
276+
"""
277+
No Stripe customer on the CheckoutIntent → 404 (from view).
278+
"""
279+
other_user = UserFactory()
280+
checkout_intent_no_stripe = CheckoutIntent.objects.create(
281+
user=other_user,
282+
enterprise_uuid=str(uuid.uuid4()),
283+
enterprise_name='Test Enterprise 3',
284+
enterprise_slug='test-enterprise-3',
285+
stripe_customer_id=None,
286+
state=CheckoutIntentState.CREATED,
287+
quantity=5,
288+
expires_at=timezone.now() + timedelta(hours=1),
289+
)
290+
291+
self.set_jwt_cookie([{
292+
'system_wide_role': SYSTEM_ENTERPRISE_LEARNER_ROLE,
293+
'context': str(uuid.uuid4()),
294+
}], user=other_user)
295+
296+
url = reverse('api:v1:customer-billing-create-checkout-portal-session',
297+
kwargs={'pk': checkout_intent_no_stripe.id})
298+
299+
response = self.client.get(url)
300+
301+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
302+
303+
def test_create_checkout_portal_session_stripe_error(self):
304+
"""
305+
Stripe API error → 422.
306+
"""
307+
self.set_jwt_cookie([{
308+
'system_wide_role': SYSTEM_ENTERPRISE_LEARNER_ROLE,
309+
'context': str(uuid.uuid4()),
310+
}])
311+
312+
url = reverse('api:v1:customer-billing-create-checkout-portal-session',
313+
kwargs={'pk': self.checkout_intent.id})
314+
315+
with mock.patch('stripe.billing_portal.Session.create') as mock_create:
316+
mock_create.side_effect = stripe.error.AuthenticationError('Invalid API key')
317+
318+
response = self.client.get(
319+
url,
320+
HTTP_ORIGIN='https://checkout.example.com'
321+
)
322+
323+
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
324+
325+
def test_create_checkout_portal_session_authentication_required(self):
326+
"""
327+
Authentication required for checkout portal session.
328+
"""
329+
url = reverse('api:v1:customer-billing-create-checkout-portal-session',
330+
kwargs={'pk': self.checkout_intent.id})
331+
332+
response = self.client.get(url)
333+
334+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

0 commit comments

Comments
 (0)