Skip to content

Commit 460e1b8

Browse files
committed
Merge remote-tracking branch 'origin/main' into lelia/sdk-purl-post-bug
# Conflicts: # pyproject.toml # socketdev/version.py # uv.lock
2 parents 7bdb34c + 2ae50f4 commit 460e1b8

2 files changed

Lines changed: 114 additions & 4 deletions

File tree

socketdev/diffscans/__init__.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,45 @@ def list(self, org_slug: str, params: Optional[Dict[str, Any]] = None) -> dict:
2121
log.error(f"Error listing diff scans: {response.status_code}, message: {response.text}")
2222
return {}
2323

24-
def get(self, org_slug: str, diff_scan_id: str) -> dict:
25-
"""Fetch a diff scan by ID."""
24+
def get(self, org_slug: str, diff_scan_id: str, params: Optional[Dict[str, Any]] = None) -> dict:
25+
"""Fetch a diff scan by ID.
26+
27+
Args:
28+
org_slug: Organization slug
29+
diff_scan_id: The ID of the diff scan to fetch
30+
params: Optional query parameters. Supports:
31+
cached: When "true", return pre-computed results immediately
32+
(200) or a processing status (202) instead of holding
33+
the connection open while the diff is computed.
34+
omit_unchanged: When "true", omit unchanged artifacts.
35+
omit_license_details: When "true", omit license details.
36+
37+
Returns:
38+
dict: On 200, the API response containing the diff_scan object.
39+
On 202 (results still processing when cached=true), a dict of
40+
{"status": "processing", "id": diff_scan_id} so callers can
41+
poll until the diff scan is ready. Empty dict on error.
42+
"""
43+
import urllib.parse
2644
path = f"orgs/{org_slug}/diff-scans/{diff_scan_id}"
45+
if params:
46+
path += "?" + urllib.parse.urlencode(params, doseq=True)
2747
response = self.api.do_request(path=path, method="GET")
2848
if response.status_code == 200:
2949
return response.json()
50+
if response.status_code == 202:
51+
result = {}
52+
try:
53+
body = response.json()
54+
if isinstance(body, dict):
55+
result.update(body)
56+
except ValueError:
57+
pass
58+
# HTTP 202 always means the requested diff is still processing.
59+
# Keep any additional response fields, but make the polling
60+
# sentinel and requested resource ID authoritative for callers.
61+
result.update({"status": "processing", "id": diff_scan_id})
62+
return result
3063
log.error(f"Error fetching diff scan: {response.status_code}, message: {response.text}")
3164
return {}
3265

@@ -61,7 +94,8 @@ def create_from_repo(self, org_slug: str, repo_slug: str, files: list, params: O
6194
import urllib.parse
6295
path = f"orgs/{org_slug}/diff-scans/from-repo/{repo_slug}"
6396
if params:
64-
path += "?" + urllib.parse.urlencode(params)
97+
# doseq=True so list values (e.g. committers) become repeated params
98+
path += "?" + urllib.parse.urlencode(params, doseq=True)
6599

66100
# Use lazy loading if requested
67101
if use_lazy_loading:
@@ -80,7 +114,7 @@ def create_from_ids(self, org_slug: str, params: Dict[str, Any]) -> dict:
80114
import urllib.parse
81115
path = f"orgs/{org_slug}/diff-scans/from-ids"
82116
if params:
83-
path += "?" + urllib.parse.urlencode(params)
117+
path += "?" + urllib.parse.urlencode(params, doseq=True)
84118
response = self.api.do_request(path=path, method="POST")
85119
if response.status_code in (200, 201):
86120
return response.json()

tests/unit/test_all_endpoints_unit.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,59 @@ def test_diffscans_get_unit(self):
113113
self.assertEqual(call_args[0][0], "GET")
114114
self.assertIn("/orgs/test-org/diff-scans/diff-123", call_args[0][1])
115115

116+
def test_diffscans_get_cached_unit(self):
117+
"""Test diffscans get passes cached/omit params through as a query string."""
118+
expected_data = {
119+
"diff_scan": {
120+
"id": "diff-123",
121+
"artifacts": {"added": [], "removed": [], "unchanged": [], "replaced": [], "updated": []},
122+
}
123+
}
124+
self._mock_response(expected_data)
125+
126+
result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"})
127+
128+
self.assertEqual(result, expected_data)
129+
call_args = self.mock_requests.request.call_args
130+
self.assertEqual(call_args[0][0], "GET")
131+
self.assertIn("/orgs/test-org/diff-scans/diff-123?cached=true", call_args[0][1])
132+
133+
def test_diffscans_get_processing_unit(self):
134+
"""Test diffscans get surfaces 202 processing status instead of an error."""
135+
self._mock_response({"status": "processing", "id": "diff-123"}, 202)
136+
137+
result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"})
138+
139+
self.assertEqual(result.get("status"), "processing")
140+
self.assertEqual(result.get("id"), "diff-123")
141+
142+
def test_diffscans_get_processing_empty_body_unit(self):
143+
"""Test diffscans get synthesizes the processing status when the 202 body is empty."""
144+
mock_response = Mock()
145+
mock_response.status_code = 202
146+
mock_response.headers = {}
147+
mock_response.json.side_effect = ValueError("no body")
148+
mock_response.text = ""
149+
self.mock_requests.request.return_value = mock_response
150+
151+
result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"})
152+
153+
self.assertEqual(result, {"status": "processing", "id": "diff-123"})
154+
155+
def test_diffscans_get_processing_sentinel_wins_unit(self):
156+
"""Test a 202 body cannot override the SDK's polling sentinel or requested ID."""
157+
self._mock_response(
158+
{"status": "pending", "id": "wrong-id", "retry_after": 5},
159+
202,
160+
)
161+
162+
result = self.sdk.diffscans.get("test-org", "diff-123", params={"cached": "true"})
163+
164+
self.assertEqual(
165+
result,
166+
{"status": "processing", "id": "diff-123", "retry_after": 5},
167+
)
168+
116169
def test_diffscans_create_from_ids_unit(self):
117170
"""Test diffscans creation from scan IDs."""
118171
expected_data = {"id": "new-diff-scan", "status": "queued"}
@@ -153,6 +206,29 @@ def test_diffscans_create_from_repo_unit(self):
153206
finally:
154207
os.unlink(f.name)
155208

209+
def test_diffscans_create_from_repo_committers_list_unit(self):
210+
"""Test list-valued params (committers) encode as repeated query params."""
211+
self._mock_response({"id": "repo-diff-scan"}, 201)
212+
213+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
214+
json.dump({"name": "test", "version": "1.0.0"}, f)
215+
f.flush()
216+
217+
try:
218+
with open(f.name, "rb") as file_obj:
219+
files = [("file", ("package.json", file_obj))]
220+
params = {"committers": ["alice", "bob"], "branch": "main"}
221+
self.sdk.diffscans.create_from_repo("test-org", "test-repo", files, params)
222+
223+
call_args = self.mock_requests.request.call_args
224+
url = call_args[0][1]
225+
self.assertIn("committers=alice", url)
226+
self.assertIn("committers=bob", url)
227+
self.assertIn("branch=main", url)
228+
229+
finally:
230+
os.unlink(f.name)
231+
156232
def test_diffscans_gfm_unit(self):
157233
"""Test diffscans GitHub Flavored Markdown export."""
158234
expected_data = {"markdown": "# Diff Report\n\n## Summary\n- Added: 0\n- Removed: 0"}

0 commit comments

Comments
 (0)