Skip to content

Commit ac1d701

Browse files
committed
Updated testcases for vulnerabilities
1 parent e4011b0 commit ac1d701

1 file changed

Lines changed: 270 additions & 1 deletion

File tree

cloudsmith_cli/cli/tests/commands/test_vulnerabilities.py

Lines changed: 270 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import unittest
2-
from unittest.mock import patch
2+
from unittest.mock import MagicMock, patch
33

44
from click.testing import CliRunner
55

@@ -108,5 +108,274 @@ def test_vulnerabilities_non_fixable_filter(self, mock_get_scan):
108108
self.assertFalse(args["fixable"])
109109

110110

111+
# ---------------------------------------------------------------------------
112+
# Helpers shared by repo-summary tests
113+
# ---------------------------------------------------------------------------
114+
115+
116+
def _pkg_dict(slug_perm, name, version="1.0.0"):
117+
return {"slug_perm": slug_perm, "name": name, "version": version}
118+
119+
120+
def _page_info(page=1, page_total=1):
121+
pi = MagicMock()
122+
pi.page = page
123+
pi.page_total = page_total
124+
return pi
125+
126+
127+
def _scan_data_vulnerable(name, version, severities):
128+
"""Scan result with at least one vulnerability."""
129+
data = MagicMock()
130+
data.package.name = name
131+
data.package.version = version
132+
scan = MagicMock()
133+
scan.results = [MagicMock(severity=s) for s in severities]
134+
data.scans = [scan]
135+
return data
136+
137+
138+
def _scan_data_safe(name, version):
139+
"""Scan result with no vulnerabilities (package was scanned, nothing found)."""
140+
data = MagicMock()
141+
data.package.name = name
142+
data.package.version = version
143+
scan = MagicMock()
144+
scan.results = []
145+
data.scans = [scan]
146+
return data
147+
148+
149+
def _scan_data_no_scan():
150+
"""No scan data available (package not yet scanned or unsupported format)."""
151+
data = MagicMock()
152+
data.scans = []
153+
return data
154+
155+
156+
# ---------------------------------------------------------------------------
157+
# Repo-level summary tests (OWNER/REPO, no package slug)
158+
# ---------------------------------------------------------------------------
159+
160+
161+
class TestRepoSummaryMode(unittest.TestCase):
162+
def setUp(self):
163+
self.runner = CliRunner()
164+
165+
# --show-assessment is rejected for repo-level summary ──────────────────
166+
167+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
168+
def test_show_assessment_rejected(self, mock_list):
169+
"""--show-assessment with OWNER/REPO prints a warning and exits cleanly."""
170+
result = self.runner.invoke(
171+
vulnerabilities, ["testorg/testrepo", "--show-assessment"]
172+
)
173+
174+
self.assertEqual(result.exit_code, 0)
175+
self.assertIn("not supported", result.output)
176+
mock_list.assert_not_called()
177+
178+
# Single-package scenarios ───────────────────────────────────────────────
179+
180+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
181+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
182+
def test_single_vulnerable_package(self, mock_list, mock_scan):
183+
"""A repo with one vulnerable package produces a summary table."""
184+
mock_list.return_value = ([_pkg_dict("slug-abc", "my-lib")], _page_info())
185+
mock_scan.return_value = _scan_data_vulnerable(
186+
"my-lib", "1.0.0", ["critical", "high"]
187+
)
188+
189+
result = self.runner.invoke(vulnerabilities, ["testorg/testrepo"])
190+
191+
self.assertEqual(result.exit_code, 0)
192+
mock_scan.assert_called_once()
193+
scan_args = mock_scan.call_args[1]
194+
self.assertEqual(scan_args["owner"], "testorg")
195+
self.assertEqual(scan_args["repo"], "testrepo")
196+
self.assertEqual(scan_args["package"], "slug-abc")
197+
198+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
199+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
200+
def test_single_safe_package(self, mock_list, mock_scan):
201+
"""A scanned package with no vulnerabilities is included in the summary."""
202+
mock_list.return_value = ([_pkg_dict("slug-safe", "clean-lib")], _page_info())
203+
mock_scan.return_value = _scan_data_safe("clean-lib", "2.0.0")
204+
205+
result = self.runner.invoke(vulnerabilities, ["testorg/testrepo"])
206+
207+
self.assertEqual(result.exit_code, 0)
208+
mock_scan.assert_called_once()
209+
210+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
211+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
212+
def test_single_no_scan_package(self, mock_list, mock_scan):
213+
"""A package with no scan data (unsupported format) is included with no-scan status."""
214+
mock_list.return_value = ([_pkg_dict("slug-bin", "binary-pkg")], _page_info())
215+
mock_scan.return_value = _scan_data_no_scan()
216+
217+
result = self.runner.invoke(vulnerabilities, ["testorg/testrepo"])
218+
219+
self.assertEqual(result.exit_code, 0)
220+
mock_scan.assert_called_once()
221+
222+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
223+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
224+
def test_scan_fetch_exception_treated_as_no_scan(self, mock_list, mock_scan):
225+
"""A package whose scan fetch raises is included with no-scan status, not dropped."""
226+
mock_list.return_value = ([_pkg_dict("slug-err", "error-pkg")], _page_info())
227+
mock_scan.side_effect = Exception("connection refused")
228+
229+
result = self.runner.invoke(vulnerabilities, ["testorg/testrepo"])
230+
231+
# Command should still exit cleanly — the package appears as no-scan
232+
self.assertEqual(result.exit_code, 0)
233+
234+
# Multiple packages ──────────────────────────────────────────────────────
235+
236+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
237+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
238+
def test_multiple_packages_mixed_statuses(self, mock_list, mock_scan):
239+
"""Three packages with different statuses are all fetched and summarised."""
240+
packages = [
241+
_pkg_dict("slug-vuln", "vuln-lib"),
242+
_pkg_dict("slug-safe", "safe-lib"),
243+
_pkg_dict("slug-none", "unsupported-pkg"),
244+
]
245+
mock_list.return_value = (packages, _page_info())
246+
mock_scan.side_effect = [
247+
_scan_data_vulnerable("vuln-lib", "1.0.0", ["critical"]),
248+
_scan_data_safe("safe-lib", "1.0.0"),
249+
_scan_data_no_scan(),
250+
]
251+
252+
result = self.runner.invoke(vulnerabilities, ["testorg/testrepo"])
253+
254+
self.assertEqual(result.exit_code, 0)
255+
self.assertEqual(mock_scan.call_count, 3)
256+
257+
# Pagination ─────────────────────────────────────────────────────────────
258+
259+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
260+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
261+
def test_pagination_fetches_all_pages(self, mock_list, mock_scan):
262+
"""Packages spanning two pages are all fetched."""
263+
page1_pkgs = [_pkg_dict("slug-a", "pkg-a"), _pkg_dict("slug-b", "pkg-b")]
264+
page2_pkgs = [_pkg_dict("slug-c", "pkg-c")]
265+
mock_list.side_effect = [
266+
(page1_pkgs, _page_info(page=1, page_total=2)),
267+
(page2_pkgs, _page_info(page=2, page_total=2)),
268+
]
269+
mock_scan.return_value = _scan_data_safe("pkg", "1.0.0")
270+
271+
result = self.runner.invoke(vulnerabilities, ["testorg/testrepo"])
272+
273+
self.assertEqual(result.exit_code, 0)
274+
self.assertEqual(mock_list.call_count, 2)
275+
self.assertEqual(mock_scan.call_count, 3)
276+
277+
# Verify page numbers were incremented correctly
278+
page_numbers = [c[1]["page"] for c in mock_list.call_args_list]
279+
self.assertEqual(page_numbers, [1, 2])
280+
281+
# Empty repo ─────────────────────────────────────────────────────────────
282+
283+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
284+
def test_empty_repo_exits_with_error(self, mock_list):
285+
"""An empty repository raises a ClickException with a helpful message."""
286+
mock_list.return_value = ([], None)
287+
288+
result = self.runner.invoke(vulnerabilities, ["testorg/testrepo"])
289+
290+
self.assertNotEqual(result.exit_code, 0)
291+
292+
# --severity filter ──────────────────────────────────────────────────────
293+
294+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
295+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
296+
def test_severity_filter_passed_to_scan(self, mock_list, mock_scan):
297+
"""--severity is forwarded to get_package_scan_result."""
298+
mock_list.return_value = ([_pkg_dict("slug-a", "pkg-a")], _page_info())
299+
mock_scan.return_value = _scan_data_vulnerable("pkg-a", "1.0.0", ["critical"])
300+
301+
result = self.runner.invoke(
302+
vulnerabilities, ["testorg/testrepo", "--severity", "CRITICAL"]
303+
)
304+
305+
self.assertEqual(result.exit_code, 0)
306+
scan_args = mock_scan.call_args[1]
307+
self.assertEqual(scan_args["severity_filter"], "CRITICAL")
308+
309+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
310+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
311+
def test_severity_filter_no_matches_shows_message(self, mock_list, mock_scan):
312+
"""When --severity matches nothing, a descriptive message is shown."""
313+
mock_list.return_value = ([_pkg_dict("slug-a", "pkg-a")], _page_info())
314+
# Safe package — no critical vulnerabilities
315+
mock_scan.return_value = _scan_data_safe("pkg-a", "1.0.0")
316+
317+
result = self.runner.invoke(
318+
vulnerabilities, ["testorg/testrepo", "--severity", "CRITICAL"]
319+
)
320+
321+
self.assertEqual(result.exit_code, 0)
322+
self.assertIn("No packages found matching filter", result.output)
323+
self.assertIn("CRITICAL", result.output)
324+
325+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
326+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
327+
def test_severity_filter_excludes_safe_and_no_scan(self, mock_list, mock_scan):
328+
"""With --severity active, safe and no-scan packages are not included in results."""
329+
packages = [
330+
_pkg_dict("slug-vuln", "vuln-lib"),
331+
_pkg_dict("slug-safe", "safe-lib"),
332+
]
333+
mock_list.return_value = (packages, _page_info())
334+
mock_scan.side_effect = [
335+
_scan_data_vulnerable("vuln-lib", "1.0.0", ["critical"]),
336+
_scan_data_safe("safe-lib", "1.0.0"),
337+
]
338+
339+
# Patch the table printer so we can inspect what rows were passed
340+
with patch(
341+
"cloudsmith_cli.cli.commands.vulnerabilities._print_repo_summary_table"
342+
) as mock_table:
343+
result = self.runner.invoke(
344+
vulnerabilities, ["testorg/testrepo", "--severity", "CRITICAL"]
345+
)
346+
347+
self.assertEqual(result.exit_code, 0)
348+
passed_rows = mock_table.call_args[0][0]
349+
statuses = [row[3] for row in passed_rows]
350+
self.assertIn("vulnerable", statuses)
351+
self.assertNotIn("safe", statuses)
352+
self.assertNotIn("no_issues_found", statuses)
353+
self.assertNotIn("no_scan", statuses)
354+
355+
# JSON output ────────────────────────────────────────────────────────────
356+
357+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.get_package_scan_result")
358+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.list_packages")
359+
@patch("cloudsmith_cli.cli.commands.vulnerabilities.utils")
360+
def test_json_output_includes_status_field(self, mock_utils, mock_list, mock_scan):
361+
"""JSON output includes slug_perm, package, status, and vulnerabilities per package."""
362+
mock_list.return_value = ([_pkg_dict("slug-abc", "my-lib")], _page_info())
363+
mock_scan.return_value = _scan_data_vulnerable("my-lib", "1.0.0", ["high"])
364+
mock_utils.should_use_stderr.return_value = False
365+
mock_utils.maybe_print_as_json.return_value = True # pretend JSON was printed
366+
367+
result = self.runner.invoke(vulnerabilities, ["testorg/testrepo"])
368+
369+
self.assertEqual(result.exit_code, 0)
370+
json_payload = mock_utils.maybe_print_as_json.call_args[0][1]
371+
self.assertEqual(json_payload["owner"], "testorg")
372+
self.assertEqual(json_payload["repository"], "testrepo")
373+
self.assertEqual(len(json_payload["packages"]), 1)
374+
pkg = json_payload["packages"][0]
375+
self.assertEqual(pkg["slug_perm"], "slug-abc")
376+
self.assertIn("status", pkg)
377+
self.assertIn("vulnerabilities", pkg)
378+
379+
111380
if __name__ == "__main__":
112381
unittest.main()

0 commit comments

Comments
 (0)