4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
297 lines
10 KiB
Python
297 lines
10 KiB
Python
"""Unit tests for the Lambda service client."""
|
|
|
|
import base64
|
|
import json
|
|
from io import BytesIO
|
|
from unittest.mock import MagicMock, patch
|
|
from zipfile import ZipFile
|
|
|
|
import pytest
|
|
from botocore.exceptions import ClientError
|
|
|
|
from integrations.aws.lambda_client import (
|
|
get_function_code,
|
|
get_function_configuration,
|
|
get_invocation_logs_by_request_id,
|
|
get_recent_invocations,
|
|
invoke_function,
|
|
list_functions,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_aws_credentials():
|
|
with patch("integrations.aws.lambda_client.require_aws_credentials", return_value=None):
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_lambda_client():
|
|
with patch("integrations.aws.lambda_client._get_lambda_client") as mock:
|
|
client = MagicMock()
|
|
mock.return_value = client
|
|
yield client
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_logs_client():
|
|
with patch("integrations.aws.lambda_client._get_cloudwatch_logs_client") as mock:
|
|
client = MagicMock()
|
|
mock.return_value = client
|
|
yield client
|
|
|
|
|
|
def test_get_function_configuration_success(mock_lambda_client) -> None:
|
|
mock_lambda_client.get_function_configuration.return_value = {
|
|
"FunctionName": "test-func",
|
|
"FunctionArn": "arn:aws:lambda:us-east-1:123:function:test-func",
|
|
"Runtime": "python3.12",
|
|
"Handler": "index.handler",
|
|
"CodeSize": 1024,
|
|
"Timeout": 30,
|
|
"MemorySize": 128,
|
|
"LastModified": "2024-01-01T00:00:00.000+0000",
|
|
"Role": "arn:aws:iam::123:role/lambda-role",
|
|
"Environment": {"Variables": {"KEY": "VALUE"}},
|
|
"Description": "Test function",
|
|
"Version": "$LATEST",
|
|
"State": "Active",
|
|
"Layers": [{"Arn": "arn:layer", "CodeSize": 500}],
|
|
}
|
|
|
|
result = get_function_configuration("test-func")
|
|
|
|
assert result["success"] is True
|
|
data = result["data"]
|
|
assert data["function_name"] == "test-func"
|
|
assert data["runtime"] == "python3.12"
|
|
assert data["environment"] == {"KEY": "VALUE"}
|
|
assert data["layers"][0]["arn"] == "arn:layer"
|
|
|
|
|
|
def test_get_function_configuration_error(mock_lambda_client) -> None:
|
|
error_response = {"Error": {"Code": "ResourceNotFoundException", "Message": "Not found"}}
|
|
mock_lambda_client.get_function_configuration.side_effect = ClientError(
|
|
error_response, "GetFunctionConfiguration"
|
|
)
|
|
|
|
result = get_function_configuration("missing-func")
|
|
|
|
assert result["success"] is False
|
|
assert "ResourceNotFoundException" in result["error"]
|
|
|
|
|
|
def test_get_function_code_success(mock_lambda_client) -> None:
|
|
mock_lambda_client.get_function.return_value = {
|
|
"Code": {"Location": "https://example.com/zip", "RepositoryType": "S3"},
|
|
"Configuration": {"CodeSize": 2048},
|
|
}
|
|
|
|
# Create a mock zip file
|
|
zip_buffer = BytesIO()
|
|
with ZipFile(zip_buffer, "w") as zf:
|
|
zf.writestr("index.py", "def handler(): pass")
|
|
zf.writestr("data.bin", b"\x80\x81\x82") # Invalid UTF-8
|
|
zf.writestr("large.txt", "a" * 100) # Will be "large" relative to max_file_size
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = zip_buffer.getvalue()
|
|
|
|
with patch("requests.get", return_value=mock_response):
|
|
result = get_function_code("test-func", max_file_size=50)
|
|
|
|
assert result["success"] is True
|
|
files = result["data"]["files"]
|
|
assert "index.py" in files
|
|
assert files["index.py"]["content"] == "def handler(): pass"
|
|
assert files["data.bin"]["binary"] is True
|
|
assert files["large.txt"]["truncated"] is True
|
|
assert result["data"]["file_count"] == 3
|
|
|
|
|
|
def test_get_recent_invocations_success(mock_logs_client) -> None:
|
|
mock_logs_client.filter_log_events.return_value = {
|
|
"events": [
|
|
{"timestamp": 1000, "message": "START RequestId: req1 Version: $LATEST\n"},
|
|
{"timestamp": 1100, "message": "hello world\n"},
|
|
{"timestamp": 1200, "message": "END RequestId: req1\n"},
|
|
{
|
|
"timestamp": 1300,
|
|
"message": "REPORT RequestId: req1\tDuration: 200.00 ms\tBilled Duration: 200 ms\tMemory Size: 128 MB\tMax Memory Used: 64 MB\t\n",
|
|
},
|
|
]
|
|
}
|
|
|
|
result = get_recent_invocations("test-func")
|
|
|
|
assert result["success"] is True
|
|
assert result["data"]["invocation_count"] == 1
|
|
invocation = result["data"]["invocations"][0]
|
|
assert invocation["request_id"] == "req1"
|
|
assert invocation["duration_ms"] == 200.0
|
|
assert invocation["memory_used_mb"] == 64
|
|
assert len(invocation["logs"]) == 4
|
|
|
|
|
|
def test_get_recent_invocations_resource_not_found(mock_logs_client) -> None:
|
|
error_response = {
|
|
"Error": {"Code": "ResourceNotFoundException", "Message": "Log group not found"}
|
|
}
|
|
mock_logs_client.filter_log_events.side_effect = ClientError(error_response, "FilterLogEvents")
|
|
|
|
result = get_recent_invocations("missing-func")
|
|
|
|
assert result["success"] is False
|
|
assert "Log group not found" in result["error"]
|
|
|
|
|
|
def test_invoke_function_success(mock_lambda_client) -> None:
|
|
mock_response = {
|
|
"StatusCode": 200,
|
|
"ExecutedVersion": "$LATEST",
|
|
"Payload": BytesIO(json.dumps({"body": "ok"}).encode()),
|
|
"LogResult": base64.b64encode(b"some logs").decode(),
|
|
}
|
|
mock_lambda_client.invoke.return_value = mock_response
|
|
|
|
result = invoke_function("test-func", payload={"key": "val"})
|
|
|
|
assert result["success"] is True
|
|
assert result["data"]["payload"] == {"body": "ok"}
|
|
assert result["data"]["log_result"] == "some logs"
|
|
|
|
# Verify call
|
|
mock_lambda_client.invoke.assert_called_once_with(
|
|
FunctionName="test-func",
|
|
InvocationType="RequestResponse",
|
|
Payload=json.dumps({"key": "val"}),
|
|
)
|
|
|
|
|
|
def test_get_function_code_no_extract(mock_lambda_client) -> None:
|
|
mock_lambda_client.get_function.return_value = {
|
|
"Code": {"Location": "https://example.com/zip", "RepositoryType": "S3"},
|
|
"Configuration": {"CodeSize": 1024},
|
|
}
|
|
# Should not call requests.get
|
|
with patch("requests.get") as mock_get:
|
|
result = get_function_code("test-func", extract_files=False)
|
|
assert result["success"] is True
|
|
assert "files" not in result["data"]
|
|
mock_get.assert_not_called()
|
|
|
|
|
|
def test_get_function_code_too_large(mock_lambda_client) -> None:
|
|
mock_lambda_client.get_function.return_value = {
|
|
"Code": {"Location": "https://example.com/zip", "RepositoryType": "S3"},
|
|
"Configuration": {"CodeSize": 10 * 1024 * 1024}, # 10MB
|
|
}
|
|
with patch("requests.get") as mock_get:
|
|
result = get_function_code("test-func")
|
|
assert result["success"] is True
|
|
assert "files" not in result["data"]
|
|
mock_get.assert_not_called()
|
|
|
|
|
|
def test_get_invocation_logs_by_request_id_success(mock_logs_client) -> None:
|
|
mock_logs_client.filter_log_events.return_value = {
|
|
"events": [
|
|
{"message": "log 1\n"},
|
|
{"message": "log 2\n"},
|
|
]
|
|
}
|
|
|
|
result = get_invocation_logs_by_request_id("test-func", "req1")
|
|
|
|
assert result["success"] is True
|
|
assert result["data"]["event_count"] == 2
|
|
assert result["data"]["logs"] == ["log 1\n", "log 2\n"]
|
|
|
|
|
|
def test_list_functions_success(mock_lambda_client) -> None:
|
|
mock_lambda_client.list_functions.return_value = {
|
|
"Functions": [
|
|
{
|
|
"FunctionName": "func1",
|
|
"FunctionArn": "arn1",
|
|
"Runtime": "python3.12",
|
|
"Handler": "h1",
|
|
"CodeSize": 100,
|
|
"Timeout": 10,
|
|
"MemorySize": 128,
|
|
"LastModified": "date1",
|
|
}
|
|
]
|
|
}
|
|
|
|
result = list_functions()
|
|
|
|
assert result["success"] is True
|
|
assert len(result["data"]["functions"]) == 1
|
|
assert result["data"]["functions"][0]["function_name"] == "func1"
|
|
|
|
|
|
def test_get_function_code_download_error(mock_lambda_client) -> None:
|
|
mock_lambda_client.get_function.return_value = {
|
|
"Code": {"Location": "https://example.com/zip", "RepositoryType": "S3"},
|
|
"Configuration": {"CodeSize": 1024},
|
|
}
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 404
|
|
with patch("requests.get", return_value=mock_response):
|
|
result = get_function_code("test-func")
|
|
|
|
assert result["success"] is True # The get_function call succeeded
|
|
assert "files" not in result["data"]
|
|
|
|
|
|
def test_get_function_code_corrupt_zip(mock_lambda_client) -> None:
|
|
mock_lambda_client.get_function.return_value = {
|
|
"Code": {"Location": "https://example.com/zip", "RepositoryType": "S3"},
|
|
"Configuration": {"CodeSize": 1024},
|
|
}
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = b"not a zip file"
|
|
|
|
with patch("requests.get", return_value=mock_response):
|
|
result = get_function_code("test-func")
|
|
|
|
assert result["success"] is True
|
|
assert "extract_error" in result["data"]
|
|
|
|
|
|
def test_get_recent_invocations_multiple(mock_logs_client) -> None:
|
|
# Test grouping of multiple overlapping/sequential invocations
|
|
mock_logs_client.filter_log_events.return_value = {
|
|
"events": [
|
|
{"timestamp": 1000, "message": "START RequestId: req1\n"},
|
|
{"timestamp": 1050, "message": "log 1\n"},
|
|
{"timestamp": 1100, "message": "END RequestId: req1\n"},
|
|
{
|
|
"timestamp": 1150,
|
|
"message": "REPORT RequestId: req1\tDuration: 100.00 ms\tBilled Duration: 100 ms\tMemory Size: 128 MB\tMax Memory Used: 64 MB\t\n",
|
|
},
|
|
{"timestamp": 1200, "message": "START RequestId: req2\n"},
|
|
{"timestamp": 1250, "message": "log 2\n"},
|
|
{
|
|
"timestamp": 1300,
|
|
"message": "REPORT RequestId: req2\tDuration: 50.00 ms\tBilled Duration: 50 ms\tMemory Size: 128 MB\tMax Memory Used: 32 MB\t\n",
|
|
},
|
|
]
|
|
}
|
|
|
|
result = get_recent_invocations("test-func")
|
|
|
|
assert result["success"] is True
|
|
assert result["data"]["invocation_count"] == 2
|
|
inv1 = result["data"]["invocations"][0]
|
|
inv2 = result["data"]["invocations"][1]
|
|
assert inv1["request_id"] == "req1"
|
|
assert inv1["duration_ms"] == 100.0
|
|
assert inv2["request_id"] == "req2"
|
|
assert inv2["duration_ms"] == 50.0
|
|
assert len(inv1["logs"]) == 4
|
|
assert len(inv2["logs"]) == 3
|