|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import json |
5 | 6 | from typing import Any |
6 | 7 |
|
7 | 8 | import pytest |
8 | 9 |
|
9 | 10 | from renku_data_services.mcp_api.dependencies import MCPDependencies |
10 | | -from renku_data_services.mcp_api.main import _authorization_server_doc, _protected_resource_doc |
| 11 | +from renku_data_services.mcp_api.main import ( |
| 12 | + _authorization_server_doc, |
| 13 | + _load_rnk_token, |
| 14 | + _protected_resource_doc, |
| 15 | + _resolve_token, |
| 16 | + _rnk_token_paths, |
| 17 | +) |
11 | 18 | from renku_data_services.mcp_api.server import ( |
12 | 19 | _admin_cache, |
13 | 20 | _is_stale_session, |
@@ -131,6 +138,86 @@ async def test_authorization_server_doc_no_keycloak_url(): |
131 | 138 | assert status == 503 |
132 | 139 |
|
133 | 140 |
|
| 141 | +# ------------------------------------------------------------------ # |
| 142 | +# Token resolution — _load_rnk_token and _resolve_token # |
| 143 | +# ------------------------------------------------------------------ # |
| 144 | + |
| 145 | + |
| 146 | +def test_load_rnk_token_finds_token_in_response_key(tmp_path, monkeypatch): |
| 147 | + """_load_rnk_token reads the token from the 'response' wrapper used by rnk.""" |
| 148 | + token_file = tmp_path / "token.json" |
| 149 | + token_file.write_text(json.dumps({"response": {"access_token": "my-token"}})) |
| 150 | + |
| 151 | + monkeypatch.setattr("renku_data_services.mcp_api.main._rnk_token_paths", lambda: [token_file]) |
| 152 | + assert _load_rnk_token() == "my-token" |
| 153 | + |
| 154 | + |
| 155 | +def test_load_rnk_token_finds_token_at_root(tmp_path, monkeypatch): |
| 156 | + """_load_rnk_token also reads the token when there's no 'response' wrapper.""" |
| 157 | + token_file = tmp_path / "token.json" |
| 158 | + token_file.write_text(json.dumps({"access_token": "my-token"})) |
| 159 | + |
| 160 | + monkeypatch.setattr("renku_data_services.mcp_api.main._rnk_token_paths", lambda: [token_file]) |
| 161 | + assert _load_rnk_token() == "my-token" |
| 162 | + |
| 163 | + |
| 164 | +def test_load_rnk_token_forwards_any_token_value(tmp_path, monkeypatch): |
| 165 | + """_load_rnk_token forwards tokens as-is without JWT validation — the API validates.""" |
| 166 | + token_file = tmp_path / "token.json" |
| 167 | + token_file.write_text(json.dumps({"access_token": "opaque-or-expired-or-wrong-issuer"})) |
| 168 | + |
| 169 | + monkeypatch.setattr("renku_data_services.mcp_api.main._rnk_token_paths", lambda: [token_file]) |
| 170 | + assert _load_rnk_token() == "opaque-or-expired-or-wrong-issuer" |
| 171 | + |
| 172 | + |
| 173 | +def test_load_rnk_token_no_file(monkeypatch): |
| 174 | + """_load_rnk_token returns None when no token file exists.""" |
| 175 | + monkeypatch.setattr("renku_data_services.mcp_api.main._rnk_token_paths", lambda: []) |
| 176 | + assert _load_rnk_token() is None |
| 177 | + |
| 178 | + |
| 179 | +def test_resolve_token_prefers_env_var(tmp_path, monkeypatch): |
| 180 | + """_resolve_token returns the env var even when an rnk token file exists.""" |
| 181 | + monkeypatch.setenv("RENKU_ACCESS_TOKEN", "env-token") |
| 182 | + monkeypatch.setattr("renku_data_services.mcp_api.main._rnk_token_paths", lambda: []) |
| 183 | + assert _resolve_token() == "env-token" |
| 184 | + |
| 185 | + |
| 186 | +def test_resolve_token_falls_back_to_rnk(tmp_path, monkeypatch): |
| 187 | + """_resolve_token falls back to the rnk file when no env var is set.""" |
| 188 | + base_url = "https://renkulab.io" |
| 189 | + monkeypatch.setenv("RENKU_BASE_URL", base_url) |
| 190 | + monkeypatch.delenv("RENKU_ACCESS_TOKEN", raising=False) |
| 191 | + monkeypatch.delenv("RENKU_TOKEN", raising=False) |
| 192 | + monkeypatch.delenv("RENKU_CLI_ACCESS_TOKEN", raising=False) |
| 193 | + |
| 194 | + token_file = tmp_path / "token.json" |
| 195 | + token_file.write_text(json.dumps({"access_token": "rnk-token"})) |
| 196 | + |
| 197 | + monkeypatch.setattr("renku_data_services.mcp_api.main._rnk_token_paths", lambda: [token_file]) |
| 198 | + assert _resolve_token() == "rnk-token" |
| 199 | + |
| 200 | + |
| 201 | +def test_resolve_token_raises_when_nothing_found(monkeypatch): |
| 202 | + """_resolve_token raises RuntimeError with a helpful message when no token is available.""" |
| 203 | + monkeypatch.delenv("RENKU_ACCESS_TOKEN", raising=False) |
| 204 | + monkeypatch.delenv("RENKU_TOKEN", raising=False) |
| 205 | + monkeypatch.delenv("RENKU_CLI_ACCESS_TOKEN", raising=False) |
| 206 | + monkeypatch.setattr("renku_data_services.mcp_api.main._rnk_token_paths", lambda: []) |
| 207 | + |
| 208 | + with pytest.raises(RuntimeError, match="rnk login"): |
| 209 | + _resolve_token() |
| 210 | + |
| 211 | + |
| 212 | +def test_rnk_token_paths_uses_xdg(monkeypatch): |
| 213 | + """_rnk_token_paths respects XDG_DATA_HOME.""" |
| 214 | + monkeypatch.setenv("XDG_DATA_HOME", "/custom/xdg") |
| 215 | + monkeypatch.delenv("APPDATA", raising=False) |
| 216 | + |
| 217 | + paths = _rnk_token_paths() |
| 218 | + assert any("/custom/xdg" in str(p) for p in paths) |
| 219 | + |
| 220 | + |
134 | 221 | # ------------------------------------------------------------------ # |
135 | 222 | # MCPDependencies.api — test via pytest-httpx # |
136 | 223 | # ------------------------------------------------------------------ # |
|
0 commit comments