Skip to content

Commit 3867154

Browse files
feat: Added coverage to patch
1 parent 442c4ef commit 3867154

1 file changed

Lines changed: 266 additions & 0 deletions

File tree

enterprise_access/apps/api/v1/tests/test_customer_billing.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,26 @@ def test_retrieve_matches_product_key(self, mock_all_prices):
130130
self.assertEqual(response.data['name'], 'AI Academy')
131131
self.assertEqual(response.data['stripe_product_id'], 'prod_ai')
132132

133+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
134+
def test_retrieve_matches_uuid(self, mock_all_prices):
135+
mock_all_prices.return_value = self.prices_by_lookup_key
136+
137+
url = reverse('api:v1:academy-products-detail', kwargs={'pk': str(self.ai_academy.uuid)})
138+
response = self.client.get(url)
139+
140+
self.assertEqual(response.status_code, status.HTTP_200_OK)
141+
self.assertEqual(response.data['name'], 'AI Academy')
142+
143+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
144+
def test_retrieve_matches_stripe_product_id(self, mock_all_prices):
145+
mock_all_prices.return_value = self.prices_by_lookup_key
146+
147+
url = reverse('api:v1:academy-products-detail', kwargs={'pk': 'prod_ai'})
148+
response = self.client.get(url)
149+
150+
self.assertEqual(response.status_code, status.HTTP_200_OK)
151+
self.assertEqual(response.data['name'], 'AI Academy')
152+
133153
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
134154
def test_retrieve_finds_inactive_academy_by_slug(self, mock_all_prices):
135155
mock_all_prices.return_value = self.prices_by_lookup_key
@@ -286,6 +306,48 @@ def test_list_can_include_inactive(self, mock_all_prices):
286306
self.assertEqual(response.status_code, status.HTTP_200_OK)
287307
self.assertEqual(response.data['count'], 2)
288308

309+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
310+
def test_list_filters_by_product_key(self, mock_all_prices):
311+
mock_all_prices.return_value = self.prices_by_lookup_key
312+
313+
url = reverse('api:v1:academy-products-list')
314+
response = self.client.get(url, {'product_key': 'academy_ai'})
315+
316+
self.assertEqual(response.status_code, status.HTTP_200_OK)
317+
self.assertEqual(response.data['count'], 1)
318+
self.assertEqual(response.data['results'][0]['name'], 'AI Academy')
319+
320+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
321+
def test_list_filters_by_valid_academy_uuid(self, mock_all_prices):
322+
mock_all_prices.return_value = self.prices_by_lookup_key
323+
324+
url = reverse('api:v1:academy-products-list')
325+
response = self.client.get(url, {'academy_uuid': str(self.ai_academy.uuid)})
326+
327+
self.assertEqual(response.status_code, status.HTTP_200_OK)
328+
self.assertEqual(response.data['count'], 1)
329+
self.assertEqual(response.data['results'][0]['name'], 'AI Academy')
330+
331+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
332+
def test_list_returns_empty_payload_when_no_active_academies(self, mock_all_prices):
333+
mock_all_prices.return_value = self.prices_by_lookup_key
334+
self.ai_academy.is_active = False
335+
self.ai_academy.save(update_fields=['is_active'])
336+
337+
url = reverse('api:v1:academy-products-list')
338+
response = self.client.get(url)
339+
340+
self.assertEqual(response.status_code, status.HTTP_200_OK)
341+
self.assertEqual(response.data['count'], 0)
342+
self.assertEqual(response.data['results'], [])
343+
344+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
345+
def test_helper_safe_get_all_stripe_prices_handles_stripe_error(self, mock_all_prices):
346+
mock_all_prices.side_effect = stripe.error.APIError('stripe api down')
347+
view = AcademyProductsViewSet()
348+
349+
self.assertEqual(view._safe_get_all_stripe_prices(), {})
350+
289351
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
290352
def test_list_returns_502_if_price_lookup_fails(self, mock_all_prices):
291353
mock_all_prices.side_effect = RuntimeError('pricing unavailable')
@@ -392,6 +454,26 @@ def test_build_normalized_academy_payload_applies_mapping_and_overrides(self):
392454
self.assertEqual(payload['stripe_product_id'], 'prod_override')
393455
self.assertTrue(payload['stripe_price_lookup_key'])
394456

457+
@override_settings(
458+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_ACADEMY_UUID={'abc': '456'},
459+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_NAME={'Data Academy': '457'},
460+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_NAME={
461+
'Data Academy': {
462+
'catalog_query_id': '789',
463+
}
464+
},
465+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_ACADEMY_UUID={
466+
'abc': {
467+
'catalog_query_id': '999',
468+
}
469+
},
470+
)
471+
def test_build_normalized_academy_payload_prefers_uuid_override_over_name_override(self):
472+
view = AcademyProductsViewSet()
473+
payload = view._build_normalized_academy_payload({'uuid': 'abc', 'name': 'Data Academy'}, 123)
474+
475+
self.assertEqual(payload['catalog_query_id'], 999)
476+
395477
def test_extract_results_helper_variants(self):
396478
view = AcademyProductsViewSet()
397479

@@ -413,6 +495,190 @@ def test_sync_essential_academies_skips_catalog_lookup_without_enterprise_custom
413495

414496
mock_client_instance.get_catalogs.assert_not_called()
415497

498+
@override_settings(
499+
ACADEMY_SYNC_ENTERPRISE_CUSTOMER_UUID='customer-uuid',
500+
ESSENTIAL_ACADEMY_UUID_ALLOWLIST=['keep-uuid'],
501+
ESSENTIAL_ACADEMY_NAME_ALLOWLIST=[],
502+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_ACADEMY_UUID={},
503+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_NAME={},
504+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_NAME={},
505+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_ACADEMY_UUID={},
506+
)
507+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.EnterpriseCatalogApiV1Client')
508+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.EnterpriseAcademy.objects')
509+
def test_sync_essential_academies_filters_by_uuid_allowlist(self, mock_academy_objects, mock_catalog_client_cls):
510+
view = AcademyProductsViewSet()
511+
512+
mock_client_instance = mock.Mock()
513+
mock_client_instance.get_academies.return_value = {
514+
'results': [
515+
{'uuid': 'keep-uuid', 'name': 'Allowed Academy'},
516+
{'uuid': 'drop-uuid', 'name': 'Filtered Academy'},
517+
]
518+
}
519+
mock_client_instance.get_catalogs.return_value = {'results': []}
520+
mock_catalog_client_cls.return_value = mock_client_instance
521+
522+
filtered_by_name_queryset = mock.Mock()
523+
filtered_by_name_queryset.first.return_value = None
524+
active_queryset = mock.Mock()
525+
active_queryset.exclude.return_value.update.return_value = 0
526+
527+
def filter_side_effect(*args, **kwargs):
528+
if kwargs.get('name__iexact'):
529+
return filtered_by_name_queryset
530+
if kwargs.get('is_active') is True:
531+
return active_queryset
532+
return mock.Mock()
533+
534+
mock_academy_objects.filter.side_effect = filter_side_effect
535+
536+
view._sync_essential_academies()
537+
538+
created_payload = mock_academy_objects.create.call_args.kwargs
539+
self.assertEqual(created_payload['name'], 'Allowed Academy')
540+
active_queryset.exclude.assert_called_once_with(name__in={'Allowed Academy'})
541+
542+
@override_settings(
543+
ACADEMY_SYNC_ENTERPRISE_CUSTOMER_UUID='customer-uuid',
544+
ESSENTIAL_ACADEMY_UUID_ALLOWLIST=[],
545+
ESSENTIAL_ACADEMY_NAME_ALLOWLIST=[],
546+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_ACADEMY_UUID={},
547+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_NAME={},
548+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_NAME={},
549+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_ACADEMY_UUID={},
550+
)
551+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.EnterpriseCatalogApiV1Client')
552+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.EnterpriseAcademy.objects')
553+
def test_sync_essential_academies_passes_default_catalog_query_id_to_builder(
554+
self,
555+
mock_academy_objects,
556+
mock_catalog_client_cls,
557+
):
558+
view = AcademyProductsViewSet()
559+
560+
mock_client_instance = mock.Mock()
561+
mock_client_instance.get_academies.return_value = {'results': [{'uuid': 'u-1', 'name': 'Data Academy'}]}
562+
mock_client_instance.get_catalogs.return_value = {
563+
'results': [
564+
{'catalog_query_id': 'invalid'},
565+
{'catalog_query_id': '777'},
566+
]
567+
}
568+
mock_catalog_client_cls.return_value = mock_client_instance
569+
570+
filtered_by_name_queryset = mock.Mock()
571+
filtered_by_name_queryset.first.return_value = None
572+
active_queryset = mock.Mock()
573+
active_queryset.exclude.return_value.update.return_value = 0
574+
575+
def filter_side_effect(*args, **kwargs):
576+
if kwargs.get('name__iexact'):
577+
return filtered_by_name_queryset
578+
if kwargs.get('is_active') is True:
579+
return active_queryset
580+
return mock.Mock()
581+
582+
mock_academy_objects.filter.side_effect = filter_side_effect
583+
584+
with mock.patch.object(
585+
view,
586+
'_build_normalized_academy_payload',
587+
wraps=view._build_normalized_academy_payload,
588+
) as wrapped_builder:
589+
view._sync_essential_academies()
590+
591+
wrapped_builder.assert_called_once_with({'uuid': 'u-1', 'name': 'Data Academy'}, 777)
592+
593+
@override_settings(
594+
ACADEMY_SYNC_ENTERPRISE_CUSTOMER_UUID='customer-uuid',
595+
ESSENTIAL_ACADEMY_UUID_ALLOWLIST=[],
596+
ESSENTIAL_ACADEMY_NAME_ALLOWLIST=[],
597+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_ACADEMY_UUID={},
598+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_NAME={},
599+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_NAME={},
600+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_ACADEMY_UUID={},
601+
)
602+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.EnterpriseCatalogApiV1Client')
603+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.EnterpriseAcademy.objects')
604+
def test_sync_essential_academies_skips_deactivation_when_no_valid_normalized_items(
605+
self,
606+
mock_academy_objects,
607+
mock_catalog_client_cls,
608+
):
609+
view = AcademyProductsViewSet()
610+
611+
mock_client_instance = mock.Mock()
612+
mock_client_instance.get_academies.return_value = {'results': [{'uuid': 'u-1'}]}
613+
mock_client_instance.get_catalogs.return_value = {'results': []}
614+
mock_catalog_client_cls.return_value = mock_client_instance
615+
616+
view._sync_essential_academies()
617+
618+
mock_academy_objects.create.assert_not_called()
619+
mock_academy_objects.filter.assert_not_called()
620+
621+
@override_settings(
622+
ACADEMY_SYNC_ENTERPRISE_CUSTOMER_UUID='customer-uuid',
623+
ESSENTIAL_ACADEMY_UUID_ALLOWLIST=[],
624+
ESSENTIAL_ACADEMY_NAME_ALLOWLIST=[],
625+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_ACADEMY_UUID={},
626+
ESSENTIAL_ACADEMY_CATALOG_QUERY_ID_BY_NAME={},
627+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_NAME={},
628+
ESSENTIAL_ACADEMY_FIELD_OVERRIDES_BY_ACADEMY_UUID={},
629+
)
630+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.EnterpriseCatalogApiV1Client')
631+
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.EnterpriseAcademy.objects')
632+
def test_sync_essential_academies_does_not_save_when_existing_record_unchanged(
633+
self,
634+
mock_academy_objects,
635+
mock_catalog_client_cls,
636+
):
637+
view = AcademyProductsViewSet()
638+
639+
mock_client_instance = mock.Mock()
640+
mock_client_instance.get_academies.return_value = {
641+
'results': [{'uuid': 'u-1', 'name': 'Data Academy', 'catalog_query_id': '200'}]
642+
}
643+
mock_client_instance.get_catalogs.return_value = {'results': []}
644+
mock_catalog_client_cls.return_value = mock_client_instance
645+
646+
existing = SimpleNamespace(
647+
name='Data Academy',
648+
long_name='Data Academy',
649+
description='',
650+
marketing_url='',
651+
thumbnail_url='',
652+
tags=[],
653+
stripe_product_id='',
654+
stripe_price_lookup_key='data-academy-u-1-price',
655+
catalog_query_id=111,
656+
product_key='data-academy',
657+
slug='data-academy',
658+
is_active=True,
659+
display_order=0,
660+
save=mock.Mock(),
661+
)
662+
663+
filtered_by_name_queryset = mock.Mock()
664+
filtered_by_name_queryset.first.return_value = existing
665+
active_queryset = mock.Mock()
666+
active_queryset.exclude.return_value.update.return_value = 0
667+
668+
def filter_side_effect(*args, **kwargs):
669+
if kwargs.get('name__iexact'):
670+
return filtered_by_name_queryset
671+
if kwargs.get('is_active') is True:
672+
return active_queryset
673+
return mock.Mock()
674+
675+
mock_academy_objects.filter.side_effect = filter_side_effect
676+
677+
view._sync_essential_academies()
678+
679+
mock_academy_objects.create.assert_not_called()
680+
existing.save.assert_not_called()
681+
416682
@override_settings(
417683
ACADEMY_SYNC_ENTERPRISE_CUSTOMER_UUID='customer-uuid',
418684
ESSENTIAL_ACADEMY_UUID_ALLOWLIST=[],

0 commit comments

Comments
 (0)