4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
262 lines
10 KiB
Python
262 lines
10 KiB
Python
"""Direct unit tests for integrations/aws/cloudwatch_client.py service functions.
|
|
|
|
Tests cover get_metric_statistics, filter_log_events, and get_log_events
|
|
with mocked boto3 client and credential helpers. Tool-registration
|
|
assertions are intentionally excluded.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from botocore.exceptions import ClientError
|
|
|
|
from integrations.aws.cloudwatch_client import (
|
|
filter_log_events,
|
|
get_log_events,
|
|
get_metric_statistics,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _mock_creds():
|
|
with patch("integrations.aws.cloudwatch_client.require_aws_credentials", return_value=None):
|
|
yield
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_cw_client():
|
|
client = MagicMock()
|
|
with patch("integrations.aws.cloudwatch_client._get_cloudwatch_client", return_value=client):
|
|
yield client
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_logs_client():
|
|
client = MagicMock()
|
|
with patch(
|
|
"integrations.aws.cloudwatch_client._get_cloudwatch_logs_client", return_value=client
|
|
):
|
|
yield client
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_metric_statistics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetMetricStatistics:
|
|
def test_returns_data_on_success(self, mock_cw_client):
|
|
mock_cw_client.get_metric_statistics.return_value = {"Datapoints": [{"Average": 50.0}]}
|
|
result = get_metric_statistics(
|
|
namespace="AWS/Batch",
|
|
metric_name="CPUUtilization",
|
|
start_time="2024-01-01T00:00:00Z",
|
|
end_time="2024-01-01T01:00:00Z",
|
|
)
|
|
assert result["success"] is True
|
|
assert result["data"]["Datapoints"] == [{"Average": 50.0}]
|
|
|
|
def test_passes_dimensions_and_statistics(self, mock_cw_client):
|
|
mock_cw_client.get_metric_statistics.return_value = {"Datapoints": []}
|
|
get_metric_statistics(
|
|
namespace="AWS/ECS",
|
|
metric_name="MemoryUtilization",
|
|
dimensions=[{"Name": "ServiceName", "Value": "my-service"}],
|
|
start_time="2024-01-01T00:00:00Z",
|
|
end_time="2024-01-01T01:00:00Z",
|
|
statistics=["Average", "Sum"],
|
|
)
|
|
call_kwargs = mock_cw_client.get_metric_statistics.call_args.kwargs
|
|
assert call_kwargs["Dimensions"] == [{"Name": "ServiceName", "Value": "my-service"}]
|
|
assert call_kwargs["Statistics"] == ["Average", "Sum"]
|
|
|
|
def test_default_statistics_when_none(self, mock_cw_client):
|
|
mock_cw_client.get_metric_statistics.return_value = {"Datapoints": []}
|
|
get_metric_statistics(
|
|
namespace="AWS/Batch",
|
|
metric_name="CPUUtilization",
|
|
start_time="2024-01-01T00:00:00Z",
|
|
end_time="2024-01-01T01:00:00Z",
|
|
)
|
|
call_kwargs = mock_cw_client.get_metric_statistics.call_args.kwargs
|
|
assert call_kwargs["Statistics"] == ["Average", "Maximum", "Minimum"]
|
|
|
|
def test_returns_error_on_client_error(self, mock_cw_client):
|
|
mock_cw_client.get_metric_statistics.side_effect = ClientError(
|
|
{"Error": {"Code": "AccessDenied", "Message": "Not authorized"}}, "GetMetricStatistics"
|
|
)
|
|
result = get_metric_statistics(
|
|
namespace="AWS/Batch",
|
|
metric_name="CPUUtilization",
|
|
start_time="2024-01-01T00:00:00Z",
|
|
end_time="2024-01-01T01:00:00Z",
|
|
)
|
|
assert result["success"] is False
|
|
assert "AccessDenied" in result["error"]
|
|
|
|
def test_returns_error_when_no_client(self):
|
|
with patch("integrations.aws.cloudwatch_client._get_cloudwatch_client", return_value=None):
|
|
result = get_metric_statistics(
|
|
namespace="AWS/Batch",
|
|
metric_name="CPUUtilization",
|
|
start_time="2024-01-01T00:00:00Z",
|
|
end_time="2024-01-01T01:00:00Z",
|
|
)
|
|
assert result["success"] is False
|
|
assert "boto3 not available" in result["error"]
|
|
|
|
def test_returns_error_when_credentials_missing(self):
|
|
with patch(
|
|
"integrations.aws.cloudwatch_client.require_aws_credentials",
|
|
return_value={"success": False, "error": "Missing AWS credentials"},
|
|
):
|
|
result = get_metric_statistics(
|
|
namespace="AWS/Batch",
|
|
metric_name="CPUUtilization",
|
|
start_time="2024-01-01T00:00:00Z",
|
|
end_time="2024-01-01T01:00:00Z",
|
|
)
|
|
assert result["success"] is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# filter_log_events
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFilterLogEvents:
|
|
def test_returns_events_on_success(self, mock_logs_client):
|
|
mock_logs_client.filter_log_events.return_value = {
|
|
"events": [{"message": "Error occurred", "timestamp": 1000}]
|
|
}
|
|
result = filter_log_events(log_group_name="/aws/batch/job", filter_pattern="Error")
|
|
assert result["success"] is True
|
|
assert len(result["data"]) == 1
|
|
|
|
def test_passes_optional_params(self, mock_logs_client):
|
|
mock_logs_client.filter_log_events.return_value = {"events": []}
|
|
filter_log_events(
|
|
log_group_name="/aws/lambda/fn",
|
|
filter_pattern="timeout",
|
|
start_time=1700000000000,
|
|
end_time=1700003600000,
|
|
limit=50,
|
|
)
|
|
call_kwargs = mock_logs_client.filter_log_events.call_args.kwargs
|
|
assert call_kwargs["filterPattern"] == "timeout"
|
|
assert call_kwargs["startTime"] == 1700000000000
|
|
assert call_kwargs["endTime"] == 1700003600000
|
|
assert call_kwargs["limit"] == 50
|
|
|
|
def test_omits_optional_params_when_none(self, mock_logs_client):
|
|
mock_logs_client.filter_log_events.return_value = {"events": []}
|
|
filter_log_events(log_group_name="/aws/batch/job")
|
|
call_kwargs = mock_logs_client.filter_log_events.call_args.kwargs
|
|
assert "filterPattern" not in call_kwargs
|
|
assert "startTime" not in call_kwargs
|
|
assert "endTime" not in call_kwargs
|
|
|
|
def test_returns_error_on_client_error(self, mock_logs_client):
|
|
mock_logs_client.filter_log_events.side_effect = ClientError(
|
|
{"Error": {"Code": "ResourceNotFoundException", "Message": "Log group not found"}},
|
|
"FilterLogEvents",
|
|
)
|
|
result = filter_log_events(log_group_name="/nonexistent")
|
|
assert result["success"] is False
|
|
assert "ResourceNotFoundException" in result["error"]
|
|
|
|
def test_returns_error_when_no_client(self):
|
|
with patch(
|
|
"integrations.aws.cloudwatch_client._get_cloudwatch_logs_client", return_value=None
|
|
):
|
|
result = filter_log_events(log_group_name="/aws/batch/job")
|
|
assert result["success"] is False
|
|
assert "boto3 not available" in result["error"]
|
|
|
|
def test_returns_error_when_credentials_missing(self):
|
|
with patch(
|
|
"integrations.aws.cloudwatch_client.require_aws_credentials",
|
|
return_value={"success": False, "error": "Missing AWS credentials"},
|
|
):
|
|
result = filter_log_events(log_group_name="/aws/batch/job")
|
|
assert result["success"] is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_log_events
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetLogEvents:
|
|
def test_returns_events_on_success(self, mock_logs_client):
|
|
mock_logs_client.get_log_events.return_value = {
|
|
"events": [{"message": "line 1"}, {"message": "line 2"}]
|
|
}
|
|
result = get_log_events(
|
|
log_group_name="/aws/batch/job",
|
|
log_stream_name="stream-1",
|
|
)
|
|
assert result["success"] is True
|
|
assert len(result["data"]) == 2
|
|
|
|
def test_passes_optional_params(self, mock_logs_client):
|
|
mock_logs_client.get_log_events.return_value = {"events": []}
|
|
get_log_events(
|
|
log_group_name="/aws/batch/job",
|
|
log_stream_name="stream-1",
|
|
start_time=1700000000000,
|
|
end_time=1700003600000,
|
|
limit=50,
|
|
)
|
|
call_kwargs = mock_logs_client.get_log_events.call_args.kwargs
|
|
assert call_kwargs["startTime"] == 1700000000000
|
|
assert call_kwargs["endTime"] == 1700003600000
|
|
assert call_kwargs["limit"] == 50
|
|
|
|
def test_omits_optional_params_when_none(self, mock_logs_client):
|
|
mock_logs_client.get_log_events.return_value = {"events": []}
|
|
get_log_events(
|
|
log_group_name="/aws/batch/job",
|
|
log_stream_name="stream-1",
|
|
)
|
|
call_kwargs = mock_logs_client.get_log_events.call_args.kwargs
|
|
assert "startTime" not in call_kwargs
|
|
assert "endTime" not in call_kwargs
|
|
|
|
def test_returns_error_on_client_error(self, mock_logs_client):
|
|
mock_logs_client.get_log_events.side_effect = ClientError(
|
|
{"Error": {"Code": "ResourceNotFoundException", "Message": "Stream not found"}},
|
|
"GetLogEvents",
|
|
)
|
|
result = get_log_events(log_group_name="/aws/batch/job", log_stream_name="bad-stream")
|
|
assert result["success"] is False
|
|
assert "ResourceNotFoundException" in result["error"]
|
|
|
|
def test_returns_error_when_no_client(self):
|
|
with patch(
|
|
"integrations.aws.cloudwatch_client._get_cloudwatch_logs_client", return_value=None
|
|
):
|
|
result = get_log_events(
|
|
log_group_name="/aws/batch/job",
|
|
log_stream_name="stream-1",
|
|
)
|
|
assert result["success"] is False
|
|
assert "boto3 not available" in result["error"]
|
|
|
|
def test_returns_error_when_credentials_missing(self):
|
|
with patch(
|
|
"integrations.aws.cloudwatch_client.require_aws_credentials",
|
|
return_value={"success": False, "error": "Missing AWS credentials"},
|
|
):
|
|
result = get_log_events(
|
|
log_group_name="/aws/batch/job",
|
|
log_stream_name="stream-1",
|
|
)
|
|
assert result["success"] is False
|