feat(opax-mcp): add internal Cloud Run v2 fetch helper
Some checks are pending
Check Python Version Consistency / Check Python Version (push) Waiting to run
Some checks are pending
Check Python Version Consistency / Check Python Version (push) Waiting to run
- Add a private helper that performs an authenticated, read-only GET for the configured opax-mcp Cloud Run v2 service. - Return only generation, observedGeneration, and updateTime on success, with sanitized reason codes for token, HTTP, network, timeout, and parsing errors. - Add mocked unit coverage for successful responses and handled failure paths.
This commit is contained in:
parent
c3fde1e38c
commit
f0039088c2
|
|
@ -116,6 +116,64 @@ def _sanitize_opax_deployment_status(service_data: dict) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_opax_deployment_service_data() -> dict:
|
||||||
|
"""
|
||||||
|
Fetches the raw Cloud Run v2 service object for OPAX-MCP, returning
|
||||||
|
only a minimal subset of fields or a sanitized failure reason.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = _get_access_token()
|
||||||
|
if not token:
|
||||||
|
return {"reason_code": "ADC_TOKEN_UNAVAILABLE"}
|
||||||
|
|
||||||
|
endpoint_url = (
|
||||||
|
f"https://run.googleapis.com/v2/projects/{PROJECT_ID}/"
|
||||||
|
f"locations/{_OPAX_MCP_REGION}/services/{_OPAX_MCP_SERVICE}"
|
||||||
|
)
|
||||||
|
req = urllib.request.Request(
|
||||||
|
endpoint_url, headers={"Authorization": f"Bearer {token}"}, method="GET"
|
||||||
|
)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
status = getattr(resp, "status", None)
|
||||||
|
if not isinstance(status, int):
|
||||||
|
return {"reason_code": "UNEXPECTED_LOCAL_FAILURE"}
|
||||||
|
if not (200 <= status < 300):
|
||||||
|
return {"reason_code": "UNEXPECTED_NON_2XX_STATUS"}
|
||||||
|
|
||||||
|
body_bytes = resp.read()
|
||||||
|
|
||||||
|
body = json.loads(body_bytes)
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
return {"reason_code": "INVALID_RESPONSE_SHAPE"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"generation": body.get("generation"),
|
||||||
|
"observedGeneration": body.get("observedGeneration"),
|
||||||
|
"updateTime": body.get("updateTime"),
|
||||||
|
}
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"reason_code": "INVALID_JSON_RESPONSE"}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code in (401, 403):
|
||||||
|
return {"reason_code": "CLOUD_RUN_UNAUTHORIZED"}
|
||||||
|
if e.code == 404:
|
||||||
|
return {"reason_code": "CLOUD_RUN_NOT_FOUND"}
|
||||||
|
return {"reason_code": "UNEXPECTED_NON_2XX_STATUS"}
|
||||||
|
except socket.timeout:
|
||||||
|
return {"reason_code": "REQUEST_TIMEOUT"}
|
||||||
|
except urllib.error.URLError:
|
||||||
|
return {"reason_code": "NETWORK_FAILURE"}
|
||||||
|
except Exception:
|
||||||
|
return {"reason_code": "UNEXPECTED_LOCAL_FAILURE"}
|
||||||
|
|
||||||
|
|
||||||
class PushStaticRequest(BaseModel):
|
class PushStaticRequest(BaseModel):
|
||||||
file_path: str
|
file_path: str
|
||||||
content: str
|
content: str
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import unittest
|
import unittest
|
||||||
from agents.mcp_server.server import _sanitize_opax_deployment_status
|
import json
|
||||||
|
import socket
|
||||||
|
import urllib.error
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from agents.mcp_server.server import (
|
||||||
|
_fetch_opax_deployment_service_data,
|
||||||
|
_sanitize_opax_deployment_status,
|
||||||
|
)
|
||||||
|
|
||||||
class TestSanitizeOpaxDeploymentStatus(unittest.TestCase):
|
class TestSanitizeOpaxDeploymentStatus(unittest.TestCase):
|
||||||
|
|
||||||
|
|
@ -69,5 +76,127 @@ class TestSanitizeOpaxDeploymentStatus(unittest.TestCase):
|
||||||
self.assertEqual(result["region"], "us-central1")
|
self.assertEqual(result["region"], "us-central1")
|
||||||
self.assertIsNone(result["reason_code"])
|
self.assertIsNone(result["reason_code"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestFetchOpaxDeploymentServiceData(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch('urllib.request.urlopen')
|
||||||
|
@patch('agents.mcp_server.server._get_access_token')
|
||||||
|
def test_success_filters_fields(self, mock_get_token, mock_urlopen):
|
||||||
|
mock_get_token.return_value = "fake-token"
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_response.read.return_value = json.dumps({
|
||||||
|
"generation": 5, "observedGeneration": 5, "updateTime": "T",
|
||||||
|
"uri": "forbidden"
|
||||||
|
}).encode()
|
||||||
|
mock_urlopen.return_value.__enter__.return_value = mock_response
|
||||||
|
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"generation": 5, "observedGeneration": 5, "updateTime": "T"})
|
||||||
|
self.assertEqual(set(result.keys()), {"generation", "observedGeneration", "updateTime"})
|
||||||
|
|
||||||
|
@patch('urllib.request.urlopen')
|
||||||
|
@patch('agents.mcp_server.server._get_access_token')
|
||||||
|
def test_success_missing_fields(self, mock_get_token, mock_urlopen):
|
||||||
|
mock_get_token.return_value = "fake-token"
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_response.read.return_value = json.dumps({"generation": 1}).encode()
|
||||||
|
mock_urlopen.return_value.__enter__.return_value = mock_response
|
||||||
|
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"generation": 1, "observedGeneration": None, "updateTime": None})
|
||||||
|
|
||||||
|
@patch('urllib.request.urlopen')
|
||||||
|
@patch('agents.mcp_server.server._get_access_token')
|
||||||
|
def test_falsy_token_skips_call(self, mock_get_token, mock_urlopen):
|
||||||
|
for token in [None, ""]:
|
||||||
|
with self.subTest(token=token):
|
||||||
|
mock_get_token.return_value = token
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"reason_code": "ADC_TOKEN_UNAVAILABLE"})
|
||||||
|
mock_urlopen.assert_not_called()
|
||||||
|
|
||||||
|
@patch('urllib.request.urlopen')
|
||||||
|
@patch('agents.mcp_server.server._get_access_token')
|
||||||
|
def test_get_token_exception(self, mock_get_token, mock_urlopen):
|
||||||
|
mock_get_token.side_effect = Exception("local failure")
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"reason_code": "UNEXPECTED_LOCAL_FAILURE"})
|
||||||
|
mock_urlopen.assert_not_called()
|
||||||
|
|
||||||
|
@patch('urllib.request.urlopen')
|
||||||
|
@patch('agents.mcp_server.server._get_access_token')
|
||||||
|
def test_absent_response_status(self, mock_get_token, mock_urlopen):
|
||||||
|
mock_get_token.return_value = "fake-token"
|
||||||
|
class MockResp:
|
||||||
|
def __init__(self):
|
||||||
|
self.read_called = False
|
||||||
|
def read(self):
|
||||||
|
self.read_called = True
|
||||||
|
return b""
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
def __exit__(self, *args):
|
||||||
|
return False
|
||||||
|
|
||||||
|
mock_response = MockResp()
|
||||||
|
mock_urlopen.return_value = mock_response
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"reason_code": "UNEXPECTED_LOCAL_FAILURE"})
|
||||||
|
self.assertFalse(mock_response.read_called)
|
||||||
|
|
||||||
|
@patch('urllib.request.urlopen')
|
||||||
|
@patch('agents.mcp_server.server._get_access_token')
|
||||||
|
def test_non_2xx_status_skips_read(self, mock_get_token, mock_urlopen):
|
||||||
|
mock_get_token.return_value = "fake-token"
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status = 503
|
||||||
|
mock_urlopen.return_value.__enter__.return_value = mock_response
|
||||||
|
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"reason_code": "UNEXPECTED_NON_2XX_STATUS"})
|
||||||
|
mock_response.read.assert_not_called()
|
||||||
|
|
||||||
|
@patch('urllib.request.urlopen')
|
||||||
|
@patch('agents.mcp_server.server._get_access_token')
|
||||||
|
def test_http_and_network_errors(self, mock_get_token, mock_urlopen):
|
||||||
|
mock_get_token.return_value = "fake-token"
|
||||||
|
errors_to_reasons = [
|
||||||
|
(urllib.error.HTTPError(None, 401, "", {}, None), "CLOUD_RUN_UNAUTHORIZED"),
|
||||||
|
(urllib.error.HTTPError(None, 403, "", {}, None), "CLOUD_RUN_UNAUTHORIZED"),
|
||||||
|
(urllib.error.HTTPError(None, 404, "", {}, None), "CLOUD_RUN_NOT_FOUND"),
|
||||||
|
(urllib.error.HTTPError(None, 500, "", {}, None), "UNEXPECTED_NON_2XX_STATUS"),
|
||||||
|
(socket.timeout(), "REQUEST_TIMEOUT"),
|
||||||
|
(urllib.error.URLError("DNS failure"), "NETWORK_FAILURE"),
|
||||||
|
]
|
||||||
|
for error, reason in errors_to_reasons:
|
||||||
|
with self.subTest(error=error.__class__.__name__, code=getattr(error, 'code', 'N/A')):
|
||||||
|
mock_urlopen.side_effect = error
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"reason_code": reason})
|
||||||
|
mock_urlopen.side_effect = None
|
||||||
|
|
||||||
|
@patch('urllib.request.urlopen')
|
||||||
|
@patch('agents.mcp_server.server._get_access_token')
|
||||||
|
def test_invalid_json_body(self, mock_get_token, mock_urlopen):
|
||||||
|
mock_get_token.return_value = "fake-token"
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_urlopen.return_value.__enter__.return_value = mock_response
|
||||||
|
|
||||||
|
# Malformed JSON
|
||||||
|
with self.subTest(case="malformed"):
|
||||||
|
mock_response.read.return_value = b'{"key":'
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"reason_code": "INVALID_JSON_RESPONSE"})
|
||||||
|
|
||||||
|
# Valid JSON, but not a dictionary object
|
||||||
|
with self.subTest(case="non-object"):
|
||||||
|
mock_response.read.return_value = b'[1, 2, 3]'
|
||||||
|
result = _fetch_opax_deployment_service_data()
|
||||||
|
self.assertEqual(result, {"reason_code": "INVALID_RESPONSE_SHAPE"})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user