"""云厂商适配器单元测试 通过 mock 各平台 API 响应,验证适配器的标准化转换逻辑、签名算法正确性 与注册表路由。确保用户填入真实凭证后,解析与签名链路可靠。 运行:.venv/bin/pytest tests/ -v """ import base64 from unittest.mock import patch from app.adapters import registry from app.adapters.ai import DeepSeekAdapter, KimiAdapter, OpenAIAdapter from app.adapters.aliyun import AliyunAdapter from app.adapters.cloudflare import CloudflareAdapter from app.adapters.digitalocean import DigitalOceanAdapter from app.adapters.tencent import TencentAdapter from app.adapters.vultr import VultrAdapter # ----------------------------- Vultr ----------------------------- # def test_vultr_list_vps_normalization(): adapter = VultrAdapter({"api_key": "x"}) mock_resp = { "instances": [ { "id": "vps-1", "label": "my-vps", "main_ip": "1.2.3.4", "region": "tokyo", "os": "Debian 12", "vcpu_count": 2, "ram": 4096, "disk": 80, "status": "active", "monthly_cost": 10.0, } ] } with patch.object(VultrAdapter, "_get", return_value=mock_resp): result = adapter.list_vps() assert len(result) == 1 v = result[0] assert v.external_id == "vps-1" assert v.name == "my-vps" assert v.memory_gb == 4.0 # 4096 MB -> 4 GB assert v.ip_address == "1.2.3.4" assert v.cpu_cores == 2 assert v.monthly_cost == 10.0 assert v.status == "active" def test_vultr_get_account(): adapter = VultrAdapter({"api_key": "x"}) with patch.object( VultrAdapter, "_get", return_value={"account": {"balance": -50.0, "pending_charges": 12.3}} ): acc = adapter.get_account() assert acc.balance == -50.0 assert acc.pending_charges == 12.3 assert acc.currency == "USD" # --------------------------- DigitalOcean --------------------------- # def test_do_list_vps_normalization(): adapter = DigitalOceanAdapter({"api_key": "x"}) mock_resp = { "droplets": [ { "id": 123, "name": "droplet-1", "vcpus": 1, "memory": 2048, "disk": 25, "status": "active", "region": {"slug": "sgp1"}, "image": {"distribution": "Debian", "name": "12 x64"}, "networks": {"v4": [{"ip_address": "5.6.7.8"}]}, } ] } with patch.object(DigitalOceanAdapter, "_get", return_value=mock_resp): result = adapter.list_vps() v = result[0] assert v.external_id == "123" assert v.memory_gb == 2.0 # 2048 MB -> 2 GB assert v.ip_address == "5.6.7.8" assert v.region == "sgp1" assert "Debian" in v.os # --------------------------- Cloudflare --------------------------- # def test_cloudflare_list_domains(): adapter = CloudflareAdapter({"api_token": "x"}) mock_resp = {"result": [{"id": "z1", "name": "example.com", "status": "active"}]} with patch.object(CloudflareAdapter, "_get", return_value=mock_resp): domains = adapter.list_domains() assert len(domains) == 1 assert domains[0].domain_name == "example.com" assert domains[0].external_id == "z1" assert domains[0].registrar == "cloudflare" def test_cloudflare_capabilities_no_vps(): caps = CloudflareAdapter({"api_token": "x"}).capabilities() assert caps["list_domains"] is True assert caps["list_vps"] is False # Cloudflare 无传统 VPS # --------------------------- 阿里云签名 --------------------------- # def test_aliyun_sign_deterministic_and_valid(): adapter = AliyunAdapter({"access_key_id": "id", "access_key_secret": "secret"}) params = { "Action": "DescribeRegions", "RegionId": "cn-hangzhou", "Timestamp": "2024-01-01T00:00:00Z", "SignatureNonce": "fixed-nonce", } sig1 = adapter._sign(params) sig2 = adapter._sign(params) assert sig1 == sig2 # 确定性 base64.b64decode(sig1) # 合法 base64(HMAC-SHA1 -> 28 字符 base64) assert len(sig1) == 28 def test_aliyun_sign_sensitive_to_secret(): params = {"Action": "DescribeRegions", "Timestamp": "2024-01-01T00:00:00Z", "SignatureNonce": "n"} a1 = AliyunAdapter({"access_key_id": "id", "access_key_secret": "secret1"}) a2 = AliyunAdapter({"access_key_id": "id", "access_key_secret": "secret2"}) assert a1._sign(params) != a2._sign(params) # --------------------------- 腾讯云签名 --------------------------- # def test_tencent_authorization_format(): adapter = TencentAdapter({"secret_id": "SID", "secret_key": "SKEY"}) auth = adapter._build_authorization("DescribeInstances", "{}", 1704067200, "2024-01-01") assert auth.startswith("TC3-HMAC-SHA256 Credential=SID/") assert "2024-01-01/cvm/tc3_request" in auth assert "SignedHeaders=content-type;host;x-tc-action" in auth assert "Signature=" in auth # 签名为 64 位十六进制(HMAC-SHA256) signature = auth.split("Signature=")[1] assert len(signature) == 64 int(signature, 16) def test_tencent_sign_sensitive_to_payload(): adapter = TencentAdapter({"secret_id": "SID", "secret_key": "SKEY"}) a1 = adapter._build_authorization("DescribeInstances", '{"Limit":1}', 1704067200, "2024-01-01") a2 = adapter._build_authorization("DescribeInstances", '{"Limit":2}', 1704067200, "2024-01-01") assert a1 != a2 # --------------------------- 注册表路由 --------------------------- # def test_registry_supported_types(): for t in [ "vultr-api", "do-api", "cloudflare-api", "aliyun-sdk", "alibabacloud-sdk", "tencent-sdk", "tencent-intl-sdk", ]: assert registry.is_supported(t), f"{t} 应被支持" assert not registry.is_supported("nonexistent-sdk") def test_registry_get_adapter_routing(): assert isinstance(registry.get_adapter("vultr-api", {}), VultrAdapter) assert isinstance(registry.get_adapter("do-api", {}), DigitalOceanAdapter) assert isinstance(registry.get_adapter("cloudflare-api", {}), CloudflareAdapter) assert isinstance(registry.get_adapter("aliyun-sdk", {}), AliyunAdapter) assert isinstance(registry.get_adapter("alibabacloud-sdk", {}), AliyunAdapter) assert isinstance(registry.get_adapter("tencent-sdk", {}), TencentAdapter) assert isinstance(registry.get_adapter("tencent-intl-sdk", {}), TencentAdapter) # --------------------------- AI 平台适配器 --------------------------- # def test_deepseek_balance_normalization(): adapter = DeepSeekAdapter({"api_key": "x"}) mock = {"data": {"total_balance": "88.50", "granted_balance": "10.00"}} with patch.object(DeepSeekAdapter, "_get", return_value=mock): acc = adapter.get_account() assert acc.balance == 88.5 # 字符串 -> float assert acc.currency == "CNY" def test_deepseek_test_connection_includes_balance(): adapter = DeepSeekAdapter({"api_key": "x"}) with patch.object(DeepSeekAdapter, "_get", return_value={"data": {"total_balance": "88.50"}}): result = adapter.test_connection() assert result["ok"] is True assert "88.5" in result["message"] def test_kimi_balance_normalization(): adapter = KimiAdapter({"api_key": "x"}) with patch.object(KimiAdapter, "_get", return_value={"data": {"available_balance": 123.45}}): acc = adapter.get_account() assert acc.balance == 123.45 assert acc.currency == "CNY" def test_openai_test_connection_models(): adapter = OpenAIAdapter({"api_key": "x"}) mock = {"data": [{"id": "gpt-4"}, {"id": "gpt-3.5-turbo"}]} with patch.object(OpenAIAdapter, "_get", return_value=mock): result = adapter.test_connection() assert result["ok"] is True assert "2" in result["message"] def test_ai_capabilities_account_only(): caps = DeepSeekAdapter({"api_key": "x"}).capabilities() assert caps["get_account"] is True assert caps["list_vps"] is False assert caps["list_domains"] is False def test_registry_supports_ai_types(): for t in ["deepseek-api", "moonshot-api", "openai-api", "minimax-api"]: assert registry.is_supported(t), f"{t} 应被支持" # --------------------------- 分页处理 --------------------------- # def test_vultr_pagination_cursor(): adapter = VultrAdapter({"api_key": "x"}) page1 = {"instances": [{"id": "a", "label": "a", "ram": 1024}], "meta": {"links": {"next": "cursor2"}}} page2 = {"instances": [{"id": "b", "label": "b", "ram": 2048}], "meta": {"links": {"next": ""}}} with patch.object(VultrAdapter, "_get", side_effect=[page1, page2]): result = adapter.list_vps() assert len(result) == 2 assert {v.external_id for v in result} == {"a", "b"} def test_do_pagination_page(): adapter = DigitalOceanAdapter({"api_key": "x"}) page1 = {"droplets": [{"id": 1, "name": "d1", "memory": 1024, "networks": {"v4": []}}], "links": {"pages": {"next": "x"}}} page2 = {"droplets": [{"id": 2, "name": "d2", "memory": 1024, "networks": {"v4": []}}], "links": {}} with patch.object(DigitalOceanAdapter, "_get", side_effect=[page1, page2]): result = adapter.list_vps() assert len(result) == 2 assert {v.external_id for v in result} == {"1", "2"} def test_cloudflare_pagination_result_info(): adapter = CloudflareAdapter({"api_token": "x"}) page1 = {"result": [{"id": "z1", "name": "a.com", "status": "active"}], "result_info": {"total_pages": 2, "page": 1}} page2 = {"result": [{"id": "z2", "name": "b.com", "status": "active"}], "result_info": {"total_pages": 2, "page": 2}} with patch.object(CloudflareAdapter, "_registrar_expiry_map", return_value={}), \ patch.object(CloudflareAdapter, "_get", side_effect=[page1, page2]): result = adapter.list_domains() assert len(result) == 2 assert {d.domain_name for d in result} == {"a.com", "b.com"}