Files
robertvoy-ComfyUI-Distributed/tests/api/test_auth_guard_integration.py
Robert Wojciechowski 3058b11ff2 Remove list splitter/collector feature and refactor distributed modules
- Remove DistributedListSplitter and DistributedListCollector nodes
- Remove /distributed/init_list_queue and /distributed/request_list_item endpoints
- Clean up all references from prompt_transform, queue_orchestration, constants, and tests
- Refactor and reorganize distributed modules for quality improvements
2026-03-07 05:22:09 +00:00

424 lines
17 KiB
Python

import asyncio
import copy
import importlib.util
import sys
import types
import unittest
from contextlib import asynccontextmanager
from pathlib import Path
from tests.api.harness import (
bootstrap_test_package,
cleanup_optional_module,
install_aiohttp_stub,
install_server_stub,
)
AUTH_TOKEN = "integration-secret"
class _FakeResponse:
def __init__(self, payload, status=200):
self.payload = payload
self.status = status
class _FakeRequest:
def __init__(self, *, headers=None, json_payload=None, post_payload=None, query=None):
self.headers = headers or {}
self._json_payload = json_payload or {}
self._post_payload = post_payload or {}
self.query = query or {}
async def json(self):
return self._json_payload
async def post(self):
return self._post_payload
def _load_module(package_name: str, rel_path: str, module_name: str):
module_path = Path(__file__).resolve().parents[2] / rel_path
spec = importlib.util.spec_from_file_location(f"{package_name}.{module_name}", module_path)
module = importlib.util.module_from_spec(spec)
assert spec is not None and spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _install_common_utils(package_name: str, config_data: dict):
logging_module = types.ModuleType(f"{package_name}.utils.logging")
logging_module.debug_log = lambda *_args, **_kwargs: None
logging_module.log = lambda *_args, **_kwargs: None
sys.modules[f"{package_name}.utils.logging"] = logging_module
config_module = types.ModuleType(f"{package_name}.utils.config")
config_module.load_config = lambda: copy.deepcopy(config_data)
config_module.save_config = lambda _cfg: True
@asynccontextmanager
async def _config_transaction():
yield config_module.load_config()
config_module.config_transaction = _config_transaction
sys.modules[f"{package_name}.utils.config"] = config_module
network_module = types.ModuleType(f"{package_name}.utils.network")
async def _handle_api_error(_request, error, status=500):
return _FakeResponse({"status": "error", "message": str(error)}, status=status)
network_module.handle_api_error = _handle_api_error
network_module.normalize_host = lambda value: value
network_module.build_worker_url = lambda worker, path="": f"http://{worker.get('host', '127.0.0.1')}{path}"
network_module.get_client_session = lambda: None
network_module.probe_worker = lambda *_args, **_kwargs: {"ok": True}
sys.modules[f"{package_name}.utils.network"] = network_module
def _load_real_guard_stack(package_name: str):
_load_module(package_name, "utils/auth.py", "utils.auth")
_load_module(package_name, "utils/parsing.py", "utils.parsing")
_load_module(package_name, "api/schemas.py", "api.schemas")
_load_module(package_name, "api/request_guards.py", "api.request_guards")
_load_module(package_name, "api/endpoint_policy.py", "api.endpoint_policy")
def _load_request_guards_only_module():
package_name = "dist_auth_guard_testpkg"
config_data = {"settings": {"distributed_api_token": AUTH_TOKEN}}
bootstrap_test_package(package_name, with_api=True, with_utils=True)
install_server_stub()
created_aiohttp_stub = install_aiohttp_stub(
lambda payload, status=200: _FakeResponse(payload, status=status)
)
_install_common_utils(package_name, config_data)
_load_real_guard_stack(package_name)
request_guards = sys.modules[f"{package_name}.api.request_guards"]
schemas = sys.modules[f"{package_name}.api.schemas"]
cleanup_optional_module("aiohttp", created_aiohttp_stub)
return request_guards, schemas
def _load_config_routes_real_guard_module():
package_name = "dist_config_auth_integration_testpkg"
config_data = {
"workers": [],
"master": {"host": "127.0.0.1"},
"settings": {"distributed_api_token": AUTH_TOKEN},
"tunnel": {},
}
bootstrap_test_package(package_name, with_api=True, with_utils=True)
install_server_stub()
created_aiohttp_stub = install_aiohttp_stub(
lambda payload, status=200: _FakeResponse(payload, status=status)
)
_install_common_utils(package_name, config_data)
_load_real_guard_stack(package_name)
module = _load_module(package_name, "api/config_routes.py", "api.config_routes")
cleanup_optional_module("aiohttp", created_aiohttp_stub)
return module
def _load_tunnel_routes_real_guard_module():
package_name = "dist_tunnel_auth_integration_testpkg"
config_data = {
"workers": [],
"master": {"host": "127.0.0.1"},
"settings": {"distributed_api_token": AUTH_TOKEN},
"tunnel": {},
}
bootstrap_test_package(package_name, with_api=True, with_utils=True)
install_server_stub()
created_aiohttp_stub = install_aiohttp_stub(
lambda payload, status=200: _FakeResponse(payload, status=status)
)
_install_common_utils(package_name, config_data)
_load_real_guard_stack(package_name)
cloudflare_module = types.ModuleType(f"{package_name}.utils.cloudflare")
cloudflare_module.cloudflare_tunnel_manager = types.SimpleNamespace(
get_status=lambda: {"active": False},
start_tunnel=lambda: {"active": True},
stop_tunnel=lambda: {"active": False},
)
sys.modules[f"{package_name}.utils.cloudflare"] = cloudflare_module
module = _load_module(package_name, "api/tunnel_routes.py", "api.tunnel_routes")
cleanup_optional_module("aiohttp", created_aiohttp_stub)
return module
def _load_usdu_routes_real_guard_module():
package_name = "dist_usdu_auth_integration_testpkg"
config_data = {"settings": {"distributed_api_token": AUTH_TOKEN}}
bootstrap_test_package(package_name, with_api=True, with_utils=True, with_upscale=True)
install_server_stub()
created_aiohttp_stub = install_aiohttp_stub(
lambda payload, status=200: _FakeResponse(payload, status=status)
)
_install_common_utils(package_name, config_data)
_load_real_guard_stack(package_name)
usdu_management_module = types.ModuleType(f"{package_name}.utils.usdu_management")
usdu_management_module.MAX_PAYLOAD_SIZE = 1024 * 1024
sys.modules[f"{package_name}.utils.usdu_management"] = usdu_management_module
prompt_server_holder = {
"value": types.SimpleNamespace(
distributed_tile_jobs_lock=asyncio.Lock(),
distributed_pending_tile_jobs={},
)
}
job_store_module = types.ModuleType(f"{package_name}.upscale.job_store")
job_store_module.ensure_tile_jobs_initialized = lambda: prompt_server_holder["value"]
job_store_module.init_dynamic_job = lambda *_args, **_kwargs: None
sys.modules[f"{package_name}.upscale.job_store"] = job_store_module
job_models_module = types.ModuleType(f"{package_name}.upscale.job_models")
class _BaseJobState:
pass
class _ImageJobState(_BaseJobState):
def __init__(self):
self.mode = "dynamic"
self.queue = asyncio.Queue()
self.pending_images = asyncio.Queue()
self.completed_images = {}
self.worker_status = {}
self.assigned_to_workers = {}
class _TileJobState(_BaseJobState):
def __init__(self):
self.mode = "static"
self.queue = asyncio.Queue()
self.pending_tasks = asyncio.Queue()
self.completed_tasks = {}
self.worker_status = {}
self.assigned_to_workers = {}
self.batched_static = False
job_models_module.BaseJobState = _BaseJobState
job_models_module.ImageJobState = _ImageJobState
job_models_module.TileJobState = _TileJobState
sys.modules[f"{package_name}.upscale.job_models"] = job_models_module
parsers_module = types.ModuleType(f"{package_name}.upscale.payload_parsers")
parsers_module.parse_tiles_from_form = lambda _data: []
sys.modules[f"{package_name}.upscale.payload_parsers"] = parsers_module
module = _load_module(package_name, "api/usdu_routes.py", "api.usdu_routes")
module._prompt_server_holder = prompt_server_holder
module._ImageJobState = _ImageJobState
cleanup_optional_module("aiohttp", created_aiohttp_stub)
return module
def _install_torch_stub_if_missing():
if "torch" in sys.modules:
return False
torch_module = types.ModuleType("torch")
torch_module.cuda = types.SimpleNamespace(
is_available=lambda: False,
empty_cache=lambda: None,
ipc_collect=lambda: None,
)
sys.modules["torch"] = torch_module
return True
def _load_worker_routes_real_guard_module():
package_name = "dist_worker_auth_integration_testpkg"
config_data = {"settings": {"distributed_api_token": AUTH_TOKEN}, "workers": []}
bootstrap_test_package(package_name, with_api=True, with_utils=True, with_workers=True)
install_server_stub()
created_aiohttp_stub = install_aiohttp_stub(
lambda payload, status=200: _FakeResponse(payload, status=status)
)
created_torch_stub = _install_torch_stub_if_missing()
_install_common_utils(package_name, config_data)
_load_real_guard_stack(package_name)
constants_module = types.ModuleType(f"{package_name}.utils.constants")
constants_module.CHUNK_SIZE = 65536
sys.modules[f"{package_name}.utils.constants"] = constants_module
workers_module = types.ModuleType(f"{package_name}.workers")
workers_module.get_worker_manager = lambda: types.SimpleNamespace(
processes={},
save_processes=lambda: None,
is_process_running=lambda _pid: False,
)
sys.modules[f"{package_name}.workers"] = workers_module
detection_module = types.ModuleType(f"{package_name}.workers.detection")
detection_module.get_machine_id = lambda: "machine-id"
detection_module.is_docker_environment = lambda: False
detection_module.is_runpod_environment = lambda: False
sys.modules[f"{package_name}.workers.detection"] = detection_module
async_helpers_module = types.ModuleType(f"{package_name}.utils.async_helpers")
class _PromptValidationError(Exception):
def __init__(self, message="validation error"):
super().__init__(message)
self.validation_error = {}
self.node_errors = {}
async def _queue_prompt_payload(*_args, **_kwargs):
return "prompt-id"
async_helpers_module.PromptValidationError = _PromptValidationError
async_helpers_module.queue_prompt_payload = _queue_prompt_payload
sys.modules[f"{package_name}.utils.async_helpers"] = async_helpers_module
module = _load_module(package_name, "api/worker_routes.py", "api.worker_routes")
cleanup_optional_module("aiohttp", created_aiohttp_stub)
if created_torch_stub:
sys.modules.pop("torch", None)
return module
def _load_job_routes_real_guard_module():
package_name = "dist_job_auth_integration_testpkg"
config_data = {"settings": {"distributed_api_token": AUTH_TOKEN}}
bootstrap_test_package(package_name, with_api=True, with_utils=True)
install_server_stub()
created_aiohttp_stub = install_aiohttp_stub(
lambda payload, status=200: _FakeResponse(payload, status=status)
)
created_torch_stub = _install_torch_stub_if_missing()
_install_common_utils(package_name, config_data)
_load_real_guard_stack(package_name)
image_module = types.ModuleType(f"{package_name}.utils.image")
image_module.pil_to_tensor = lambda _img: None
image_module.ensure_contiguous = lambda tensor: tensor
sys.modules[f"{package_name}.utils.image"] = image_module
constants_module = types.ModuleType(f"{package_name}.utils.constants")
constants_module.JOB_INIT_GRACE_PERIOD = 0.1
constants_module.MEMORY_CLEAR_DELAY = 0.0
sys.modules[f"{package_name}.utils.constants"] = constants_module
runtime_state_module = types.ModuleType(f"{package_name}.utils.runtime_state")
runtime_state = types.SimpleNamespace(
distributed_jobs_lock=asyncio.Lock(),
distributed_pending_jobs={},
)
runtime_state_module.ensure_distributed_runtime_state = lambda: runtime_state
sys.modules[f"{package_name}.utils.runtime_state"] = runtime_state_module
orchestration_module = types.ModuleType(f"{package_name}.api.queue_orchestration")
orchestration_module.orchestrate_distributed_execution = lambda *_args, **_kwargs: None
sys.modules[f"{package_name}.api.queue_orchestration"] = orchestration_module
queue_request_module = types.ModuleType(f"{package_name}.api.queue_request")
queue_request_module.parse_queue_request_payload = lambda data: data
sys.modules[f"{package_name}.api.queue_request"] = queue_request_module
module = _load_module(package_name, "api/job_routes.py", "api.job_routes")
cleanup_optional_module("aiohttp", created_aiohttp_stub)
if created_torch_stub:
sys.modules.pop("torch", None)
return module
request_guards_module, schemas_module = _load_request_guards_only_module()
config_routes_module = _load_config_routes_real_guard_module()
tunnel_routes_module = _load_tunnel_routes_real_guard_module()
usdu_routes_module = _load_usdu_routes_real_guard_module()
worker_routes_module = _load_worker_routes_real_guard_module()
job_routes_module = _load_job_routes_real_guard_module()
class AuthGuardContractTests(unittest.IsolatedAsyncioTestCase):
async def test_is_authorized_request_missing_token_fails(self):
request = _FakeRequest(headers={})
config = {"settings": {"distributed_api_token": AUTH_TOKEN}}
self.assertFalse(schemas_module.is_authorized_request(request, config))
async def test_is_authorized_request_matching_token_succeeds(self):
request = _FakeRequest(headers={schemas_module.AUTH_HEADER_NAME: AUTH_TOKEN})
config = {"settings": {"distributed_api_token": AUTH_TOKEN}}
self.assertTrue(schemas_module.is_authorized_request(request, config))
async def test_authorization_error_or_none_returns_403_for_invalid_token(self):
request = _FakeRequest(headers={schemas_module.AUTH_HEADER_NAME: "wrong-token"})
response = await request_guards_module.authorization_error_or_none(request)
self.assertIsNotNone(response)
self.assertEqual(response.status, 403)
async def test_authorization_error_or_none_returns_none_for_valid_token(self):
request = _FakeRequest(headers={schemas_module.AUTH_HEADER_NAME: AUTH_TOKEN})
response = await request_guards_module.authorization_error_or_none(request)
self.assertIsNone(response)
class RouteAuthIntegrationTests(unittest.IsolatedAsyncioTestCase):
def _auth_headers(self):
return {schemas_module.AUTH_HEADER_NAME: AUTH_TOKEN}
async def test_config_endpoint_enforces_real_guard(self):
unauthorized = await config_routes_module.get_config_endpoint(_FakeRequest(headers={}))
self.assertEqual(unauthorized.status, 403)
authorized = await config_routes_module.get_config_endpoint(_FakeRequest(headers=self._auth_headers()))
self.assertEqual(authorized.status, 200)
self.assertIn("settings", authorized.payload)
async def test_tunnel_endpoint_enforces_real_guard(self):
unauthorized = await tunnel_routes_module.tunnel_status_endpoint(_FakeRequest(headers={}))
self.assertEqual(unauthorized.status, 403)
authorized = await tunnel_routes_module.tunnel_status_endpoint(_FakeRequest(headers=self._auth_headers()))
self.assertEqual(authorized.status, 200)
self.assertEqual(authorized.payload.get("status"), "success")
async def test_usdu_endpoint_enforces_real_guard(self):
unauthorized = await usdu_routes_module.job_status_endpoint(_FakeRequest(headers={}))
self.assertEqual(unauthorized.status, 403)
authorized = await usdu_routes_module.job_status_endpoint(
_FakeRequest(headers=self._auth_headers(), query={"multi_job_id": "job-1"})
)
self.assertEqual(authorized.status, 200)
self.assertIn("ready", authorized.payload)
async def test_worker_endpoint_enforces_real_guard(self):
worker_routes_module._collect_network_info_sync = lambda: {
"interfaces": [],
"recommended_host": "127.0.0.1",
}
unauthorized = await worker_routes_module.get_network_info_endpoint(_FakeRequest(headers={}))
self.assertEqual(unauthorized.status, 403)
authorized = await worker_routes_module.get_network_info_endpoint(
_FakeRequest(headers=self._auth_headers())
)
self.assertEqual(authorized.status, 200)
self.assertEqual(authorized.payload.get("status"), "success")
async def test_job_endpoint_enforces_real_guard(self):
unauthorized = await job_routes_module.prepare_job_endpoint(
_FakeRequest(headers={}, json_payload={"multi_job_id": "job-1"})
)
self.assertEqual(unauthorized.status, 403)
authorized = await job_routes_module.prepare_job_endpoint(
_FakeRequest(headers=self._auth_headers(), json_payload={"multi_job_id": "job-1"})
)
self.assertEqual(authorized.status, 200)
self.assertEqual(authorized.payload.get("status"), "success")
if __name__ == "__main__":
unittest.main()