Spaces:
Paused
Paused
| """Tests for the API server bind-address startup guard. | |
| Validates that is_network_accessible() correctly classifies addresses and | |
| that connect() refuses to start on non-loopback without API_SERVER_KEY. | |
| """ | |
| import socket | |
| from unittest.mock import AsyncMock, patch | |
| import pytest | |
| from gateway.config import PlatformConfig | |
| from gateway.platforms.api_server import APIServerAdapter | |
| from gateway.platforms.base import is_network_accessible | |
| # --------------------------------------------------------------------------- | |
| # Unit tests: is_network_accessible() | |
| # --------------------------------------------------------------------------- | |
| class TestIsNetworkAccessible: | |
| """Direct tests for the address classification helper.""" | |
| # -- Loopback (safe, should return False) -- | |
| def test_ipv4_loopback(self): | |
| assert is_network_accessible("127.0.0.1") is False | |
| def test_ipv6_loopback(self): | |
| assert is_network_accessible("::1") is False | |
| def test_ipv4_mapped_loopback(self): | |
| # ::ffff:127.0.0.1 — Python's is_loopback returns False for mapped | |
| # addresses; the helper must unwrap and check ipv4_mapped. | |
| assert is_network_accessible("::ffff:127.0.0.1") is False | |
| # -- Network-accessible (should return True) -- | |
| def test_ipv4_wildcard(self): | |
| assert is_network_accessible("0.0.0.0") is True | |
| def test_ipv6_wildcard(self): | |
| # This is the bypass vector that the string-based check missed. | |
| assert is_network_accessible("::") is True | |
| def test_ipv4_mapped_unspecified(self): | |
| assert is_network_accessible("::ffff:0.0.0.0") is True | |
| def test_private_ipv4(self): | |
| assert is_network_accessible("10.0.0.1") is True | |
| def test_private_ipv4_class_c(self): | |
| assert is_network_accessible("192.168.1.1") is True | |
| def test_public_ipv4(self): | |
| assert is_network_accessible("8.8.8.8") is True | |
| # -- Hostname resolution -- | |
| def test_localhost_resolves_to_loopback(self): | |
| loopback_result = [ | |
| (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("127.0.0.1", 0)), | |
| ] | |
| with patch("gateway.platforms.base._socket.getaddrinfo", return_value=loopback_result): | |
| assert is_network_accessible("localhost") is False | |
| def test_hostname_resolving_to_non_loopback(self): | |
| non_loopback_result = [ | |
| (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("10.0.0.1", 0)), | |
| ] | |
| with patch("gateway.platforms.base._socket.getaddrinfo", return_value=non_loopback_result): | |
| assert is_network_accessible("my-server.local") is True | |
| def test_hostname_mixed_resolution(self): | |
| """If a hostname resolves to both loopback and non-loopback, it's | |
| network-accessible (any non-loopback address is enough).""" | |
| mixed_result = [ | |
| (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("127.0.0.1", 0)), | |
| (socket.AF_INET, socket.SOCK_STREAM, 0, "", ("10.0.0.1", 0)), | |
| ] | |
| with patch("gateway.platforms.base._socket.getaddrinfo", return_value=mixed_result): | |
| assert is_network_accessible("dual-host.local") is True | |
| def test_dns_failure_fails_closed(self): | |
| """Unresolvable hostnames should require an API key (fail closed).""" | |
| with patch( | |
| "gateway.platforms.base._socket.getaddrinfo", | |
| side_effect=socket.gaierror("Name resolution failed"), | |
| ): | |
| assert is_network_accessible("nonexistent.invalid") is True | |
| # --------------------------------------------------------------------------- | |
| # Integration tests: connect() startup guard | |
| # --------------------------------------------------------------------------- | |
| class TestConnectBindGuard: | |
| """Verify that connect() refuses dangerous configurations.""" | |
| async def test_refuses_ipv4_wildcard_without_key(self): | |
| adapter = APIServerAdapter(PlatformConfig(enabled=True, extra={"host": "0.0.0.0"})) | |
| result = await adapter.connect() | |
| assert result is False | |
| async def test_refuses_ipv6_wildcard_without_key(self): | |
| adapter = APIServerAdapter(PlatformConfig(enabled=True, extra={"host": "::"})) | |
| result = await adapter.connect() | |
| assert result is False | |
| def test_allows_loopback_without_key(self): | |
| """Loopback with no key should pass the guard.""" | |
| adapter = APIServerAdapter(PlatformConfig(enabled=True, extra={"host": "127.0.0.1"})) | |
| assert adapter._api_key == "" | |
| # The guard condition: is_network_accessible(host) AND NOT api_key | |
| # For loopback, is_network_accessible is False so the guard does not block. | |
| assert is_network_accessible(adapter._host) is False | |
| async def test_allows_wildcard_with_key(self): | |
| """Non-loopback with a key should pass the guard.""" | |
| adapter = APIServerAdapter( | |
| PlatformConfig(enabled=True, extra={"host": "0.0.0.0", "key": "sk-test"}) | |
| ) | |
| # The guard checks: is_network_accessible(host) AND NOT api_key | |
| # With a key set, the guard should not block. | |
| assert adapter._api_key == "sk-test" | |
| assert is_network_accessible("0.0.0.0") is True | |
| # Combined: the guard condition is False (key is set), so it passes | |