Skip to content

Commit b0cb580

Browse files
AIML-644: Migrate credit tracking to org-level endpoint
Add get_credit_tracking_org() using the org-scoped URL (/organizations/{orgId}/credit-tracking) and migrate all four get_credit_tracking() call sites in main.py to use it, removing the CONTRAST_APP_ID dependency. The org-level route already exists in the backend (RemediationCreditTrackingController). Add tests covering the new function's success, URL shape, and error handling. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 77a23aa commit b0cb580

3 files changed

Lines changed: 124 additions & 8 deletions

File tree

src/contrast_api.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,4 +1139,51 @@ def get_credit_tracking(contrast_host: str, contrast_org_id: str, contrast_app_i
11391139
debug_log(f"Unexpected error calling credit-tracking API: {e}")
11401140
return None
11411141

1142+
1143+
def get_credit_tracking_org(contrast_host: str, contrast_org_id: str, contrast_auth_key: str, contrast_api_key: str) -> Optional[CreditTrackingResponse]:
1144+
"""Get credit tracking information from the org-level Contrast API endpoint.
1145+
1146+
Args:
1147+
contrast_host: The Contrast Security host URL.
1148+
contrast_org_id: The organization ID.
1149+
contrast_auth_key: The Contrast authorization key.
1150+
contrast_api_key: The Contrast API key.
1151+
1152+
Returns:
1153+
CreditTrackingResponse object if successful, None if failed.
1154+
"""
1155+
api_url = f"https://{normalize_host(contrast_host)}/api/v4/aiml-remediation/organizations/{contrast_org_id}/credit-tracking"
1156+
1157+
headers = {
1158+
"Authorization": contrast_auth_key,
1159+
"API-Key": contrast_api_key,
1160+
"Content-Type": "application/json",
1161+
"Accept": "application/json",
1162+
"User-Agent": config.USER_AGENT
1163+
}
1164+
1165+
try:
1166+
debug_log(f"Fetching org-level credit tracking from: {api_url}")
1167+
response = requests.get(api_url, headers=headers, timeout=30)
1168+
response.raise_for_status()
1169+
1170+
debug_log(f"Org-level credit tracking API response status code: {response.status_code}")
1171+
debug_log(f"Raw org-level credit tracking response: {response.text}")
1172+
1173+
data = response.json()
1174+
return CreditTrackingResponse.from_api_response(data)
1175+
1176+
except requests.exceptions.HTTPError as e:
1177+
debug_log(f"HTTP error fetching org-level credit tracking: {e.response.status_code} - {e.response.text}")
1178+
return None
1179+
except requests.exceptions.RequestException as e:
1180+
debug_log(f"Request error fetching org-level credit tracking: {e}")
1181+
return None
1182+
except json.JSONDecodeError:
1183+
debug_log("Error decoding JSON response from org-level credit-tracking API.")
1184+
return None
1185+
except Exception as e:
1186+
debug_log(f"Unexpected error calling org-level credit-tracking API: {e}")
1187+
return None
1188+
11421189
# %%

src/main.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,9 @@ def _main_impl(vuln_count): # noqa: C901
158158

159159
# Log initial credit tracking status if using Contrast LLM (only for SMARTFIX agent)
160160
if config.CODING_AGENT == CodingAgents.SMARTFIX.name and config.USE_CONTRAST_LLM:
161-
initial_credit_info = contrast_api.get_credit_tracking(
161+
initial_credit_info = contrast_api.get_credit_tracking_org(
162162
contrast_host=config.CONTRAST_HOST,
163163
contrast_org_id=config.CONTRAST_ORG_ID,
164-
contrast_app_id=config.CONTRAST_APP_ID,
165164
contrast_auth_key=config.CONTRAST_AUTHORIZATION_KEY,
166165
contrast_api_key=config.CONTRAST_API_KEY
167166
)
@@ -209,10 +208,9 @@ def _main_impl(vuln_count): # noqa: C901
209208

210209
# Check credit exhaustion for Contrast LLM usage
211210
if config.USE_CONTRAST_LLM:
212-
current_credit_info = contrast_api.get_credit_tracking(
211+
current_credit_info = contrast_api.get_credit_tracking_org(
213212
contrast_host=config.CONTRAST_HOST,
214213
contrast_org_id=config.CONTRAST_ORG_ID,
215-
contrast_app_id=config.CONTRAST_APP_ID,
216214
contrast_auth_key=config.CONTRAST_AUTHORIZATION_KEY,
217215
contrast_api_key=config.CONTRAST_API_KEY
218216
)
@@ -513,10 +511,9 @@ def _main_impl(vuln_count): # noqa: C901
513511

514512
# Append credit tracking information to PR body if using Contrast LLM
515513
if config.CODING_AGENT == CodingAgents.SMARTFIX.name and config.USE_CONTRAST_LLM:
516-
current_credit_info = contrast_api.get_credit_tracking(
514+
current_credit_info = contrast_api.get_credit_tracking_org(
517515
contrast_host=config.CONTRAST_HOST,
518516
contrast_org_id=config.CONTRAST_ORG_ID,
519-
contrast_app_id=config.CONTRAST_APP_ID,
520517
contrast_auth_key=config.CONTRAST_AUTHORIZATION_KEY,
521518
contrast_api_key=config.CONTRAST_API_KEY
522519
)
@@ -599,10 +596,9 @@ def _main_impl(vuln_count): # noqa: C901
599596

600597
# Log updated credit tracking status after PR notification (only for SMARTFIX agent)
601598
if config.CODING_AGENT == CodingAgents.SMARTFIX.name and config.USE_CONTRAST_LLM:
602-
updated_credit_info = contrast_api.get_credit_tracking(
599+
updated_credit_info = contrast_api.get_credit_tracking_org(
603600
contrast_host=config.CONTRAST_HOST,
604601
contrast_org_id=config.CONTRAST_ORG_ID,
605-
contrast_app_id=config.CONTRAST_APP_ID,
606602
contrast_auth_key=config.CONTRAST_AUTHORIZATION_KEY,
607603
contrast_api_key=config.CONTRAST_API_KEY
608604
)

test/contrast_api/test_contrast_api_credit_tracking.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,5 +260,78 @@ def test_get_credit_tracking_uses_bearer_authorization(self, mock_get):
260260
self.assertEqual(headers['Authorization'], 'test-auth-key')
261261

262262

263+
class TestContrastApiCreditTrackingOrg(unittest.TestCase):
264+
"""Test cases for org-level credit tracking (no app_id) in contrast_api module."""
265+
266+
def setUp(self):
267+
self.sample_api_response = {
268+
"organizationId": "12345678-1234-1234-1234-123456789abc",
269+
"enabled": True,
270+
"maxCredits": 50,
271+
"creditsUsed": 7,
272+
"startDate": "2024-10-01T14:30:00Z",
273+
"endDate": "2024-11-12T14:30:00Z"
274+
}
275+
276+
@patch('src.contrast_api.requests.get')
277+
def test_get_credit_tracking_org_returns_valid_response(self, mock_get):
278+
"""Test that org-level endpoint returns properly structured CreditTrackingResponse."""
279+
mock_response = MagicMock()
280+
mock_response.status_code = 200
281+
mock_response.text = json.dumps(self.sample_api_response)
282+
mock_response.json.return_value = self.sample_api_response
283+
mock_get.return_value = mock_response
284+
285+
result = contrast_api.get_credit_tracking_org(
286+
contrast_host="test.contrastsecurity.com",
287+
contrast_org_id="test-org-id",
288+
contrast_auth_key="test-auth-key",
289+
contrast_api_key="test-api-key"
290+
)
291+
292+
self.assertIsInstance(result, CreditTrackingResponse)
293+
self.assertTrue(result.enabled)
294+
295+
@patch('src.contrast_api.requests.get')
296+
def test_get_credit_tracking_org_url_has_no_app_id(self, mock_get):
297+
"""Test that the org-level endpoint URL does not include an application ID."""
298+
mock_response = MagicMock()
299+
mock_response.status_code = 200
300+
mock_response.text = json.dumps(self.sample_api_response)
301+
mock_response.json.return_value = self.sample_api_response
302+
mock_get.return_value = mock_response
303+
304+
contrast_api.get_credit_tracking_org(
305+
contrast_host="test.contrastsecurity.com",
306+
contrast_org_id="test-org-id",
307+
contrast_auth_key="test-auth-key",
308+
contrast_api_key="test-api-key"
309+
)
310+
311+
called_url = mock_get.call_args[0][0]
312+
self.assertIn("/organizations/test-org-id/credit-tracking", called_url)
313+
self.assertNotIn("/applications/", called_url)
314+
315+
@patch('src.contrast_api.requests.get')
316+
def test_get_credit_tracking_org_returns_none_on_http_error(self, mock_get):
317+
"""Test that org-level endpoint returns None on HTTP errors."""
318+
mock_response = MagicMock()
319+
mock_response.status_code = 404
320+
mock_response.text = "Not Found"
321+
mock_get.return_value = mock_response
322+
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
323+
response=mock_response
324+
)
325+
326+
result = contrast_api.get_credit_tracking_org(
327+
contrast_host="test.contrastsecurity.com",
328+
contrast_org_id="test-org-id",
329+
contrast_auth_key="test-auth-key",
330+
contrast_api_key="test-api-key"
331+
)
332+
333+
self.assertIsNone(result)
334+
335+
263336
if __name__ == '__main__':
264337
unittest.main()

0 commit comments

Comments
 (0)