Files
ServiceMap-Public/backend/tests/test_docker_provider.py
T

245 lines
8.2 KiB
Python

import httpx
import pytest
from app import docker_provider
def test_broker_mode_never_falls_back_to_a_socket_or_request_provided_host(monkeypatch) -> None:
monkeypatch.setenv("SERVICEMAP_DOCKER_PROVIDER", "broker")
monkeypatch.delenv("SERVICEMAP_DOCKER_BROKER_URL", raising=False)
monkeypatch.delenv("SERVICEMAP_DOCKER_BROKER_TOKEN_FILE", raising=False)
socket_used = False
def socket_client(*args, **kwargs):
nonlocal socket_used
socket_used = True
raise AssertionError("broker mode must not create a socket client")
monkeypatch.setattr(docker_provider.docker, "DockerClient", socket_client)
with pytest.raises(docker_provider.DockerProviderError, match="Broker mode requires"):
docker_provider.docker_client_for_inventory("https://attacker.invalid")
assert socket_used is False
def test_broker_destination_is_deployment_configuration_only(tmp_path, monkeypatch) -> None:
token_file = tmp_path / "broker-token"
token_file.write_text("test-token", encoding="utf-8")
monkeypatch.setenv("SERVICEMAP_DOCKER_PROVIDER", "broker")
monkeypatch.setenv("SERVICEMAP_DOCKER_BROKER_URL", "https://broker.internal")
monkeypatch.setenv("SERVICEMAP_DOCKER_BROKER_TOKEN_FILE", str(token_file))
client = docker_provider.docker_client_for_inventory("https://attacker.invalid")
assert isinstance(client, docker_provider.BrokerDockerClient)
assert client.base_url == "https://broker.internal"
client.close()
def _mocked_broker_client(
monkeypatch, handler, *, base_url="https://broker.internal", token="opaque-token"
):
original_client = httpx.Client
transport = httpx.MockTransport(handler)
options = {}
def client_factory(**kwargs):
options.update(kwargs)
return original_client(transport=transport, **kwargs)
monkeypatch.setattr(docker_provider.httpx, "Client", client_factory)
client = docker_provider.BrokerDockerClient(base_url, token)
return client, options
def test_inventory_uses_authenticated_fixed_get_and_health_check_has_no_token(monkeypatch) -> None:
requests = []
def handler(request):
requests.append(request)
if request.url.path == "/healthz":
return httpx.Response(200)
return httpx.Response(200, json={"items": []})
client, options = _mocked_broker_client(monkeypatch, handler)
try:
assert client.ping() is True
assert client.containers.list() == []
finally:
client.close()
assert [(request.method, request.url.path) for request in requests] == [
("GET", "/healthz"),
("GET", "/v1/containers"),
]
assert "authorization" not in requests[0].headers
assert requests[1].headers["authorization"] == "Bearer opaque-token"
assert options["follow_redirects"] is False
assert options["trust_env"] is False
assert 0 < options["timeout"].connect <= 5
assert 0 < options["timeout"].read <= 5
def test_broker_rejects_other_authenticated_paths_without_requesting_them(monkeypatch) -> None:
requests = []
client, _ = _mocked_broker_client(
monkeypatch,
lambda request: requests.append(request) or httpx.Response(200, json={"items": []}),
)
try:
with pytest.raises(docker_provider.DockerProviderError, match="unsupported"):
client._get("/v1/containers/other")
finally:
client.close()
assert requests == []
@pytest.mark.parametrize(
"base_url",
[
"ftp://broker.internal",
"https://user:password@broker.internal",
"https://broker.internal?token=secret",
"https://broker.internal/#fragment",
"https:///missing-host",
],
)
def test_broker_rejects_unsafe_destinations(base_url) -> None:
with pytest.raises(docker_provider.DockerProviderError):
docker_provider.BrokerDockerClient(base_url, "opaque-token")
def test_broker_rejects_header_injection_in_token() -> None:
with pytest.raises(docker_provider.DockerProviderError):
docker_provider.BrokerDockerClient("https://broker.internal", "token\r\nX-Leak: yes")
@pytest.mark.parametrize(
"payload",
[
{},
{"items": "not-a-list"},
{"items": [{"containerRef": "one"}, "malformed-row"]},
{"items": [{"containerRef": "one", "name": "one", "state": "running"}]},
],
)
def test_broker_rejects_partial_or_malformed_inventory(payload, monkeypatch) -> None:
client, _ = _mocked_broker_client(
monkeypatch,
lambda request: httpx.Response(200, json=payload),
)
try:
with pytest.raises(docker_provider.DockerProviderError, match="invalid"):
client.containers.list()
finally:
client.close()
def test_broker_rejects_non_json_inventory_response(monkeypatch) -> None:
client, _ = _mocked_broker_client(
monkeypatch,
lambda request: httpx.Response(
200, content=b'{"items":[]}', headers={"content-type": "text/plain"}
),
)
try:
with pytest.raises(docker_provider.DockerProviderError, match="invalid"):
client.containers.list()
finally:
client.close()
def test_broker_rejects_oversized_inventory_response(monkeypatch) -> None:
monkeypatch.setattr(docker_provider, "_BROKER_MAX_RESPONSE_BYTES", 4)
client, _ = _mocked_broker_client(
monkeypatch,
lambda request: httpx.Response(200, content=b'{"items":[]}'),
)
try:
with pytest.raises(docker_provider.DockerProviderError, match="invalid"):
client.containers.list()
finally:
client.close()
def test_broker_container_preserves_discovery_fields_without_exposing_environment(
monkeypatch,
) -> None:
item = {
"containerRef": "abcdef1234567890",
"name": "/example-service",
"image": "example/service:1.0",
"state": "running",
"health": "healthy",
"healthcheckPresent": True,
"labels": {"servicemap.enabled": "true"},
"networks": ["default"],
"ports": {
"8000/tcp": [{"hostIp": "0.0.0.0", "hostPort": "1253"}],
"5432/tcp": None,
},
}
client, _ = _mocked_broker_client(
monkeypatch,
lambda request: httpx.Response(200, json={"items": [item]}),
)
try:
[container] = client.containers.list()
finally:
client.close()
assert container.id == item["containerRef"]
assert container.name == "example-service"
assert container.image.tags == [item["image"]]
assert container.attrs["State"] == {"Status": "running", "Health": {"Status": "healthy"}}
assert container.attrs["Config"]["Labels"] == item["labels"]
assert container.attrs["Config"]["Healthcheck"] == {"BrokerReported": True}
assert container.attrs["Config"]["Env"] == []
assert container.attrs["NetworkSettings"]["Networks"] == {"default": {}}
assert container.attrs["NetworkSettings"]["Ports"] == {
"8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": "1253"}],
"5432/tcp": [],
}
def test_broker_health_status_proves_healthcheck_when_old_schema_omits_flag(monkeypatch) -> None:
item = {
"containerRef": "abcdef1234567890",
"name": "example-service",
"image": "example/service:1.0",
"state": "running",
"health": "healthy",
"labels": {},
"networks": [],
"ports": {},
}
client, _ = _mocked_broker_client(
monkeypatch, lambda request: httpx.Response(200, json={"items": [item]})
)
try:
[container] = client.containers.list()
finally:
client.close()
assert container.attrs["Config"]["Healthcheck"] == {"BrokerReported": True}
def test_broker_error_messages_do_not_expose_destination_or_credential(monkeypatch) -> None:
def handler(request):
raise httpx.ConnectError(
"failed with opaque-token at https://broker.internal", request=request
)
client, _ = _mocked_broker_client(monkeypatch, handler)
try:
with pytest.raises(docker_provider.DockerProviderError) as caught:
client.containers.list()
finally:
client.close()
assert "Docker broker request failed" in str(caught.value)
assert "opaque-token" not in str(caught.value)
assert "broker.internal" not in str(caught.value)