test_resources.py•25.6 kB
"""Unit tests for AWS MCP Server resources module."""
import configparser
import os
from unittest.mock import MagicMock, patch
import pytest
from botocore.exceptions import ClientError
from aws_mcp_server.resources import (
_get_region_description,
_get_region_geographic_location,
get_aws_account_info,
get_aws_environment,
get_aws_profiles,
get_aws_regions,
get_region_available_services,
get_region_details,
register_resources,
)
@pytest.fixture
def mock_config_files(monkeypatch, tmp_path):
"""Create mock AWS config and credentials files for testing."""
config_dir = tmp_path / ".aws"
config_dir.mkdir()
config_file = config_dir / "config"
config_file.write_text("[default]\nregion = us-west-2\n\n[profile dev]\nregion = us-east-1\n\n[profile prod]\nregion = eu-west-1\n")
creds_file = config_dir / "credentials"
creds_file.write_text(
"[default]\n"
"aws_access_key_id = AKIADEFAULT000000000\n"
"aws_secret_access_key = 1234567890abcdef1234567890abcdef\n"
"\n"
"[dev]\n"
"aws_access_key_id = AKIADEV0000000000000\n"
"aws_secret_access_key = abcdef1234567890abcdef1234567890\n"
"\n"
"[test]\n"
"aws_access_key_id = AKIATEST000000000000\n"
"aws_secret_access_key = test1234567890abcdef1234567890ab\n"
)
monkeypatch.setenv("HOME", str(tmp_path))
return tmp_path
def test_get_aws_profiles(mock_config_files):
"""Test retrieving AWS profiles from config files."""
profiles = get_aws_profiles()
assert set(profiles) == {"default", "dev", "prod", "test"}
def test_get_aws_profiles_custom_config_file(monkeypatch, tmp_path):
"""Test get_aws_profiles with custom AWS_CONFIG_FILE."""
custom_config = tmp_path / "custom_config"
custom_config.write_text("[default]\nregion = us-west-2\n\n[profile custom-profile]\nregion = eu-west-1\n")
monkeypatch.setenv("AWS_CONFIG_FILE", str(custom_config))
monkeypatch.setenv("HOME", str(tmp_path))
profiles = get_aws_profiles()
assert "default" in profiles
assert "custom-profile" in profiles
def test_get_aws_profiles_custom_credentials_file(monkeypatch, tmp_path):
"""Test get_aws_profiles with custom AWS_SHARED_CREDENTIALS_FILE."""
custom_creds = tmp_path / "custom_credentials"
custom_creds.write_text(
"[default]\n"
"aws_access_key_id = AKIATEST\n"
"aws_secret_access_key = secret\n\n"
"[custom-creds-profile]\n"
"aws_access_key_id = AKIACUSTOM\n"
"aws_secret_access_key = customsecret\n"
)
monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", str(custom_creds))
monkeypatch.setenv("HOME", str(tmp_path))
profiles = get_aws_profiles()
assert "default" in profiles
assert "custom-creds-profile" in profiles
def test_get_aws_profiles_both_custom_paths(monkeypatch, tmp_path):
"""Test get_aws_profiles with both custom config and credentials files."""
custom_config = tmp_path / "custom_config"
custom_config.write_text("[profile config-only-profile]\nregion = us-west-2\n")
custom_creds = tmp_path / "custom_credentials"
custom_creds.write_text("[creds-only-profile]\naws_access_key_id = AKIATEST\naws_secret_access_key = secret\n")
monkeypatch.setenv("AWS_CONFIG_FILE", str(custom_config))
monkeypatch.setenv("AWS_SHARED_CREDENTIALS_FILE", str(custom_creds))
monkeypatch.setenv("HOME", str(tmp_path))
profiles = get_aws_profiles()
assert "default" in profiles
assert "config-only-profile" in profiles
assert "creds-only-profile" in profiles
@patch("boto3.session.Session")
def test_get_aws_regions(mock_session):
"""Test retrieving AWS regions with mocked boto3."""
mock_ec2 = MagicMock()
mock_session.return_value.client.return_value = mock_ec2
mock_ec2.describe_regions.return_value = {
"Regions": [
{"RegionName": "us-east-1"},
{"RegionName": "us-west-2"},
{"RegionName": "eu-central-1"},
]
}
regions = get_aws_regions()
assert len(regions) == 3
assert regions[0]["RegionName"] == "eu-central-1"
assert regions[0]["RegionDescription"] == "EU Central (Frankfurt)"
assert regions[1]["RegionName"] == "us-east-1"
assert regions[2]["RegionName"] == "us-west-2"
mock_session.assert_called_once()
mock_session.return_value.client.assert_called_once_with("ec2")
@patch("boto3.session.Session")
def test_get_aws_regions_fallback(mock_session):
"""Test fallback behavior when region retrieval fails."""
mock_session.return_value.client.side_effect = ClientError(
{"Error": {"Code": "AccessDenied", "Message": "Access denied"}},
"DescribeRegions",
)
regions = get_aws_regions()
assert len(regions) >= 12
assert any(r["RegionName"] == "us-east-1" for r in regions)
assert any(r["RegionName"] == "eu-west-1" for r in regions)
@patch("boto3.session.Session")
def test_get_aws_environment(mock_session, monkeypatch):
"""Test retrieving AWS environment information."""
monkeypatch.setenv("AWS_PROFILE", "test-profile")
monkeypatch.setenv("AWS_REGION", "us-west-2")
mock_credentials = MagicMock()
mock_credentials.method = "shared-credentials-file"
mock_session.return_value.get_credentials.return_value = mock_credentials
env_info = get_aws_environment()
assert env_info["aws_profile"] == "test-profile"
assert env_info["aws_region"] == "us-west-2"
assert env_info["has_credentials"] is True
assert env_info["credentials_source"] == "profile"
@patch("boto3.session.Session")
def test_get_aws_environment_no_credentials(mock_session, monkeypatch):
"""Test environment info with no credentials."""
for var in ["AWS_PROFILE", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_ACCESS_KEY_ID"]:
if var in os.environ:
monkeypatch.delenv(var)
mock_session.return_value.get_credentials.return_value = None
env_info = get_aws_environment()
assert env_info["aws_profile"] == "default"
assert env_info["aws_region"] == "us-east-1"
assert env_info["has_credentials"] is False
assert env_info["credentials_source"] == "none"
@patch("boto3.session.Session")
def test_get_aws_account_info(mock_session):
"""Test retrieving AWS account information."""
mock_sts = MagicMock()
mock_iam = MagicMock()
mock_org = MagicMock()
mock_session.return_value.client.side_effect = lambda service: {
"sts": mock_sts,
"iam": mock_iam,
"organizations": mock_org,
}[service]
mock_sts.get_caller_identity.return_value = {"Account": "123456789012"}
mock_iam.list_account_aliases.return_value = {"AccountAliases": ["my-account"]}
mock_org.describe_organization.return_value = {"Organization": {"Id": "o-abcdef1234"}}
account_info = get_aws_account_info()
assert account_info["account_id"] == "123456789012"
assert account_info["account_alias"] == "my-account"
assert account_info["organization_id"] == "o-abcdef1234"
@patch("boto3.session.Session")
def test_get_aws_account_info_minimal(mock_session):
"""Test account info with minimal permissions."""
mock_sts = MagicMock()
def mock_client(service):
if service == "sts":
return mock_sts
elif service == "iam":
raise ClientError({"Error": {"Code": "AccessDenied"}}, "ListAccountAliases")
else:
raise ClientError({"Error": {"Code": "AccessDenied"}}, "DescribeAccount")
mock_session.return_value.client.side_effect = mock_client
mock_sts.get_caller_identity.return_value = {"Account": "123456789012"}
account_info = get_aws_account_info()
assert account_info["account_id"] == "123456789012"
assert account_info["account_alias"] is None
assert account_info["organization_id"] is None
def test_register_resources():
"""Test registering MCP resources."""
mock_mcp = MagicMock()
register_resources(mock_mcp)
assert mock_mcp.resource.call_count == 5
expected_resources = [
{
"uri": "aws://config/profiles",
"name": "aws_profiles",
"description": "Get available AWS profiles",
},
{
"uri": "aws://config/regions",
"name": "aws_regions",
"description": "Get available AWS regions",
},
{
"uri": "aws://config/regions/{region}",
"name": "aws_region_details",
"description": "Get detailed information about a specific AWS region",
},
{
"uri": "aws://config/environment",
"name": "aws_environment",
"description": "Get AWS environment information",
},
{
"uri": "aws://config/account",
"name": "aws_account",
"description": "Get AWS account information",
},
]
for call in mock_mcp.resource.call_args_list:
found = False
for resource in expected_resources:
if resource["uri"] == call.kwargs.get("uri"):
assert call.kwargs.get("name") == resource["name"]
assert call.kwargs.get("description") == resource["description"]
found = True
break
assert found, f"URI {call.kwargs.get('uri')} not found in expected resources"
def test_get_region_description():
"""Test the region description utility function."""
assert _get_region_description("us-east-1") == "US East (N. Virginia)"
assert _get_region_description("eu-west-2") == "EU West (London)"
assert _get_region_description("ap-southeast-1") == "Asia Pacific (Singapore)"
assert _get_region_description("unknown-region-1") == "AWS Region unknown-region-1"
assert _get_region_description("test-region-2") == "AWS Region test-region-2"
@patch("configparser.ConfigParser")
@patch("os.path.exists")
def test_get_aws_profiles_exception(mock_exists, mock_config_parser):
"""Test exception handling in get_aws_profiles."""
mock_exists.return_value = True
mock_parser_instance = MagicMock()
mock_config_parser.return_value = mock_parser_instance
mock_parser_instance.read.side_effect = configparser.Error("Config file error")
profiles = get_aws_profiles()
assert profiles == ["default"]
assert mock_parser_instance.read.called
@patch("boto3.session.Session")
def test_get_aws_regions_generic_exception(mock_session):
"""Test general exception handling in get_aws_regions."""
mock_session.return_value.client.side_effect = Exception("Generic error")
regions = get_aws_regions()
assert len(regions) == 0
assert isinstance(regions, list)
@patch("boto3.session.Session")
def test_get_aws_environment_credential_methods(mock_session):
"""Test different credential methods in get_aws_environment."""
test_cases = [
("environment", "environment"),
("iam-role", "instance-profile"),
("assume-role", "assume-role"),
("container-role", "container-role"),
("unknown-method", "profile"),
]
for method, expected_source in test_cases:
mock_session.reset_mock()
mock_credentials = MagicMock()
mock_credentials.method = method
mock_session.return_value.get_credentials.return_value = mock_credentials
env_info = get_aws_environment()
assert env_info["has_credentials"] is True
assert env_info["credentials_source"] == expected_source
@patch("boto3.session.Session")
def test_get_aws_environment_exception(mock_session):
"""Test exception handling in get_aws_environment."""
mock_session.return_value.get_credentials.side_effect = ClientError(
{"Error": {"Code": "AccessDenied", "Message": "Credential error"}},
"GetCredentials",
)
env_info = get_aws_environment()
assert env_info["aws_profile"] == "default"
assert env_info["aws_region"] == "us-east-1"
assert env_info["has_credentials"] is False
assert env_info["credentials_source"] == "none"
@patch("boto3.session.Session")
def test_get_aws_account_info_with_org(mock_session):
"""Test AWS account info with organization access."""
mock_sts = MagicMock()
mock_iam = MagicMock()
mock_org = MagicMock()
mock_session.return_value.client.side_effect = lambda service: {
"sts": mock_sts,
"iam": mock_iam,
"organizations": mock_org,
}[service]
mock_sts.get_caller_identity.return_value = {"Account": "123456789012"}
mock_iam.list_account_aliases.return_value = {"AccountAliases": ["my-account"]}
mock_org.describe_organization.return_value = {"Organization": {}}
account_info = get_aws_account_info()
assert account_info["account_id"] == "123456789012"
assert account_info["account_alias"] == "my-account"
assert account_info["organization_id"] is None
@patch("boto3.session.Session")
def test_get_aws_account_info_general_exception(mock_session):
"""Test exception handling in get_aws_account_info."""
mock_session.return_value.client.side_effect = ClientError(
{"Error": {"Code": "AccessDenied", "Message": "Access denied"}},
"GetCallerIdentity",
)
account_info = get_aws_account_info()
assert account_info["account_id"] is None
assert account_info["account_alias"] is None
assert account_info["organization_id"] is None
@patch("aws_mcp_server.resources.get_aws_profiles")
@patch("os.environ.get")
def test_resource_aws_profiles(mock_environ_get, mock_get_aws_profiles):
"""Test the aws_profiles resource function implementation."""
mock_environ_get.return_value = "test-profile"
mock_get_aws_profiles.return_value = ["default", "test-profile", "dev"]
async def mock_resource_function():
profiles = mock_get_aws_profiles.return_value
current_profile = mock_environ_get.return_value
return {"profiles": [{"name": profile, "is_current": profile == current_profile} for profile in profiles]}
import asyncio
result = asyncio.run(mock_resource_function())
assert "profiles" in result
assert len(result["profiles"]) == 3
current_profile = None
for profile in result["profiles"]:
if profile["is_current"]:
current_profile = profile["name"]
assert current_profile == "test-profile"
@patch("aws_mcp_server.resources.get_aws_regions")
@patch("os.environ.get")
def test_resource_aws_regions(mock_environ_get, mock_get_aws_regions):
"""Test the aws_regions resource function implementation."""
mock_environ_get.side_effect = lambda key, default=None: ("us-west-2" if key in ("AWS_REGION", "AWS_DEFAULT_REGION") else default)
mock_get_aws_regions.return_value = [
{"RegionName": "us-east-1", "RegionDescription": "US East (N. Virginia)"},
{"RegionName": "us-west-2", "RegionDescription": "US West (Oregon)"},
]
async def mock_resource_function():
regions = mock_get_aws_regions.return_value
current_region = "us-west-2"
return {
"regions": [
{
"name": region["RegionName"],
"description": region["RegionDescription"],
"is_current": region["RegionName"] == current_region,
}
for region in regions
]
}
import asyncio
result = asyncio.run(mock_resource_function())
assert "regions" in result
assert len(result["regions"]) == 2
current_region = None
for region in result["regions"]:
if region["is_current"]:
current_region = region["name"]
assert current_region == "us-west-2"
@patch("aws_mcp_server.resources.get_aws_environment")
def test_resource_aws_environment(mock_get_aws_environment):
"""Test the aws_environment resource function implementation."""
mock_env = {
"aws_profile": "test-profile",
"aws_region": "us-west-2",
"has_credentials": True,
"credentials_source": "profile",
}
mock_get_aws_environment.return_value = mock_env
async def mock_resource_function():
return mock_get_aws_environment.return_value
import asyncio
result = asyncio.run(mock_resource_function())
assert result == mock_env
@patch("aws_mcp_server.resources.get_aws_account_info")
def test_resource_aws_account(mock_get_aws_account_info):
"""Test the aws_account resource function implementation."""
mock_account_info = {
"account_id": "123456789012",
"account_alias": "test-account",
"organization_id": "o-abcdef123456",
}
mock_get_aws_account_info.return_value = mock_account_info
async def mock_resource_function():
return mock_get_aws_account_info.return_value
import asyncio
result = asyncio.run(mock_resource_function())
assert result == mock_account_info
def test_get_region_geographic_location():
"""Test the region geographic location utility function."""
us_east_1 = _get_region_geographic_location("us-east-1")
assert us_east_1["continent"] == "North America"
assert us_east_1["country"] == "United States"
assert us_east_1["city"] == "Ashburn, Virginia"
eu_west_2 = _get_region_geographic_location("eu-west-2")
assert eu_west_2["continent"] == "Europe"
assert eu_west_2["country"] == "United Kingdom"
assert eu_west_2["city"] == "London"
unknown = _get_region_geographic_location("unknown-region")
assert unknown["continent"] == "Unknown"
assert unknown["country"] == "Unknown"
assert unknown["city"] == "Unknown"
@patch("boto3.session.Session")
def test_get_region_available_services(mock_session):
"""Test retrieving available AWS services for a region using Service Quotas API."""
mock_quotas_client = MagicMock()
def mock_client(service_name, **kwargs):
if service_name == "service-quotas":
return mock_quotas_client
return MagicMock()
mock_session.return_value.client.side_effect = mock_client
mock_quotas_client.list_services.return_value = {
"Services": [
{"ServiceCode": "AWS.EC2", "ServiceName": "Amazon Elastic Compute Cloud"},
{"ServiceCode": "AWS.S3", "ServiceName": "Amazon Simple Storage Service"},
{"ServiceCode": "Lambda", "ServiceName": "AWS Lambda"},
{"ServiceCode": "Organizations", "ServiceName": "AWS Organizations"},
{"ServiceCode": "AWS.CloudFormation", "ServiceName": "AWS CloudFormation"},
],
"NextToken": None,
}
services = get_region_available_services(mock_session.return_value, "us-east-1")
assert len(services) == 5
assert {"id": "ec2", "name": "Amazon Elastic Compute Cloud"} in services
assert {"id": "s3", "name": "Amazon Simple Storage Service"} in services
assert {"id": "lambda", "name": "AWS Lambda"} in services
assert {"id": "organizations", "name": "AWS Organizations"} in services
assert {"id": "cloudformation", "name": "AWS CloudFormation"} in services
mock_quotas_client.list_services.assert_called_once()
@patch("boto3.session.Session")
def test_get_region_available_services_pagination(mock_session):
"""Test pagination handling in Service Quotas API."""
mock_quotas_client = MagicMock()
mock_session.return_value.client.return_value = mock_quotas_client
mock_quotas_client.list_services.side_effect = [
{
"Services": [
{
"ServiceCode": "AWS.EC2",
"ServiceName": "Amazon Elastic Compute Cloud",
},
{
"ServiceCode": "AWS.S3",
"ServiceName": "Amazon Simple Storage Service",
},
],
"NextToken": "next-token-1",
},
{
"Services": [
{"ServiceCode": "Lambda", "ServiceName": "AWS Lambda"},
{"ServiceCode": "AWS.DynamoDB", "ServiceName": "Amazon DynamoDB"},
],
"NextToken": None,
},
]
services = get_region_available_services(mock_session.return_value, "us-east-1")
assert len(services) == 4
assert mock_quotas_client.list_services.call_count == 2
mock_quotas_client.list_services.assert_any_call()
mock_quotas_client.list_services.assert_any_call(NextToken="next-token-1")
@patch("boto3.session.Session")
def test_get_region_available_services_fallback(mock_session):
"""Test fallback to client creation when Service Quotas API fails."""
def mock_client(service_name, **kwargs):
if service_name == "service-quotas":
raise ClientError({"Error": {"Code": "AccessDenied"}}, "ListServices")
return MagicMock()
mock_session.return_value.client.side_effect = mock_client
services = get_region_available_services(mock_session.return_value, "us-east-1")
assert len(services) > 0
common_service_ids = [service["id"] for service in services]
for service_id in ["ec2", "s3", "lambda"]:
assert service_id in common_service_ids
for service in services:
assert "id" in service
assert "name" in service
@patch("aws_mcp_server.resources.get_region_available_services")
@patch("boto3.session.Session")
def test_get_region_details(mock_session, mock_get_region_available_services):
"""Test retrieving detailed AWS region information."""
mock_ec2 = MagicMock()
def mock_client(service_name, **kwargs):
if service_name == "ec2":
return mock_ec2
return MagicMock()
mock_session.return_value.client.side_effect = mock_client
mock_ec2.describe_availability_zones.return_value = {
"AvailabilityZones": [
{
"ZoneName": "us-east-1a",
"State": "available",
"ZoneId": "use1-az1",
"ZoneType": "availability-zone",
},
{
"ZoneName": "us-east-1b",
"State": "available",
"ZoneId": "use1-az2",
"ZoneType": "availability-zone",
},
]
}
mock_services = [
{"id": "ec2", "name": "EC2"},
{"id": "s3", "name": "S3"},
{"id": "lambda", "name": "Lambda"},
]
mock_get_region_available_services.return_value = mock_services
region_details = get_region_details("us-east-1")
assert region_details["code"] == "us-east-1"
assert region_details["name"] == "US East (N. Virginia)"
geo_location = region_details["geographic_location"]
assert geo_location["continent"] == "North America"
assert geo_location["country"] == "United States"
assert geo_location["city"] == "Ashburn, Virginia"
assert len(region_details["availability_zones"]) == 2
assert region_details["availability_zones"][0]["name"] == "us-east-1a"
assert region_details["availability_zones"][1]["name"] == "us-east-1b"
assert region_details["services"] == mock_services
mock_get_region_available_services.assert_called_once_with(mock_session.return_value, "us-east-1")
@patch("aws_mcp_server.resources.get_region_available_services")
@patch("boto3.session.Session")
def test_get_region_details_with_error(mock_session, mock_get_region_available_services):
"""Test region details with API errors."""
mock_session.return_value.client.side_effect = ClientError(
{"Error": {"Code": "AccessDenied", "Message": "Access denied"}},
"DescribeAvailabilityZones",
)
mock_get_region_available_services.return_value = []
region_details = get_region_details("us-east-1")
assert region_details["code"] == "us-east-1"
assert region_details["name"] == "US East (N. Virginia)"
assert "geographic_location" in region_details
assert len(region_details["availability_zones"]) == 0
assert region_details["services"] == []
mock_get_region_available_services.assert_called_once_with(mock_session.return_value, "us-east-1")
@patch("aws_mcp_server.resources.get_region_details")
def test_resource_aws_region_details(mock_get_region_details):
"""Test the aws_region_details resource function implementation."""
mock_region_details = {
"code": "us-east-1",
"name": "US East (N. Virginia)",
"geographic_location": {
"continent": "North America",
"country": "United States",
"city": "Ashburn, Virginia",
},
"availability_zones": [
{
"name": "us-east-1a",
"state": "available",
"zone_id": "use1-az1",
"zone_type": "availability-zone",
},
{
"name": "us-east-1b",
"state": "available",
"zone_id": "use1-az2",
"zone_type": "availability-zone",
},
],
"services": [
{"id": "ec2", "name": "EC2"},
{"id": "s3", "name": "S3"},
{"id": "lambda", "name": "Lambda"},
],
"is_current": True,
}
mock_get_region_details.return_value = mock_region_details
async def mock_resource_function(region: str):
return mock_get_region_details(region)
import asyncio
result = asyncio.run(mock_resource_function("us-east-1"))
mock_get_region_details.assert_called_once_with("us-east-1")
assert result == mock_region_details