Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 77 additions & 15 deletions datadog_sync/model/synthetics_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"]
Expand All @@ -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
Expand Down Expand Up @@ -176,18 +244,15 @@ 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]
dest_public_id = resp["public_id"]
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"):
Expand All @@ -202,19 +267,16 @@ 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"]))
return _id, resp

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] = []
Expand Down
32 changes: 28 additions & 4 deletions tests/unit/test_synthetics_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 []
Expand Down
Loading