@@ -727,3 +727,142 @@ async def test_keep_download(self):
727727
728728 # Verify cleanup was NOT called (keep_download=True)
729729 mock_rmtree .assert_not_called ()
730+
731+ @pytest .mark .asyncio
732+ async def test_maven_parent_pom_resolution (self ):
733+ """Test Maven parent POM license resolution when package POM has no license."""
734+ # Mock purl2src output for Maven package
735+ purl2src_data = [{
736+ "purl" : "pkg:maven/org.example/library@1.0.0" ,
737+ "download_url" : "https://repo1.maven.org/maven2/org/example/library/1.0.0/library-1.0.0.jar" ,
738+ "validated" : True
739+ }]
740+
741+ # Mock osslili output (no licenses found in JAR)
742+ osslili_data = {
743+ "components" : []
744+ }
745+
746+ # Mock upmex output (no license in package POM)
747+ upmex_no_license = {
748+ "name" : "library" ,
749+ "version" : "1.0.0"
750+ # No license field
751+ }
752+
753+ # Mock upmex with --registry --api clearlydefined (finds parent POM license)
754+ upmex_with_parent = {
755+ "name" : "library" ,
756+ "version" : "1.0.0" ,
757+ "license" : "Apache-2.0" # Found in parent POM
758+ }
759+
760+ with patch ("mcp_semclone.server._run_tool" ) as mock_run , \
761+ patch ("urllib.request.urlretrieve" ) as mock_download , \
762+ patch ("tempfile.mkdtemp" , return_value = "/tmp/test" ), \
763+ patch ("pathlib.Path.exists" , return_value = True ), \
764+ patch ("shutil.rmtree" ):
765+
766+ call_count = {"upmex" : 0 }
767+
768+ def run_tool_side_effect (tool_name , args , * a , ** kw ):
769+ if tool_name == "purl2notices" :
770+ # purl2notices fails
771+ return MagicMock (returncode = 1 , stdout = "" , stderr = "failed" )
772+ elif tool_name == "purl2src" :
773+ return MagicMock (returncode = 0 , stdout = json .dumps (purl2src_data ))
774+ elif tool_name == "osslili" :
775+ return MagicMock (returncode = 0 , stdout = json .dumps (osslili_data ))
776+ elif tool_name == "upmex" :
777+ call_count ["upmex" ] += 1
778+ # First call: no license
779+ if call_count ["upmex" ] == 1 :
780+ return MagicMock (returncode = 0 , stdout = json .dumps (upmex_no_license ))
781+ # Second call with --registry --api: has license from parent POM
782+ elif "--registry" in args and "--api" in args :
783+ return MagicMock (returncode = 0 , stdout = json .dumps (upmex_with_parent ))
784+ return MagicMock (returncode = 1 , stdout = "" )
785+
786+ mock_run .side_effect = run_tool_side_effect
787+
788+ result = await server_module .download_and_scan_package (
789+ purl = "pkg:maven/org.example/library@1.0.0"
790+ )
791+
792+ # Verify Maven parent POM resolution was triggered
793+ assert result ["declared_license" ] == "Apache-2.0"
794+ assert result ["metadata" ].get ("license" ) == "Apache-2.0"
795+ assert result ["metadata" ].get ("license_source" ) == "parent_pom_via_clearlydefined"
796+
797+ # Verify upmex was called twice (once normal, once with --registry --api)
798+ assert call_count ["upmex" ] == 2
799+
800+ @pytest .mark .asyncio
801+ async def test_maven_combined_source_and_parent_pom_licenses (self ):
802+ """Test Maven package with licenses in both source headers AND parent POM."""
803+ # Mock purl2src output
804+ purl2src_data = [{
805+ "purl" : "pkg:maven/org.example/library@1.0.0" ,
806+ "download_url" : "https://repo1.maven.org/maven2/org/example/library/1.0.0/library-1.0.0.jar" ,
807+ "validated" : True
808+ }]
809+
810+ # Mock osslili output (finds MIT in source file headers)
811+ osslili_data = {
812+ "components" : [{
813+ "licenses" : [{"license" : {"id" : "MIT" }}],
814+ "properties" : [{"name" : "copyright" , "value" : "Copyright 2024" }]
815+ }]
816+ }
817+
818+ # Mock upmex output (no license in package POM)
819+ upmex_no_license = {
820+ "name" : "library" ,
821+ "version" : "1.0.0"
822+ }
823+
824+ # Mock upmex with --registry --api (finds Apache-2.0 in parent POM)
825+ upmex_with_parent = {
826+ "name" : "library" ,
827+ "version" : "1.0.0" ,
828+ "license" : "Apache-2.0"
829+ }
830+
831+ with patch ("mcp_semclone.server._run_tool" ) as mock_run , \
832+ patch ("urllib.request.urlretrieve" ), \
833+ patch ("tempfile.mkdtemp" , return_value = "/tmp/test" ), \
834+ patch ("pathlib.Path.exists" , return_value = True ), \
835+ patch ("shutil.rmtree" ):
836+
837+ call_count = {"upmex" : 0 }
838+
839+ def run_tool_side_effect (tool_name , args , * a , ** kw ):
840+ if tool_name == "purl2notices" :
841+ return MagicMock (returncode = 1 , stdout = "" , stderr = "failed" )
842+ elif tool_name == "purl2src" :
843+ return MagicMock (returncode = 0 , stdout = json .dumps (purl2src_data ))
844+ elif tool_name == "osslili" :
845+ return MagicMock (returncode = 0 , stdout = json .dumps (osslili_data ))
846+ elif tool_name == "upmex" :
847+ call_count ["upmex" ] += 1
848+ if call_count ["upmex" ] == 1 :
849+ return MagicMock (returncode = 0 , stdout = json .dumps (upmex_no_license ))
850+ elif "--registry" in args and "--api" in args :
851+ return MagicMock (returncode = 0 , stdout = json .dumps (upmex_with_parent ))
852+ return MagicMock (returncode = 1 , stdout = "" )
853+
854+ mock_run .side_effect = run_tool_side_effect
855+
856+ result = await server_module .download_and_scan_package (
857+ purl = "pkg:maven/org.example/library@1.0.0"
858+ )
859+
860+ # Verify BOTH licenses are present
861+ assert result ["declared_license" ] == "Apache-2.0" # From parent POM
862+ assert "MIT" in result ["detected_licenses" ] # From source headers
863+ assert "Apache-2.0" in result ["detected_licenses" ] # Added from parent POM
864+ assert len (result ["detected_licenses" ]) == 2 # Both licenses
865+ assert result ["metadata" ].get ("license_source" ) == "parent_pom_via_clearlydefined"
866+
867+ # Verify summary mentions parent POM
868+ assert "parent pom" in result ["scan_summary" ].lower ()
0 commit comments