@@ -335,6 +335,7 @@ def test_filter_with_org_id_anonymous():
335335
336336
337337@pytest .mark .django_db
338+ @pytest .mark .skip_nplusone_check
338339def test_filter_with_org_id_returns_contracted_course (
339340 mocker , contract_ready_course , mock_course_run_clone
340341):
@@ -363,6 +364,7 @@ def test_filter_with_org_id_returns_contracted_course(
363364
364365
365366@pytest .mark .django_db
367+ @pytest .mark .skip_nplusone_check
366368def test_filter_with_org_id_user_not_associated_with_org_returns_no_courses (
367369 contract_ready_course , mock_course_run_clone
368370):
@@ -386,6 +388,304 @@ def test_filter_with_org_id_user_not_associated_with_org_returns_no_courses(
386388 assert unrelated_course .title not in titles
387389
388390
391+ @pytest .mark .django_db
392+ @pytest .mark .skip_nplusone_check
393+ def test_filter_with_org_id_multiple_courses_same_org (
394+ contract_ready_course , mock_course_run_clone
395+ ):
396+ """Test that filtering by org_id returns all contracted courses for that org"""
397+ org = OrganizationPageFactory (name = "Test Org" )
398+ contract = ContractPageFactory (organization = org , active = True )
399+ user = UserFactory ()
400+ user .b2b_organizations .add (org )
401+ user .b2b_contracts .add (contract )
402+ user .refresh_from_db ()
403+
404+ # Create multiple courses for the same org
405+ (course1 , _ ) = contract_ready_course
406+ create_contract_run (contract , course1 )
407+
408+ course2 = CourseFactory ()
409+ CourseRunFactory (course = course2 , is_source_run = True )
410+ create_contract_run (contract , course2 )
411+
412+ course3 = CourseFactory ()
413+ CourseRunFactory (course = course3 , is_source_run = True )
414+ create_contract_run (contract , course3 )
415+
416+ # Create unrelated course (no contract_run - should not appear in results)
417+ unrelated_course = CourseFactory ()
418+ CourseRunFactory (course = unrelated_course )
419+
420+ client = APIClient ()
421+ client .force_authenticate (user = user )
422+
423+ url = reverse ("v2:courses_api-list" )
424+ response = client .get (url , {"org_id" : org .id })
425+
426+ course_ids = [result ["id" ] for result in response .data ["results" ]]
427+ assert course1 .id in course_ids
428+ assert course2 .id in course_ids
429+ assert course3 .id in course_ids
430+ assert unrelated_course .id not in course_ids
431+ assert len (course_ids ) == 3
432+
433+
434+ @pytest .mark .django_db
435+ @pytest .mark .skip_nplusone_check
436+ def test_filter_with_org_id_inactive_contract_excluded (
437+ contract_ready_course , mock_course_run_clone
438+ ):
439+ """Test that courses from inactive contracts are not returned"""
440+ org = OrganizationPageFactory (name = "Test Org" )
441+ contract = ContractPageFactory (organization = org , active = False )
442+ user = UserFactory ()
443+ user .b2b_organizations .add (org )
444+ user .b2b_contracts .add (contract )
445+ user .refresh_from_db ()
446+
447+ (course , _ ) = contract_ready_course
448+ create_contract_run (contract , course )
449+
450+ client = APIClient ()
451+ client .force_authenticate (user = user )
452+
453+ url = reverse ("v2:courses_api-list" )
454+ response = client .get (url , {"org_id" : org .id })
455+
456+ assert response .data ["results" ] == []
457+
458+
459+ @pytest .mark .django_db
460+ @pytest .mark .skip_nplusone_check
461+ def test_filter_with_org_id_multiple_orgs (contract_ready_course , mock_course_run_clone ):
462+ """Test that filtering by org_id returns courses only for that specific org"""
463+ org1 = OrganizationPageFactory (name = "Test Org 1" )
464+ org2 = OrganizationPageFactory (name = "Test Org 2" )
465+ contract1 = ContractPageFactory (organization = org1 , active = True )
466+ contract2 = ContractPageFactory (organization = org2 , active = True )
467+
468+ user = UserFactory ()
469+ user .b2b_organizations .add (org1 , org2 )
470+ user .b2b_contracts .add (contract1 , contract2 )
471+ user .refresh_from_db ()
472+
473+ (course1 , _ ) = contract_ready_course
474+ create_contract_run (contract1 , course1 )
475+
476+ course2 = CourseFactory ()
477+ CourseRunFactory (course = course2 , is_source_run = True )
478+ create_contract_run (contract2 , course2 )
479+
480+ client = APIClient ()
481+ client .force_authenticate (user = user )
482+
483+ url = reverse ("v2:courses_api-list" )
484+
485+ # Filter for org1
486+ response = client .get (url , {"org_id" : org1 .id })
487+ course_ids = [result ["id" ] for result in response .data ["results" ]]
488+ assert course1 .id in course_ids
489+ assert course2 .id not in course_ids
490+
491+ # Filter for org2
492+ response = client .get (url , {"org_id" : org2 .id })
493+ course_ids = [result ["id" ] for result in response .data ["results" ]]
494+ assert course2 .id in course_ids
495+ assert course1 .id not in course_ids
496+
497+
498+ @pytest .mark .django_db
499+ @pytest .mark .skip_nplusone_check
500+ def test_filter_with_org_id_user_in_org_but_no_contract (
501+ contract_ready_course , mock_course_run_clone
502+ ):
503+ """Test that user in org can see org's contracted courses"""
504+ org = OrganizationPageFactory (name = "Test Org" )
505+ contract = ContractPageFactory (organization = org , active = True )
506+ user = UserFactory ()
507+ user .b2b_organizations .add (org )
508+ # User is in org but NOT added to contract
509+ user .refresh_from_db ()
510+
511+ (course , _ ) = contract_ready_course
512+ create_contract_run (contract , course )
513+
514+ client = APIClient ()
515+ client .force_authenticate (user = user )
516+
517+ url = reverse ("v2:courses_api-list" )
518+ response = client .get (url , {"org_id" : org .id })
519+
520+ titles = [result ["title" ] for result in response .data ["results" ]]
521+ assert course .title in titles
522+
523+
524+ @pytest .mark .django_db
525+ def test_filter_with_org_id_nonexistent_org_id (user_drf_client ):
526+ """Test that filtering with a nonexistent org_id returns no results"""
527+ course = CourseFactory (title = "Test Course" )
528+ CourseRunFactory (course = course )
529+
530+ url = reverse ("v2:courses_api-list" )
531+ response = user_drf_client .get (url , {"org_id" : 99999 })
532+
533+ assert response .data ["results" ] == []
534+
535+
536+ @pytest .mark .django_db
537+ @pytest .mark .skip_nplusone_check
538+ def test_filter_with_org_id_returns_detail_view (
539+ contract_ready_course , mock_course_run_clone
540+ ):
541+ """Test that org_id filter works on detail view endpoint"""
542+ org = OrganizationPageFactory (name = "Test Org" )
543+ contract = ContractPageFactory (organization = org , active = True )
544+ user = UserFactory ()
545+ user .b2b_organizations .add (org )
546+ user .b2b_contracts .add (contract )
547+ user .refresh_from_db ()
548+
549+ (course , _ ) = contract_ready_course
550+ create_contract_run (contract , course )
551+
552+ client = APIClient ()
553+ client .force_authenticate (user = user )
554+
555+ url = reverse ("v2:courses_api-detail" , kwargs = {"pk" : course .id })
556+ response = client .get (url , {"org_id" : org .id })
557+
558+ assert response .status_code == status .HTTP_200_OK
559+ assert response .data ["id" ] == course .id
560+ assert response .data ["title" ] == course .title
561+
562+
563+ @pytest .mark .django_db
564+ @pytest .mark .skip_nplusone_check
565+ def test_filter_with_org_id_detail_view_unauthorized_user (
566+ contract_ready_course , mock_course_run_clone
567+ ):
568+ """Test that org_id filter prevents unauthorized users from viewing contracted courses"""
569+ org = OrganizationPageFactory (name = "Test Org" )
570+ contract = ContractPageFactory (organization = org , active = True )
571+ user = UserFactory ()
572+ # User NOT added to org
573+ user .refresh_from_db ()
574+
575+ (course , _ ) = contract_ready_course
576+ create_contract_run (contract , course )
577+
578+ client = APIClient ()
579+ client .force_authenticate (user = user )
580+
581+ url = reverse ("v2:courses_api-detail" , kwargs = {"pk" : course .id })
582+ response = client .get (url , {"org_id" : org .id })
583+
584+ # User doesn't have access to this org, so should get 404
585+ assert response .status_code == status .HTTP_404_NOT_FOUND
586+
587+
588+ @pytest .mark .django_db
589+ @pytest .mark .skip_nplusone_check
590+ def test_filter_with_org_id_respects_course_live_status (
591+ contract_ready_course , mock_course_run_clone
592+ ):
593+ """Test that org_id filter returns contracted courses regardless of live status"""
594+ org = OrganizationPageFactory (name = "Test Org" )
595+ contract = ContractPageFactory (organization = org , active = True )
596+ user = UserFactory ()
597+ user .b2b_organizations .add (org )
598+ user .b2b_contracts .add (contract )
599+ user .refresh_from_db ()
600+
601+ (course , _ ) = contract_ready_course
602+ create_contract_run (contract , course )
603+
604+ # Make course page not live - org_id filter doesn't filter by live status
605+ course .page .live = False
606+ course .page .save ()
607+
608+ client = APIClient ()
609+ client .force_authenticate (user = user )
610+
611+ url = reverse ("v2:courses_api-list" )
612+ response = client .get (url , {"org_id" : org .id })
613+
614+ # Course should appear even though it's not live
615+ course_ids = [result ["id" ] for result in response .data ["results" ]]
616+ assert course .id in course_ids
617+
618+
619+ @pytest .mark .django_db
620+ @pytest .mark .skip_nplusone_check
621+ def test_filter_with_org_id_pagination (contract_ready_course , mock_course_run_clone ):
622+ """Test that org_id filter works correctly with pagination"""
623+ org = OrganizationPageFactory (name = "Test Org" )
624+ contract = ContractPageFactory (organization = org , active = True )
625+ user = UserFactory ()
626+ user .b2b_organizations .add (org )
627+ user .b2b_contracts .add (contract )
628+ user .refresh_from_db ()
629+
630+ (course1 , _ ) = contract_ready_course
631+ create_contract_run (contract , course1 )
632+
633+ # Create more courses to test pagination
634+ for i in range (15 ):
635+ course = CourseFactory (title = f"Org Course { i } " )
636+ CourseRunFactory (course = course , is_source_run = True )
637+ create_contract_run (contract , course )
638+
639+ client = APIClient ()
640+ client .force_authenticate (user = user )
641+
642+ url = reverse ("v2:courses_api-list" )
643+ response = client .get (url , {"org_id" : org .id , "page_size" : 5 })
644+
645+ assert response .status_code == status .HTTP_200_OK
646+ assert response .data ["count" ] >= 15
647+ assert len (response .data ["results" ]) == 5
648+ assert "next" in response .data
649+ assert response .data ["next" ] is not None
650+
651+
652+ @pytest .mark .django_db
653+ @pytest .mark .skip_nplusone_check
654+ def test_filter_with_org_id_combined_with_other_filters (
655+ contract_ready_course , mock_course_run_clone
656+ ):
657+ """Test that org_id filter can be combined with other filters"""
658+ org = OrganizationPageFactory (name = "Test Org" )
659+ contract = ContractPageFactory (organization = org , active = True )
660+ user = UserFactory ()
661+ user .b2b_organizations .add (org )
662+ user .b2b_contracts .add (contract )
663+ user .refresh_from_db ()
664+
665+ (course1 , _ ) = contract_ready_course
666+ create_contract_run (contract , course1 )
667+
668+ course2 = CourseFactory ()
669+ CourseRunFactory (course = course2 , is_source_run = True )
670+ create_contract_run (contract , course2 )
671+
672+ unrelated_course = CourseFactory ()
673+ CourseRunFactory (course = unrelated_course )
674+
675+ client = APIClient ()
676+ client .force_authenticate (user = user )
677+
678+ url = reverse ("v2:courses_api-list" )
679+ # Filter by org_id and specific course readable_id
680+ response = client .get (url , {"org_id" : org .id , "readable_id" : course1 .readable_id })
681+
682+ assert response .status_code == status .HTTP_200_OK
683+ course_ids = [result ["id" ] for result in response .data ["results" ]]
684+ assert course1 .id in course_ids
685+ assert course2 .id not in course_ids
686+ assert unrelated_course .id not in course_ids
687+
688+
389689@pytest .mark .django_db
390690@pytest .mark .skip_nplusone_check
391691def test_filter_without_org_id_authenticated_user (user_drf_client ):
0 commit comments