| """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 |
|
|
|
|
| |
| |
| |
|
|
|
|
| class TestIsNetworkAccessible: |
| """Direct tests for the address classification helper.""" |
|
|
| |
|
|
| 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): |
| |
| |
| assert is_network_accessible("::ffff:127.0.0.1") is False |
|
|
| |
|
|
| def test_ipv4_wildcard(self): |
| assert is_network_accessible("0.0.0.0") is True |
|
|
| def test_ipv6_wildcard(self): |
| |
| 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 |
|
|
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
|
|
| class TestConnectBindGuard: |
| """Verify that connect() refuses dangerous configurations.""" |
|
|
| @pytest.mark.asyncio |
| 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 |
|
|
| @pytest.mark.asyncio |
| 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 == "" |
| |
| |
| assert is_network_accessible(adapter._host) is False |
|
|
| @pytest.mark.asyncio |
| 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"}) |
| ) |
| |
| |
| assert adapter._api_key == "sk-test" |
| assert is_network_accessible("0.0.0.0") is True |
| |
|
|