diff --git a/datadog_sync/model/synthetics_tests.py b/datadog_sync/model/synthetics_tests.py index b209c14f..00e5cef7 100644 --- a/datadog_sync/model/synthetics_tests.py +++ b/datadog_sync/model/synthetics_tests.py @@ -72,9 +72,64 @@ class SyntheticsTests(BaseResource): browser_test_path: str = "/api/v1/synthetics/tests/browser/{}" api_test_path: str = "/api/v1/synthetics/tests/api/{}" mobile_test_path: str = "/api/v1/synthetics/tests/mobile/{}" + network_test_path: str = "/api/v2/synthetics/tests/network/{}" + network_base_path: str = "/api/v2/synthetics/tests/network" + network_delete_path: str = "/api/v2/synthetics/tests/bulk-delete" get_params = {"include_metadata": "true"} versions: List = [] + @staticmethod + def _unwrap_network_response(resp: Dict) -> Dict: + """Unwrap a v2 network test response into a flat resource dict.""" + resource = resp["data"]["attributes"] + resource["public_id"] = resp["data"]["id"] + resource["type"] = "network" + return resource + + @staticmethod + def _wrap_network_request(resource: Dict) -> Dict: + """Wrap a flat resource dict into a v2 network test request body.""" + resource.pop("public_id", None) + return {"data": {"attributes": resource, "type": "network"}} + + async def _get_test(self, client: CustomClient, test_type: str, public_id: str) -> Dict: + """Fetch a single test, handling v2 envelope for network tests.""" + if test_type == "network": + resp = await client.get(self.network_test_path.format(public_id), params=self.get_params) + return self._unwrap_network_response(resp) + path = self.resource_config.base_path + f"/{test_type}/{public_id}" + return await client.get(path, params=self.get_params) + + async def _create_test(self, client: CustomClient, test_type: str, resource: Dict) -> Dict: + """Create a test, handling v2 envelope for network tests.""" + if test_type == "network": + body = self._wrap_network_request(resource) + resp = await client.post(self.network_base_path, body) + return self._unwrap_network_response(resp) + return await client.post(self.resource_config.base_path + f"/{test_type}", resource) + + async def _update_test(self, client: CustomClient, public_id: str, resource: Dict) -> Dict: + """Update a test, handling v2 envelope for network tests.""" + if resource.get("type") == "network": + body = self._wrap_network_request(resource) + resp = await client.put(self.network_base_path + f"/{public_id}", body) + return self._unwrap_network_response(resp) + return await client.put(self.resource_config.base_path + f"/{public_id}", resource) + + async def _delete_test(self, client: CustomClient, test_type: str, public_id: str) -> None: + """Delete a test, handling v2 envelope for network tests.""" + if test_type == "network": + body = { + "data": { + "type": "delete_tests_request", + "attributes": {"public_ids": [public_id]}, + } + } + await client.post(self.network_delete_path, body) + else: + body = {"public_ids": [public_id]} + await client.post(self.resource_config.base_path + "/delete", body) + async def get_resources(self, client: CustomClient) -> List[Dict]: resp = await client.get( self.resource_config.base_path, @@ -99,10 +154,17 @@ async def import_resource(self, _id: Optional[str] = None, resource: Optional[Di params=self.get_params, ) except Exception: - resource = await source_client.get( - self.mobile_test_path.format(_id), - params=self.get_params, - ) + try: + resource = await source_client.get( + self.mobile_test_path.format(_id), + params=self.get_params, + ) + except Exception: + resp = await source_client.get( + self.network_test_path.format(_id), + params=self.get_params, + ) + resource = self._unwrap_network_response(resp) resource = cast(dict, resource) _id = resource["public_id"] @@ -127,6 +189,12 @@ async def import_resource(self, _id: Optional[str] = None, resource: Optional[Di if i["application_id"] == resource["options"]["mobileApplication"]["applicationId"] ] resource["mobileApplicationsVersions"] = list(set(versions)) + elif resource.get("type") == "network": + resp = await source_client.get( + self.network_test_path.format(_id), + params=self.get_params, + ) + resource = self._unwrap_network_response(resp) resource = cast(dict, resource) return f"{resource['public_id']}#{resource['monitor_id']}", resource @@ -176,7 +244,7 @@ async def create_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]: # on destination during failover scenarios. Status can be manually changed after creation. resource["status"] = "paused" - resp = await destination_client.post(self.resource_config.base_path + f"/{test_type}", resource) + resp = await self._create_test(destination_client, test_type, resource) # Now that we have the destination public_id, fix variables that embed the source public_id. source_public_id = _id.split("#")[0] @@ -184,10 +252,7 @@ async def create_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]: if source_public_id != dest_public_id and self._replace_variable_public_id( resource, source_public_id, dest_public_id ): - resp = await destination_client.put( - self.resource_config.base_path + f"/{dest_public_id}", - resource, - ) + resp = await self._update_test(destination_client, dest_public_id, resource) # Persist metadata in state so destination JSON has it and diffs compare correctly. if resource.get("metadata"): @@ -202,10 +267,7 @@ async def update_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]: dest_public_id = self.config.state.destination[self.resource_type][_id]["public_id"] self._replace_variable_public_id(resource, source_public_id, dest_public_id) - resp = await destination_client.put( - self.resource_config.base_path + f"/{dest_public_id}", - resource, - ) + resp = await self._update_test(destination_client, dest_public_id, resource) # Persist metadata in state so destination JSON has it and diffs compare correctly. if resource.get("metadata"): resp.setdefault("metadata", {}).update(deepcopy(resource["metadata"])) @@ -213,8 +275,8 @@ async def update_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]: async def delete_resource(self, _id: str) -> None: destination_client = self.config.destination_client - body = {"public_ids": [self.config.state.destination[self.resource_type][_id]["public_id"]]} - await destination_client.post(self.resource_config.base_path + "/delete", body) + dest_resource = self.config.state.destination[self.resource_type][_id] + await self._delete_test(destination_client, dest_resource.get("type"), dest_resource["public_id"]) def connect_id(self, key: str, r_obj: Dict, resource_to_connect: str) -> Optional[List[str]]: failed_connections: List[str] = [] diff --git a/tests/unit/test_synthetics_tests.py b/tests/unit/test_synthetics_tests.py index c7f7da02..c859dd24 100644 --- a/tests/unit/test_synthetics_tests.py +++ b/tests/unit/test_synthetics_tests.py @@ -69,15 +69,15 @@ def test_create_resource_with_different_test_types(self): """Verify status forcing works for all test types.""" mock_config = MagicMock(spec=Configuration) mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value={"public_id": "abc-123", "status": "paused"}) mock_config.destination_client = mock_client synthetics_tests = SyntheticsTests(mock_config) - test_types = ["api", "browser", "mobile"] - - for test_type in test_types: + # v1 test types return flat responses + v1_test_types = ["api", "browser", "mobile"] + for test_type in v1_test_types: mock_client.reset_mock() + mock_client.post = AsyncMock(return_value={"public_id": "abc-123", "status": "paused"}) test_resource = { "type": test_type, "status": "live", @@ -94,6 +94,30 @@ def test_create_resource_with_different_test_types(self): call_args = mock_client.post.call_args assert f"/{test_type}" in call_args[0][0], f"Should call correct endpoint for {test_type}" + # network test type returns v2 wrapped response + mock_client.reset_mock() + mock_client.post = AsyncMock( + return_value={"data": {"id": "abc-123", "type": "network_test", "attributes": {"status": "paused"}}} + ) + test_resource = { + "type": "network", + "status": "live", + "name": "Test network", + "config": {}, + "locations": [], + } + + asyncio.run(synthetics_tests.create_resource("src-pub-id#network", test_resource)) + + assert test_resource["status"] == "paused", "Status should be paused for network tests" + + call_args = mock_client.post.call_args + assert "/api/v2/synthetics/tests/network" == call_args[0][0], "Should call v2 endpoint for network" + # Verify v2 wrapped body + sent_body = call_args[0][1] + assert "data" in sent_body, "Network test should use v2 wrapped body" + assert sent_body["data"]["type"] == "network" + def test_status_not_in_nullable_attributes(self): """Verify status is not in non_nullable_attr to ensure it's properly handled.""" non_nullable = SyntheticsTests.resource_config.non_nullable_attr or []