diff --git a/pyproject.toml b/pyproject.toml index 609daa2..43db636 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "socketdev" -version = "3.3.0" +version = "3.4.0" requires-python = ">= 3.9" dependencies = [ 'requests', diff --git a/socketdev/diffscans/__init__.py b/socketdev/diffscans/__init__.py index 28075f2..8953275 100644 --- a/socketdev/diffscans/__init__.py +++ b/socketdev/diffscans/__init__.py @@ -21,12 +21,41 @@ def list(self, org_slug: str, params: Optional[Dict[str, Any]] = None) -> dict: log.error(f"Error listing diff scans: {response.status_code}, message: {response.text}") return {} - def get(self, org_slug: str, diff_scan_id: str) -> dict: - """Fetch a diff scan by ID.""" + def get(self, org_slug: str, diff_scan_id: str, params: Optional[Dict[str, Any]] = None) -> dict: + """Fetch a diff scan by ID. + + Args: + org_slug: Organization slug + diff_scan_id: The ID of the diff scan to fetch + params: Optional query parameters. Supports: + cached: When "true", return pre-computed results immediately + (200) or a processing status (202) instead of holding + the connection open while the diff is computed. + omit_unchanged: When "true", omit unchanged artifacts. + omit_license_details: When "true", omit license details. + + Returns: + dict: On 200, the API response containing the diff_scan object. + On 202 (results still processing when cached=true), a dict of + {"status": "processing", "id": diff_scan_id} so callers can + poll until the diff scan is ready. Empty dict on error. + """ + import urllib.parse path = f"orgs/{org_slug}/diff-scans/{diff_scan_id}" + if params: + path += "?" + urllib.parse.urlencode(params, doseq=True) response = self.api.do_request(path=path, method="GET") if response.status_code == 200: return response.json() + if response.status_code == 202: + result = {"status": "processing", "id": diff_scan_id} + try: + body = response.json() + if isinstance(body, dict): + result.update(body) + except ValueError: + pass + return result log.error(f"Error fetching diff scan: {response.status_code}, message: {response.text}") return {} @@ -61,7 +90,8 @@ def create_from_repo(self, org_slug: str, repo_slug: str, files: list, params: O import urllib.parse path = f"orgs/{org_slug}/diff-scans/from-repo/{repo_slug}" if params: - path += "?" + urllib.parse.urlencode(params) + # doseq=True so list values (e.g. committers) become repeated params + path += "?" + urllib.parse.urlencode(params, doseq=True) # Use lazy loading if requested if use_lazy_loading: @@ -80,7 +110,7 @@ def create_from_ids(self, org_slug: str, params: Dict[str, Any]) -> dict: import urllib.parse path = f"orgs/{org_slug}/diff-scans/from-ids" if params: - path += "?" + urllib.parse.urlencode(params) + path += "?" + urllib.parse.urlencode(params, doseq=True) response = self.api.do_request(path=path, method="POST") if response.status_code in (200, 201): return response.json() diff --git a/socketdev/version.py b/socketdev/version.py index 88c513e..903a158 100644 --- a/socketdev/version.py +++ b/socketdev/version.py @@ -1 +1 @@ -__version__ = "3.3.0" +__version__ = "3.4.0" diff --git a/tests/unit/test_all_endpoints_unit.py b/tests/unit/test_all_endpoints_unit.py index 64895ee..545c008 100644 --- a/tests/unit/test_all_endpoints_unit.py +++ b/tests/unit/test_all_endpoints_unit.py @@ -113,6 +113,45 @@ def test_diffscans_get_unit(self): self.assertEqual(call_args[0][0], "GET") self.assertIn("/orgs/test-org/diff-scans/diff-123", call_args[0][1]) + def test_diffscans_get_cached_unit(self): + """Test diffscans get passes cached/omit params through as a query string.""" + expected_data = { + "diff_scan": { + "id": "diff-123", + "artifacts": {"added": [], "removed": [], "unchanged": [], "replaced": [], "updated": []}, + } + } + self._mock_response(expected_data) + + result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"}) + + self.assertEqual(result, expected_data) + call_args = self.mock_requests.request.call_args + self.assertEqual(call_args[0][0], "GET") + self.assertIn("/orgs/test-org/diff-scans/diff-123?cached=true", call_args[0][1]) + + def test_diffscans_get_processing_unit(self): + """Test diffscans get surfaces 202 processing status instead of an error.""" + self._mock_response({"status": "processing", "id": "diff-123"}, 202) + + result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"}) + + self.assertEqual(result.get("status"), "processing") + self.assertEqual(result.get("id"), "diff-123") + + def test_diffscans_get_processing_empty_body_unit(self): + """Test diffscans get synthesizes the processing status when the 202 body is empty.""" + mock_response = Mock() + mock_response.status_code = 202 + mock_response.headers = {} + mock_response.json.side_effect = ValueError("no body") + mock_response.text = "" + self.mock_requests.request.return_value = mock_response + + result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"}) + + self.assertEqual(result, {"status": "processing", "id": "diff-123"}) + def test_diffscans_create_from_ids_unit(self): """Test diffscans creation from scan IDs.""" expected_data = {"id": "new-diff-scan", "status": "queued"} @@ -153,6 +192,29 @@ def test_diffscans_create_from_repo_unit(self): finally: os.unlink(f.name) + def test_diffscans_create_from_repo_committers_list_unit(self): + """Test list-valued params (committers) encode as repeated query params.""" + self._mock_response({"id": "repo-diff-scan"}, 201) + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json.dump({"name": "test", "version": "1.0.0"}, f) + f.flush() + + try: + with open(f.name, "rb") as file_obj: + files = [("file", ("package.json", file_obj))] + params = {"committers": ["alice", "bob"], "branch": "main"} + self.sdk.diffscans.create_from_repo("test-org", "test-repo", files, params) + + call_args = self.mock_requests.request.call_args + url = call_args[0][1] + self.assertIn("committers=alice", url) + self.assertIn("committers=bob", url) + self.assertIn("branch=main", url) + + finally: + os.unlink(f.name) + def test_diffscans_gfm_unit(self): """Test diffscans GitHub Flavored Markdown export.""" expected_data = {"markdown": "# Diff Report\n\n## Summary\n- Added: 0\n- Removed: 0"} diff --git a/uv.lock b/uv.lock index 7397d8f..92acd1c 100644 --- a/uv.lock +++ b/uv.lock @@ -1353,7 +1353,7 @@ wheels = [ [[package]] name = "socketdev" -version = "3.3.0" +version = "3.4.0" source = { editable = "." } dependencies = [ { name = "requests" },