Skip to content

Commit ecada6e

Browse files
test: Add comprehensive unit tests for download_and_scan_package
Added 5 unit tests covering all code paths: 1. test_purl2notices_success - Tests primary method (purl2notices) - Verifies license and copyright extraction - Confirms method_used = 'purl2notices' 2. test_deep_scan_fallback - Tests fallback when purl2notices fails - Mocks purl2src → download → osslili → upmex workflow - Verifies artifact download and local scanning 3. test_online_fallback - Tests online API fallback - Mocks purl2notices and deep scan failures - Verifies upmex --api clearlydefined usage 4. test_all_methods_fail - Tests error handling when all methods fail - Verifies methods_attempted tracking - Confirms error state and messaging 5. test_keep_download - Tests keep_download parameter - Verifies cleanup is NOT called when keep_download=True Test Results: ✅ All 5 tests passed in 0.37s Coverage: - Primary workflow (purl2notices) - Deep scan workflow (purl2src + download + osslili + upmex) - Online fallback workflow (upmex --api) - Error handling for all failure scenarios - File cleanup behavior
1 parent 58a91c8 commit ecada6e

File tree

1 file changed

+204
-1
lines changed

1 file changed

+204
-1
lines changed

tests/test_server.py

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,4 +523,207 @@ def test_run_tool_not_found(self):
523523
mock_run.side_effect = FileNotFoundError("command not found")
524524

525525
with pytest.raises(FileNotFoundError):
526-
_run_tool("nonexistent_tool", ["--help"])
526+
_run_tool("nonexistent_tool", ["--help"])
527+
528+
class TestDownloadAndScanPackage:
529+
"""Test cases for download_and_scan_package tool."""
530+
531+
@pytest.mark.asyncio
532+
async def test_purl2notices_success(self):
533+
"""Test successful analysis using purl2notices (primary method)."""
534+
# Mock purl2notices cache output
535+
cache_data = {
536+
"components": [{
537+
"name": "express",
538+
"version": "4.21.2",
539+
"purl": "pkg:npm/express@4.21.2",
540+
"licenses": [
541+
{"license": {"id": "MIT"}},
542+
{"license": {"id": "MIT-or-later"}}
543+
],
544+
"properties": [
545+
{"name": "copyright", "value": "Copyright 2009-2013 TJ Holowaychuk"},
546+
{"name": "copyright", "value": "Copyright 2014-2015 Douglas Christopher Wilson"}
547+
]
548+
}]
549+
}
550+
551+
with patch("mcp_semclone.server._run_tool") as mock_run, \
552+
patch("builtins.open", create=True) as mock_open, \
553+
patch("pathlib.Path.unlink"):
554+
555+
# Mock purl2notices execution
556+
mock_run.return_value = MagicMock(returncode=0, stdout="success")
557+
558+
# Mock cache file read
559+
mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(cache_data)
560+
561+
result = await server_module.download_and_scan_package(
562+
purl="pkg:npm/express@4.21.2"
563+
)
564+
565+
# Verify method used
566+
assert result["method_used"] == "purl2notices"
567+
assert result["purl"] == "pkg:npm/express@4.21.2"
568+
569+
# Verify data extracted
570+
assert result["metadata"]["name"] == "express"
571+
assert result["metadata"]["version"] == "4.21.2"
572+
assert "MIT" in result["detected_licenses"]
573+
assert len(result["copyright_statements"]) == 2
574+
assert "purl2notices" in result["methods_attempted"]
575+
576+
@pytest.mark.asyncio
577+
async def test_deep_scan_fallback(self):
578+
"""Test deep scan fallback when purl2notices fails."""
579+
# Mock purl2src output
580+
purl2src_data = [{
581+
"purl": "pkg:npm/express@4.21.2",
582+
"download_url": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
583+
"validated": True
584+
}]
585+
586+
# Mock osslili output
587+
osslili_data = {
588+
"components": [{
589+
"licenses": [{"license": {"id": "MIT"}}],
590+
"properties": [
591+
{"name": "copyright", "value": "Copyright Test"}
592+
]
593+
}]
594+
}
595+
596+
# Mock upmex output
597+
upmex_data = {
598+
"name": "express",
599+
"version": "4.21.2",
600+
"license": "MIT"
601+
}
602+
603+
with patch("mcp_semclone.server._run_tool") as mock_run, \
604+
patch("urllib.request.urlretrieve") as mock_download, \
605+
patch("tempfile.mkdtemp", return_value="/tmp/test"), \
606+
patch("pathlib.Path.exists", return_value=True), \
607+
patch("shutil.rmtree"):
608+
609+
def run_tool_side_effect(tool_name, args, *a, **kw):
610+
if tool_name == "purl2notices":
611+
# purl2notices fails
612+
return MagicMock(returncode=1, stdout="", stderr="failed")
613+
elif tool_name == "purl2src":
614+
return MagicMock(returncode=0, stdout=json.dumps(purl2src_data))
615+
elif tool_name == "osslili":
616+
return MagicMock(returncode=0, stdout=json.dumps(osslili_data))
617+
elif tool_name == "upmex":
618+
return MagicMock(returncode=0, stdout=json.dumps(upmex_data))
619+
return MagicMock(returncode=1, stdout="")
620+
621+
mock_run.side_effect = run_tool_side_effect
622+
623+
result = await server_module.download_and_scan_package(
624+
purl="pkg:npm/express@4.21.2"
625+
)
626+
627+
# Verify deep scan was used
628+
assert result["method_used"] == "deep_scan"
629+
assert "purl2notices" in result["methods_attempted"]
630+
assert "deep_scan" in result["methods_attempted"]
631+
632+
# Verify download was called
633+
mock_download.assert_called_once()
634+
635+
# Verify data extracted
636+
assert "MIT" in result["detected_licenses"]
637+
assert len(result["copyright_statements"]) >= 1
638+
639+
@pytest.mark.asyncio
640+
async def test_online_fallback(self):
641+
"""Test online fallback when deep scan fails."""
642+
# Mock upmex online output
643+
upmex_data = {
644+
"name": "express",
645+
"version": "4.21.2",
646+
"license": "MIT"
647+
}
648+
649+
with patch("mcp_semclone.server._run_tool") as mock_run, \
650+
patch("builtins.open", create=True), \
651+
patch("pathlib.Path.unlink"):
652+
653+
def run_tool_side_effect(tool_name, args, *a, **kw):
654+
if tool_name == "purl2notices":
655+
# purl2notices fails
656+
return MagicMock(returncode=1, stdout="", stderr="failed")
657+
elif tool_name == "purl2src":
658+
# purl2src fails (no download URL)
659+
return MagicMock(returncode=1, stdout="[]")
660+
elif tool_name == "upmex" and "--api" in args:
661+
# Online API succeeds
662+
return MagicMock(returncode=0, stdout=json.dumps(upmex_data))
663+
return MagicMock(returncode=1, stdout="")
664+
665+
mock_run.side_effect = run_tool_side_effect
666+
667+
result = await server_module.download_and_scan_package(
668+
purl="pkg:pypi/somepackage@1.0.0"
669+
)
670+
671+
# Verify online fallback was used
672+
assert result["method_used"] == "online_fallback"
673+
assert "purl2notices" in result["methods_attempted"]
674+
assert "deep_scan" in result["methods_attempted"]
675+
assert "online_fallback" in result["methods_attempted"]
676+
677+
# Verify metadata extracted
678+
assert result["metadata"]["license"] == "MIT"
679+
680+
@pytest.mark.asyncio
681+
async def test_all_methods_fail(self):
682+
"""Test behavior when all methods fail."""
683+
with patch("mcp_semclone.server._run_tool") as mock_run, \
684+
patch("builtins.open", create=True), \
685+
patch("pathlib.Path.unlink"):
686+
687+
# All tools fail
688+
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="failed")
689+
690+
result = await server_module.download_and_scan_package(
691+
purl="pkg:npm/invalid@0.0.0"
692+
)
693+
694+
# Verify error state
695+
assert result["method_used"] is None
696+
assert result["error"] == "All methods failed to retrieve package data"
697+
assert len(result["methods_attempted"]) == 3
698+
assert "purl2notices" in result["methods_attempted"]
699+
assert "deep_scan" in result["methods_attempted"]
700+
assert "online_fallback" in result["methods_attempted"]
701+
702+
@pytest.mark.asyncio
703+
async def test_keep_download(self):
704+
"""Test that keep_download preserves downloaded files."""
705+
cache_data = {
706+
"components": [{
707+
"name": "express",
708+
"version": "4.21.2",
709+
"purl": "pkg:npm/express@4.21.2",
710+
"licenses": [{"license": {"id": "MIT"}}],
711+
"properties": []
712+
}]
713+
}
714+
715+
with patch("mcp_semclone.server._run_tool") as mock_run, \
716+
patch("builtins.open", create=True) as mock_open, \
717+
patch("pathlib.Path.unlink"), \
718+
patch("shutil.rmtree") as mock_rmtree:
719+
720+
mock_run.return_value = MagicMock(returncode=0, stdout="success")
721+
mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(cache_data)
722+
723+
result = await server_module.download_and_scan_package(
724+
purl="pkg:npm/express@4.21.2",
725+
keep_download=True
726+
)
727+
728+
# Verify cleanup was NOT called (keep_download=True)
729+
mock_rmtree.assert_not_called()

0 commit comments

Comments
 (0)