File size: 10,235 Bytes
0fdbc7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fdfbfb3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99e8ddc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
"""The `gr.api` machine endpoints are gated by caller-token verification (ADR-0004/0018).

A machine call carries no OAuth session, so identity comes from a caller token resolved via
`whoami`. These endpoints are the orchestrator's front door β€” if they are open, the allow-list
on the UI is decorative.
"""

from __future__ import annotations

import json

import pytest

import gradio_ui


class _Request:
    """Minimal stand-in for `gr.Request` β€” only `.headers.get` is used."""

    def __init__(self, headers=None):
        self.headers = headers or {}


@pytest.fixture(autouse=True)
def _enforce(monkeypatch, tmp_path):
    monkeypatch.setenv("ACCESS_CONTROL", "enforce")
    monkeypatch.setenv("ALLOWED_IDS", "orchestrator-bot")
    monkeypatch.setenv("LOG_SINK", "local")
    monkeypatch.setenv("LOG_SINK_LOCAL_DIR", str(tmp_path / "run_logs"))
    gradio_ui._WHOAMI_CACHE.clear()
    yield
    gradio_ui._WHOAMI_CACHE.clear()


def _explode(*a, **k):  # pragma: no cover
    raise AssertionError("tool was called despite a denied access decision")


MACHINE_CASES = [
    ("query_variant_status", {"genes": "KRAS", "source": "cbioportal:paad_tcga"}),
    ("variant_by_subtype", {"genes": "KRAS", "source": "cbioportal:paad_tcga"}),
    ("panel", {}),
]


@pytest.mark.parametrize("endpoint,kwargs", MACHINE_CASES)
def test_machine_endpoint_denies_tokenless_call(monkeypatch, endpoint, kwargs):
    """No token header β‡’ denied, fail-closed, before any tool call."""
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)
    monkeypatch.setattr(gradio_ui, "_variant_by_subtype", _explode)

    body = json.loads(getattr(gradio_ui, endpoint)(**kwargs, request=_Request()))

    assert body["status"] == "denied"


@pytest.mark.parametrize("endpoint,kwargs", MACHINE_CASES)
def test_machine_endpoint_denies_unresolvable_token(monkeypatch, endpoint, kwargs):
    """A token `whoami` cannot resolve is an anonymous caller, not a trusted one."""
    monkeypatch.setattr(gradio_ui, "_resolve_token_identity", lambda token: None)
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)
    monkeypatch.setattr(gradio_ui, "_variant_by_subtype", _explode)

    request = _Request({"x-orchestrator-token": "Bearer nonsense"})
    body = json.loads(getattr(gradio_ui, endpoint)(**kwargs, request=request))

    assert body["status"] == "denied"


def test_machine_endpoint_denies_resolvable_but_unlisted_identity(monkeypatch):
    """A real HF account that is not allow-listed is still refused."""
    monkeypatch.setattr(gradio_ui, "_resolve_token_identity", lambda token: "random-person")
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)

    request = _Request({"x-orchestrator-token": "tok"})
    body = json.loads(gradio_ui.query_variant_status(genes="KRAS", source="x", request=request))

    assert body["status"] == "denied"
    assert "random-person" in body["reason"]


def test_machine_endpoint_allows_listed_identity(monkeypatch):
    monkeypatch.setattr(gradio_ui, "_resolve_token_identity", lambda token: "orchestrator-bot")
    monkeypatch.setattr(
        gradio_ui,
        "_query_variant_status",
        lambda genes, source: {"n_samples": 7, "genes": {}, "source": source},
    )

    request = _Request({"x-orchestrator-token": "Bearer good-token"})
    body = json.loads(
        gradio_ui.query_variant_status(
            genes="KRAS,TP53", source="cbioportal:paad_tcga", request=request
        )
    )

    assert body["n_samples"] == 7


def test_bearer_prefix_is_stripped(monkeypatch):
    seen = {}
    monkeypatch.setattr(
        gradio_ui, "_resolve_token_identity", lambda token: seen.setdefault("token", token)
    )
    gradio_ui._machine_caller_identity(_Request({"x-orchestrator-token": "Bearer abc123"}))
    assert seen["token"] == "abc123"


def test_empty_genes_falls_back_to_full_panel():
    """The machine contract sends a comma-separated string; empty means 'the whole panel'."""
    assert gradio_ui._split_genes("") == list(gradio_ui.PANEL)
    assert gradio_ui._split_genes("kras, tp53") == ["KRAS", "TP53"]


# --------------------------------------------------------------------------- #
# Denial DIAGNOSIS β€” the three failure modes must be distinguishable
# --------------------------------------------------------------------------- #
# Regression guard for a real misdiagnosis (2026-07-27β†’28). The orchestrator was denied and
# the response said "Please sign in with your HuggingFace account", so the investigation went
# server-side β€” probing headers, suspecting `whoami`. Nothing was wrong here: the caller had
# simply sent no header at all (its own HF_TOKEN was unset). These three states are
# operationally different and must never again be one indistinguishable message.


def test_no_token_header_is_diagnosed_as_caller_misconfiguration(monkeypatch):
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)
    body = json.loads(gradio_ui.query_variant_status(source="x", request=_Request()))

    assert body["status"] == "denied"
    assert body["machine_auth"] == "no_token_header"
    # names the calling Space's secret, and does NOT tell a machine to sign in
    assert "HF_TOKEN" in body["reason"]
    assert "sign in" not in body["reason"].lower()


def test_unresolvable_token_is_diagnosed_separately_from_a_missing_one(monkeypatch):
    monkeypatch.setattr(gradio_ui, "_resolve_token_identity", lambda token: None)
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)

    body = json.loads(
        gradio_ui.query_variant_status(
            source="x", request=_Request({"x-orchestrator-token": "bad-token"})
        )
    )

    assert body["machine_auth"] == "token_unresolved"


def test_resolved_but_unlisted_identity_is_the_only_real_authorization_denial(monkeypatch):
    """Identity resolved fine β€” this one IS an allow-list decision, and still names who."""
    monkeypatch.setattr(gradio_ui, "_resolve_token_identity", lambda token: "stranger-bot")
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)

    body = json.loads(
        gradio_ui.query_variant_status(
            source="x", request=_Request({"x-orchestrator-token": "good-token"})
        )
    )

    assert body["machine_auth"] == "not_allowlisted"
    assert "stranger-bot" in body["reason"]


def test_identity_resolution_reports_resolved_on_success(monkeypatch):
    monkeypatch.setattr(gradio_ui, "_resolve_token_identity", lambda token: "orchestrator-bot")
    identity, diagnosis = gradio_ui._machine_caller_identity(
        _Request({"x-orchestrator-token": "good-token"})
    )
    assert (identity, diagnosis) == ("orchestrator-bot", "resolved")


# --------------------------------------------------------------------------- #
# Argument DIAGNOSIS β€” the same lesson, applied to caller-side type mistakes
# --------------------------------------------------------------------------- #
# Found while verifying the ADR-0007 deploy: passing the obvious-looking
# `genes=["KRAS","TP53"]` returned
#   {"status": "error", "reason": "AttributeError: 'list' object has no attribute 'split'"}
# β€” indistinguishable from a server fault, so it sends the reader into this repo when the fix
# is in their own call. Exactly the failure mode the denial-diagnosis block above exists for.
def _allowed(monkeypatch):
    monkeypatch.setattr(gradio_ui, "_resolve_token_identity", lambda token: "orchestrator-bot")
    return _Request({"x-orchestrator-token": "tok"})


def test_list_genes_reports_a_caller_side_argument_error(monkeypatch):
    """A list of genes is named as such, with the exact string to send instead."""
    request = _allowed(monkeypatch)
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)  # never runs

    body = json.loads(
        gradio_ui.query_variant_status(
            genes=["KRAS", "TP53"], source="cbioportal:paad_tcga", request=request
        )
    )

    assert body["machine_input"] == "genes_not_a_string"
    assert 'genes="KRAS,TP53"' in body["reason"]  # the fix, spelled out
    assert "caller-side" in body["reason"]
    assert "AttributeError" not in body["reason"]


def test_non_string_source_does_not_raise_before_the_gate(monkeypatch):
    """`registered_source` runs pre-gate; a non-string `source` must not blow up there.

    This was an uncaught exception (a 500), not a result β€” the one input that escaped
    `_safe_result` entirely because the audit descriptor is built before the auth check.
    """
    request = _allowed(monkeypatch)
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)

    body = json.loads(
        gradio_ui.query_variant_status(genes="KRAS", source=["cbioportal:paad_tcga"], request=request)
    )

    assert body["machine_input"] == "source_not_a_string"


def test_argument_errors_are_reported_only_after_the_auth_gate(monkeypatch):
    """A tokenless caller gets `denied`, not a critique of their arguments."""
    monkeypatch.setattr(gradio_ui, "_query_variant_status", _explode)

    body = json.loads(
        gradio_ui.query_variant_status(genes=["KRAS"], source=1234, request=_Request())
    )

    assert body["status"] == "denied"
    assert "machine_input" not in body


@pytest.mark.parametrize("field", ["subtype_attribute", "modality"])
def test_every_string_argument_is_covered(monkeypatch, field):
    request = _allowed(monkeypatch)
    monkeypatch.setattr(gradio_ui, "_variant_by_subtype", _explode)

    body = json.loads(
        gradio_ui.variant_by_subtype(
            genes="KRAS", source="cbioportal:paad_tcga", request=request, **{field: ["x"]}
        )
    )

    assert body["machine_input"] == f"{field}_not_a_string"


def test_valid_strings_are_unaffected(monkeypatch):
    """The happy path must not acquire a new way to fail."""
    request = _allowed(monkeypatch)
    monkeypatch.setattr(
        gradio_ui, "_query_variant_status", lambda genes, source: {"n_samples": 7, "genes": {}}
    )

    body = json.loads(
        gradio_ui.query_variant_status(
            genes="KRAS,TP53", source="cbioportal:paad_tcga", request=request
        )
    )

    assert body["n_samples"] == 7
    assert "machine_input" not in body