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)