Spaces:
Sleeping
Sleeping
| """Every UI handler and machine endpoint must gate FIRST and fail closed (ADR-0004). | |
| This is the test the sibling repo keeps (`test_ui_handler_gating.py`) and the reason it keeps | |
| it: `api_name=False` hides nothing (ADR-0018), so the per-handler `check_access` call is the | |
| only real boundary. If someone "simplifies" a gate away, this fails. | |
| No network: the gate must deny *before* any tool call, so a denied path never reaches | |
| cBioPortal. The allowed-path tests monkeypatch the tool functions. | |
| """ | |
| from __future__ import annotations | |
| import inspect | |
| import json | |
| import pytest | |
| import gradio_ui | |
| class _Profile: | |
| def __init__(self, username): | |
| self.username = username | |
| def _enforce(monkeypatch): | |
| """Turn the gate ON with a one-name allow-list for every test in this module.""" | |
| monkeypatch.setenv("ACCESS_CONTROL", "enforce") | |
| monkeypatch.setenv("ALLOWED_IDS", "allowed-user") | |
| monkeypatch.delenv("ADMIN_IDS", raising=False) | |
| monkeypatch.delenv("UPLOAD_ADMIN_IDS", raising=False) | |
| def _no_sink(monkeypatch, tmp_path): | |
| """Audit writes go to a temp dir, never the repo's run_logs.""" | |
| monkeypatch.setenv("LOG_SINK", "local") | |
| monkeypatch.setenv("LOG_SINK_LOCAL_DIR", str(tmp_path / "run_logs")) | |
| def _explode(*a, **k): # pragma: no cover - must never be reached on a denied path | |
| raise AssertionError("tool was called despite a denied access decision") | |
| def _no_chart(update): | |
| """True when a plot slot carries no chart — either `None` or a hidden-visibility update. | |
| Handlers no longer agree on how many plot slots they have: variant-status and BYOD render one | |
| chart PER MODALITY (never a stack, since the modalities have different denominators), while | |
| the subtype tab still has one. The gate assertion is "a denied caller sees no chart at all", | |
| which is about the slots' contents, not their count. | |
| """ | |
| if update is None: | |
| return True | |
| return isinstance(update, dict) and update.get("visible") is False | |
| def _final(outputs): | |
| """The handler's last frame, whether it returns a tuple or yields several. | |
| `_ui_variant_status` is a GENERATOR: it blanks the chart first, then yields the answer, so a | |
| narrowing query can never leave the previous cohort's bars under the new cohort's caution | |
| (prod, 2026-08-05). The gate assertions below are about the ANSWER, so they read the final | |
| frame — and a denied path still yields exactly one, which keeps "denied" indistinguishable | |
| here from a plain return. | |
| """ | |
| if inspect.isgenerator(outputs): | |
| frames = list(outputs) | |
| assert frames, "handler produced no output at all" | |
| return frames[-1] | |
| return outputs | |
| def _assert_denial_visible(caution, reason): | |
| """A denial must land in the caution box the user can actually see, not only in the | |
| collapsed JSON accordion — the invisible-denial bug (2026-08-14) looked like a dead button.""" | |
| assert isinstance(caution, dict) | |
| assert caution.get("visible") is True | |
| assert reason in caution["value"] | |
| UI_CASES = [ | |
| ("_ui_variant_status", ("cbioportal:paad_tcga", ["KRAS"])), | |
| ("_ui_variant_by_subtype", ("cbioportal:paad_tcga", ["KRAS"], "mutation", "")), | |
| ("_ui_byod", (None, None, "GRCh38", "human", ["KRAS"])), | |
| ] | |
| def test_ui_handler_denies_anonymous(monkeypatch, handler_name, args): | |
| """No OAuth profile ⇒ denied, and no tool call happens at all.""" | |
| monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode) | |
| monkeypatch.setattr(gradio_ui, "_variant_by_subtype", _explode) | |
| monkeypatch.setattr(gradio_ui, "build_status_matrix", _explode) | |
| outputs = _final(getattr(gradio_ui, handler_name)(*args, profile=None)) | |
| assert all(_no_chart(o) for o in outputs[1:-1]) | |
| body = json.loads(outputs[-1]) | |
| assert body["status"] == "denied" | |
| assert "sign in" in body["reason"].lower() | |
| _assert_denial_visible(outputs[0], body["reason"]) | |
| def test_ui_handler_denies_unlisted_user(monkeypatch, handler_name, args): | |
| """A signed-in identity that is not on the allow-list is refused by name.""" | |
| monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode) | |
| monkeypatch.setattr(gradio_ui, "_variant_by_subtype", _explode) | |
| monkeypatch.setattr(gradio_ui, "build_status_matrix", _explode) | |
| outputs = _final(getattr(gradio_ui, handler_name)( | |
| *args, profile=_Profile("someone-else") | |
| )) | |
| assert all(_no_chart(o) for o in outputs[1:-1]) | |
| body = json.loads(outputs[-1]) | |
| assert body["status"] == "denied" | |
| assert "someone-else" in body["reason"] | |
| _assert_denial_visible(outputs[0], body["reason"]) | |
| def test_ui_handler_allows_listed_user(monkeypatch): | |
| """An allow-listed identity reaches the tool and gets its result back.""" | |
| monkeypatch.setattr( | |
| gradio_ui, | |
| "_query_variant_status", | |
| lambda genes, source: {"source": source, "n_samples": 3, "genes": {}}, | |
| ) | |
| payload = _final(getattr(gradio_ui, "_ui_variant_status")( | |
| "cbioportal:paad_tcga", ["KRAS"], profile=_Profile("allowed-user") | |
| ))[-1] | |
| assert json.loads(payload)["n_samples"] == 3 | |
| def test_gate_off_by_default(monkeypatch): | |
| """Dark-launch: with ACCESS_CONTROL unset, an anonymous user is allowed through.""" | |
| monkeypatch.delenv("ACCESS_CONTROL", raising=False) | |
| monkeypatch.setattr( | |
| gradio_ui, "_query_variant_status", lambda genes, source: {"n_samples": 1, "genes": {}} | |
| ) | |
| payload = _final(gradio_ui._ui_variant_status( | |
| "cbioportal:paad_tcga", ["KRAS"], profile=None | |
| ))[-1] | |
| assert json.loads(payload)["n_samples"] == 1 | |
| def test_ui_error_is_data_not_exception(monkeypatch): | |
| """A tool blowing up becomes an error payload, never a crashed handler.""" | |
| def _boom(*a, **k): | |
| raise RuntimeError("cBioPortal is down") | |
| monkeypatch.setattr(gradio_ui, "_query_variant_status", _boom) | |
| payload = _final(gradio_ui._ui_variant_status( | |
| "cbioportal:paad_tcga", ["KRAS"], profile=_Profile("allowed-user") | |
| ))[-1] | |
| body = json.loads(payload) | |
| assert body["status"] == "error" | |
| assert "cBioPortal is down" in body["reason"] | |
| def test_byod_refusal_surfaces_reason(monkeypatch, tmp_path): | |
| """A BYOD hard-gate refusal reaches the user as an explanation, not a stack trace.""" | |
| from src.workflows.byod import BYODRefusal | |
| def _refuse(*a, **k): | |
| raise BYODRefusal("non_human_species", "Species 'mouse' is not human.") | |
| monkeypatch.setattr(gradio_ui, "build_status_matrix", _refuse) | |
| upload = tmp_path / "variants.maf" | |
| upload.write_text("Hugo_Symbol\tTumor_Sample_Barcode\nKras\tS1\n") | |
| outputs = gradio_ui._ui_byod( | |
| str(upload), None, "GRCh38", "mouse", ["KRAS"], profile=_Profile("allowed-user") | |
| ) | |
| caution, payload = outputs[0], outputs[-1] | |
| body = json.loads(payload) | |
| assert body["status"] == "refused" | |
| assert body["reason"] == "non_human_species" | |
| assert "refused" in caution["value"].lower() | |