Files
ss-tools/backend/tests/test_gradio_proxy_config.py
busya 20071b8c7a security: fullstack hardening — task ownership, mapping validation, API-key scoping, test fixes
Backend:
- Add validate_mapping_database_ownership() to verify source/target UUIDs
  belong to declared environments before persisting mappings (mappings.py)
- Add API-key environment scoping to get_mappings (filter) and
  suggest_mappings_api (enforce) (mappings.py)
- Add user_id Column to TaskRecord model + Alembic migration (task.py)
- Persist task.user_id on save, restore on load (persistence.py)
- Wire current_user.id into migrate_dashboards + backup_dashboards
  task creation (_action_routes.py)
- Fix test_migration_routes.py: module-level patch leak → autouse fixture,
  SupersetClient→AsyncSupersetClient, AsyncMock for sync_environment/run
- Fix 7 Pydantic serializer warnings: 'PENDING'→TaskStatus.PENDING
  in test_tasks.py + import TaskStatus

Frontend:
- Deepen isDryRunResult(): validate selection field, risk.items entries
  (all 5 fields), and diff object uuids individually (ExecutorModel.svelte.ts)

Prior work included: task password redaction, resume ownership checks,
canonical dry-run DTO alignment, migration UI callback fixes, credential
exposure reduction, assistant dry-run await fix.
2026-07-15 23:02:23 +03:00

86 lines
4.4 KiB
Python

# #region Test.Infrastructure.GradioProxy [C:3] [TYPE Module] [SEMANTICS test,nginx,docker,agent,gradio]
# @BRIEF Verify Gradio agent proxy contracts across nginx, compose, and agent launch config.
# @RELATION BINDS_TO -> [docker.nginx.conf]
# @RELATION BINDS_TO -> [docker.nginx.ssl.conf]
# @RELATION BINDS_TO -> [AgentChat.Config]
# @TEST_EDGE: docker_dns_name -> Proxy targets compose service name `agent`.
# @TEST_EDGE: prefix_forwarding -> Proxy preserves `/api/agent/gradio` for Gradio root_path.
# @TEST_EDGE: auth_forwarding -> Browser Authorization header is forwarded.
# @TEST_INVARIANT: trace_forwarding -> Valid browser trace IDs cross nginx into backend and agent.
# @TEST_INVARIANT: proxy_access_events -> nginx stdout contains one JSON access event per request.
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
def _read(relative_path: str) -> str:
return (PROJECT_ROOT / relative_path).read_text(encoding="utf-8")
def test_nginx_http_gradio_proxy_targets_agent_service_and_preserves_prefix():
"""HTTP nginx routes Gradio traffic to compose service `agent` without stripping root_path."""
text = _read("docker/nginx.conf")
assert "location /api/agent/gradio/" in text
assert "set $agent_api http://agent:7860;" in text
assert "superset-tools-agent" not in text
assert "rewrite ^/api/agent/gradio" not in text
assert "proxy_set_header Authorization $http_authorization;" in text
assert "proxy_set_header X-Trace-ID $http_x_trace_id;" in text
assert "proxy_set_header Connection $connection_upgrade;" in text
assert "proxy_redirect http://agent:7860/ /api/agent/gradio/;" in text
def test_nginx_ssl_gradio_proxy_matches_http_contract():
"""SSL nginx keeps the same Gradio proxy invariants as HTTP nginx."""
text = _read("docker/nginx.ssl.conf")
assert "map $http_upgrade $connection_upgrade" in text
assert "location /api/agent/gradio/" in text
assert "set $agent_api http://agent:7860;" in text
assert "proxy_set_header X-Trace-ID $http_x_trace_id;" in text
assert "superset-tools-agent" not in text
assert "rewrite ^/api/agent/gradio" not in text
assert "proxy_set_header Authorization $http_authorization;" in text
assert "proxy_set_header X-Forwarded-Proto https;" in text
def test_nginx_proxy_emits_json_access_logs_and_forwards_trace_ids():
"""Both nginx modes must make request correlation available on container stdout."""
for relative_path in ("docker/nginx.conf", "docker/nginx.ssl.conf"):
text = _read(relative_path)
assert "log_format observability_json escape=json" in text
assert "access_log /dev/stdout observability_json;" in text
assert "error_log /dev/stderr warn;" in text
assert '"trace_id":"$http_x_trace_id"' in text
assert '"upstream_response_time":"$upstream_response_time"' in text
assert text.count("proxy_set_header X-Trace-ID $http_x_trace_id;") == 3
def test_compose_frontend_depends_on_agent_and_agent_uses_root_path():
"""Compose starts agent before frontend and configures Gradio root_path."""
for compose_path in ("docker-compose.yml", "docker-compose.enterprise-clean.yml"):
text = _read(compose_path)
assert " frontend:" in text
frontend_block = text.split(" frontend:", 1)[1].split("\n\n", 1)[0]
assert " - backend" in frontend_block
assert " - agent" in frontend_block
agent_block = text.split(" agent:", 1)[1]
assert "GRADIO_ROOT_PATH: /api/agent/gradio" in agent_block
def test_build_script_generated_enterprise_compose_keeps_agent_proxy_contract():
"""Release bundle compose heredocs preserve frontend->agent dependency and root_path."""
text = _read("build.sh")
assert text.count(" - agent") >= 1
assert text.count("GRADIO_ROOT_PATH: /api/agent/gradio") >= 1
def test_agent_launch_uses_default_gradio_root_path():
"""Agent runtime exports `/api/agent/gradio` as Gradio root_path for @gradio/client."""
config_text = _read("agent/src/ss_tools/agent/_config.py")
run_text = _read("agent/src/ss_tools/agent/run.py")
app_text = _read("agent/src/ss_tools/agent/app.py")
assert 'GRADIO_ROOT_PATH: str = os.getenv("GRADIO_ROOT_PATH", "/api/agent/gradio")' in config_text
assert "root_path=GRADIO_ROOT_PATH" in run_text
assert "root_path=GRADIO_ROOT_PATH" in app_text
# #endregion Test.Infrastructure.GradioProxy