@@ -540,6 +540,181 @@ def test_package_api_filter_by_checksums(self):
540540 self .assertEqual (expected_status , response .data ["status" ])
541541
542542
543+ class PackageApiDependentsTestCase (TestCase ):
544+ def setUp (self ):
545+ self .client = APIClient ()
546+
547+ # Target package: the package we want to find dependents of.
548+ self .target_package = Package .objects .create (
549+ download_url = "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" ,
550+ type = "npm" ,
551+ name = "lodash" ,
552+ version = "4.17.21" ,
553+ )
554+ self .target_package .refresh_from_db ()
555+
556+ # Package A depends on the target package.
557+ self .package_a = Package .objects .create (
558+ download_url = "https://registry.npmjs.org/express/-/express-4.18.2.tgz" ,
559+ type = "npm" ,
560+ name = "express" ,
561+ version = "4.18.2" ,
562+ )
563+ self .package_a .refresh_from_db ()
564+ from packagedb .models import DependentPackage
565+
566+ DependentPackage .objects .create (
567+ package = self .package_a ,
568+ purl = "pkg:npm/lodash@>=4.0.0" ,
569+ scope = "runtime" ,
570+ is_runtime = True ,
571+ is_optional = False ,
572+ )
573+
574+ # Package B also depends on the target package (as optional).
575+ self .package_b = Package .objects .create (
576+ download_url = "https://registry.npmjs.org/webpack/-/webpack-5.88.0.tgz" ,
577+ type = "npm" ,
578+ name = "webpack" ,
579+ version = "5.88.0" ,
580+ )
581+ self .package_b .refresh_from_db ()
582+ DependentPackage .objects .create (
583+ package = self .package_b ,
584+ purl = "pkg:npm/lodash" ,
585+ scope = "develop" ,
586+ is_runtime = False ,
587+ is_optional = True ,
588+ )
589+
590+ # Package C does NOT depend on the target package.
591+ self .package_c = Package .objects .create (
592+ download_url = "https://registry.npmjs.org/react/-/react-18.2.0.tgz" ,
593+ type = "npm" ,
594+ name = "react" ,
595+ version = "18.2.0" ,
596+ )
597+ self .package_c .refresh_from_db ()
598+ DependentPackage .objects .create (
599+ package = self .package_c ,
600+ purl = "pkg:npm/scheduler@>=0.20.0" ,
601+ scope = "runtime" ,
602+ is_runtime = True ,
603+ is_optional = False ,
604+ )
605+
606+ def test_api_package_dependents_action (self ):
607+ response = self .client .get (
608+ reverse ("api:package-dependents" , args = [self .target_package .uuid ])
609+ )
610+ self .assertEqual (response .status_code , status .HTTP_200_OK )
611+ self .assertEqual (2 , response .data ["count" ])
612+ result_purls = {r ["purl" ] for r in response .data ["results" ]}
613+ self .assertIn (self .package_a .purl , result_purls )
614+ self .assertIn (self .package_b .purl , result_purls )
615+ self .assertNotIn (self .package_c .purl , result_purls )
616+
617+ def test_api_package_dependents_filter_by_scope (self ):
618+ response = self .client .get (
619+ reverse ("api:package-dependents" , args = [self .target_package .uuid ]),
620+ {"scope" : "runtime" },
621+ )
622+ self .assertEqual (response .status_code , status .HTTP_200_OK )
623+ self .assertEqual (1 , response .data ["count" ])
624+ self .assertEqual (self .package_a .purl , response .data ["results" ][0 ]["purl" ])
625+
626+ def test_api_package_dependents_filter_by_is_runtime (self ):
627+ response = self .client .get (
628+ reverse ("api:package-dependents" , args = [self .target_package .uuid ]),
629+ {"is_runtime" : "false" },
630+ )
631+ self .assertEqual (response .status_code , status .HTTP_200_OK )
632+ self .assertEqual (1 , response .data ["count" ])
633+ self .assertEqual (self .package_b .purl , response .data ["results" ][0 ]["purl" ])
634+
635+ def test_api_package_dependents_filter_by_is_optional (self ):
636+ response = self .client .get (
637+ reverse ("api:package-dependents" , args = [self .target_package .uuid ]),
638+ {"is_optional" : "true" },
639+ )
640+ self .assertEqual (response .status_code , status .HTTP_200_OK )
641+ self .assertEqual (1 , response .data ["count" ])
642+ self .assertEqual (self .package_b .purl , response .data ["results" ][0 ]["purl" ])
643+
644+ def test_api_package_dependents_empty_results (self ):
645+ # react has no dependents
646+ response = self .client .get (
647+ reverse ("api:package-dependents" , args = [self .package_c .uuid ])
648+ )
649+ self .assertEqual (response .status_code , status .HTTP_200_OK )
650+ self .assertEqual (0 , response .data ["count" ])
651+
652+ def test_api_package_dependents_with_namespace (self ):
653+ """Test that dependents lookup works correctly for packages with namespaces."""
654+ namespaced_package = Package .objects .create (
655+ download_url = "https://repo1.maven.org/org/apache/commons/commons-lang3-3.12.jar" ,
656+ type = "maven" ,
657+ namespace = "org.apache.commons" ,
658+ name = "commons-lang3" ,
659+ version = "3.12.0" ,
660+ )
661+ namespaced_package .refresh_from_db ()
662+
663+ dependent = Package .objects .create (
664+ download_url = "https://repo1.maven.org/org/example/myapp-1.0.jar" ,
665+ type = "maven" ,
666+ namespace = "org.example" ,
667+ name = "myapp" ,
668+ version = "1.0" ,
669+ )
670+ dependent .refresh_from_db ()
671+ from packagedb .models import DependentPackage
672+
673+ DependentPackage .objects .create (
674+ package = dependent ,
675+ purl = "pkg:maven/org.apache.commons/commons-lang3@>=3.0" ,
676+ scope = "compile" ,
677+ is_runtime = True ,
678+ is_optional = False ,
679+ )
680+
681+ response = self .client .get (
682+ reverse ("api:package-dependents" , args = [namespaced_package .uuid ])
683+ )
684+ self .assertEqual (response .status_code , status .HTTP_200_OK )
685+ self .assertEqual (1 , response .data ["count" ])
686+ self .assertEqual (dependent .purl , response .data ["results" ][0 ]["purl" ])
687+
688+ def test_api_package_dependents_no_false_positive_on_similar_names (self ):
689+ """Test that 'lodash' does not match 'lodash-es' dependencies."""
690+ from packagedb .models import DependentPackage
691+
692+ # Package D depends on lodash-es (not lodash).
693+ package_d = Package .objects .create (
694+ download_url = "https://registry.npmjs.org/some-pkg/-/some-pkg-1.0.0.tgz" ,
695+ type = "npm" ,
696+ name = "some-pkg" ,
697+ version = "1.0.0" ,
698+ )
699+ DependentPackage .objects .create (
700+ package = package_d ,
701+ purl = "pkg:npm/lodash-es@4.17.21" ,
702+ scope = "runtime" ,
703+ is_runtime = True ,
704+ is_optional = False ,
705+ )
706+
707+ response = self .client .get (
708+ reverse ("api:package-dependents" , args = [self .target_package .uuid ])
709+ )
710+ self .assertEqual (response .status_code , status .HTTP_200_OK )
711+ result_purls = {r ["purl" ] for r in response .data ["results" ]}
712+ # Should NOT include package_d which depends on lodash-es.
713+ self .assertNotIn (package_d .purl , result_purls )
714+ # Should still include the two legitimate dependents.
715+ self .assertEqual (2 , response .data ["count" ])
716+
717+
543718class PackageApiReindexingTestCase (JsonBasedTesting , TestCase ):
544719 test_data_dir = os .path .join (os .path .dirname (__file__ ), "testfiles" )
545720
0 commit comments