From 3074a3e047982d2ac2d25a60162a2cc7b1e1ca1c Mon Sep 17 00:00:00 2001 From: Weslley Morellato Bueno Date: Wed, 24 Jun 2026 15:02:12 +0100 Subject: [PATCH 1/3] Required refresh token only for first use --- src/onedep_lib/auths/token.py | 13 ++++++++----- src/onedep_lib/config.py | 4 +++- tests/auths/test_token.py | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/onedep_lib/auths/token.py b/src/onedep_lib/auths/token.py index 66d6a0d..cd9a0df 100644 --- a/src/onedep_lib/auths/token.py +++ b/src/onedep_lib/auths/token.py @@ -30,8 +30,8 @@ def store_tokens(self, access_token: str, refresh_token: str) -> None: def get_access_token(self) -> str: entry = self._read_entry() - token = entry["access_token"] - if self._is_expired(token): + token = entry.get("access_token") + if token is None or self._is_expired(token): return self.refresh() return token @@ -93,9 +93,12 @@ def clear_tokens(self) -> None: def _read_entry(self) -> dict[str, str]: access_token = self._config.access_token refresh_token = self._config.refresh_token - if access_token is None or refresh_token is None: - raise AuthError("No access token stored. Paste a token pair first.") - return {"access_token": access_token, "refresh_token": refresh_token} + if refresh_token is None: + raise AuthError("No refresh token stored. Paste a refresh token first.") + entry = {"refresh_token": refresh_token} + if access_token is not None: + entry["access_token"] = access_token + return entry def _fqdn_key(self) -> str: key = _hostname_to_fqdn_key(self._config.hostname) diff --git a/src/onedep_lib/config.py b/src/onedep_lib/config.py index 9f3bb93..10a954d 100644 --- a/src/onedep_lib/config.py +++ b/src/onedep_lib/config.py @@ -113,7 +113,9 @@ def load(cls, **overrides: object) -> DepositConfig: acc = entry.get("access_token") ref = entry.get("refresh_token") if acc is not None or ref is not None: - if not isinstance(acc, str) or not isinstance(ref, str): + if acc is not None and not isinstance(acc, str): + raise ConfigError(f"Malformed token data in [auths.{fqdn_key}]") + if not isinstance(ref, str): raise ConfigError(f"Malformed token data in [auths.{fqdn_key}]") merged["access_token"] = acc merged["refresh_token"] = ref diff --git a/tests/auths/test_token.py b/tests/auths/test_token.py index 1e1a3fa..081c630 100644 --- a/tests/auths/test_token.py +++ b/tests/auths/test_token.py @@ -104,6 +104,30 @@ def test_get_access_token_refreshes_expired_token(tmp_path: Path, httpserver): assert store._read_entry()["refresh_token"] == "new-refresh" +def test_get_access_token_refreshes_when_only_refresh_token_is_loaded(tmp_path: Path, httpserver): + config_file = tmp_path / "config.toml" + hostname = httpserver.url_for("/deposition").rstrip("/") + config_file.write_text( + "[default]\n" + f"hostname = \"{hostname}\"\n" + "ssl_verify = false\n" + "\n" + "[auths.localhost]\n" + "refresh_token = \"bootstrap-refresh\"\n" + ) + fresh = _make_jwt(3600) + config = DepositConfig.load(config_path=config_file) + store = TokenStore(config=config) + httpserver.expect_request( + "/deposition/auth/tokens/refresh", + method="POST", + json={"refresh_token": "bootstrap-refresh"}, + ).respond_with_json({"access_token": fresh, "refresh_token": "rotated-refresh"}) + + assert store.get_access_token() == fresh + assert store._read_entry() == {"access_token": fresh, "refresh_token": "rotated-refresh"} + + def test_refresh_401_explains_manual_token_required(tmp_path: Path, httpserver): config_file = tmp_path / "config.toml" config_file.write_text("[default]\n") From 90968043eb8c35f0b30eeba3d966434a911e77f0 Mon Sep 17 00:00:00 2001 From: Weslley Morellato Bueno Date: Wed, 24 Jun 2026 16:13:16 +0100 Subject: [PATCH 2/3] Exposing check_auth_key() outside of Deposition class --- src/onedep_lib/dsp.py | 18 ++++++++++-------- tests/auths/test_token.py | 4 ++-- tests/unit/test_deposition_facade.py | 18 ++++++++++++++++-- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/onedep_lib/dsp.py b/src/onedep_lib/dsp.py index e55d9a8..b52ec71 100644 --- a/src/onedep_lib/dsp.py +++ b/src/onedep_lib/dsp.py @@ -53,6 +53,16 @@ def list_sessions(base_dir: Path | None = None) -> list[tuple[LocalSession, list return results +def check_auth_key(config) -> bool: + """Return True if the configured credentials are valid, False otherwise.""" + try: + api_client = HttpApiClient(config, auth_provider=TokenStore(config)) + api_client.get_all_depositions() + return True + except Exception: # noqa: BLE001 + return False + + def deposit_init( email: str, users: list[str], @@ -180,14 +190,6 @@ def set_em_params( self._session.em_subtype = em_subtype self._session.coordinates = coordinates - def check_auth_key(self) -> bool: - """Return True if the configured credentials are valid, False otherwise.""" - try: - self._api_client.get_all_depositions() - return True - except Exception: # noqa: BLE001 - return False - def add_file(self, file_path: str, file_type: FileType) -> str: """Register a local file for this deposition. diff --git a/tests/auths/test_token.py b/tests/auths/test_token.py index 081c630..39cd4c4 100644 --- a/tests/auths/test_token.py +++ b/tests/auths/test_token.py @@ -163,12 +163,12 @@ def test_revoke_posts_refresh_token_and_clears_local_storage(tmp_path: Path, htt json={"refresh_token": "refresh"}, ).respond_with_data(status=204) store.revoke() - with pytest.raises(AuthError, match="No access token"): + with pytest.raises(AuthError, match="No refresh token stored. Paste a refresh token first."): store.get_access_token() def test_get_access_token_raises_auth_error_when_no_tokens_loaded(config: DepositConfig): store = TokenStore(config=config) # config was constructed directly (not via load()), so access_token is None - with pytest.raises(AuthError, match="No access token"): + with pytest.raises(AuthError, match="No refresh token stored. Paste a refresh token first."): store.get_access_token() diff --git a/tests/unit/test_deposition_facade.py b/tests/unit/test_deposition_facade.py index 6b3b12a..0f0d1bc 100644 --- a/tests/unit/test_deposition_facade.py +++ b/tests/unit/test_deposition_facade.py @@ -1,6 +1,8 @@ import pytest from pathlib import Path from onedep_lib.checks.report import CheckReport +from onedep_lib import dsp +from onedep_lib.config import DepositConfig from onedep_lib.dsp import deposit_init, deposit_resume from onedep_lib.enums import Country, ExperimentType, FileType from tests.unit.apis.deposit.test_stub_api_client import StubApiClient @@ -99,9 +101,21 @@ def test_add_nonexistent_file_raises(dep): dep.add_file("/nonexistent/path.cif", FileType.MMCIF_COORD) -def test_check_auth_key_returns_bool(dep): - result = dep.check_auth_key() +def test_check_auth_key_returns_bool(monkeypatch): + class StubAuthClient: + def __init__(self, config, auth_provider): + self.config = config + self.auth_provider = auth_provider + + def get_all_depositions(self): + return [] + + monkeypatch.setattr(dsp, "HttpApiClient", StubAuthClient) + + result = dsp.check_auth_key(DepositConfig(access_token="access", refresh_token="refresh")) + assert isinstance(result, bool) + assert result is True def test_deposit_resume_restores_session(dep, tmp_path, stub_api): From e3551bf988e37ab03df30ba30d56be6fed74bc80 Mon Sep 17 00:00:00 2001 From: Weslley Morellato Bueno Date: Fri, 26 Jun 2026 15:22:14 +0100 Subject: [PATCH 3/3] Exposing check_auth_key --- examples/em_deposition.py | 2 +- examples/xray_deposition.py | 2 +- src/onedep_lib/__init__.py | 3 ++- src/onedep_lib/dsp.py | 4 +++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/em_deposition.py b/examples/em_deposition.py index 1a0f07a..b17a0fd 100644 --- a/examples/em_deposition.py +++ b/examples/em_deposition.py @@ -90,7 +90,7 @@ def main() -> None: # ── 3. Check auth key ───────────────────────────────────────────────── spin.update("[cyan]Checking auth key…[/cyan]") - auth_ok = dep.check_auth_key() + auth_ok = dsp.check_auth_key() if auth_ok: ok("Auth key valid") else: diff --git a/examples/xray_deposition.py b/examples/xray_deposition.py index 3c5fbfa..4ecd261 100644 --- a/examples/xray_deposition.py +++ b/examples/xray_deposition.py @@ -83,7 +83,7 @@ def main() -> None: # ── 3. Check auth key ───────────────────────────────────────────────── spin.update("[cyan]Checking auth key…[/cyan]") - auth_ok = dep.check_auth_key() + auth_ok = dsp.check_auth_key() if auth_ok: ok("Auth key valid") else: diff --git a/src/onedep_lib/__init__.py b/src/onedep_lib/__init__.py index 06aa4ff..16e012c 100644 --- a/src/onedep_lib/__init__.py +++ b/src/onedep_lib/__init__.py @@ -6,7 +6,7 @@ from onedep_lib.auths.token import TokenStore from onedep_lib.auths.types import AuthProvider from onedep_lib.checks.report import CheckIssue, CheckReport, CheckSeverity, CifLocation -from onedep_lib.dsp import Deposition, deposit_init, deposit_resume, list_sessions +from onedep_lib.dsp import Deposition, deposit_init, deposit_resume, list_sessions, check_auth_key from onedep_lib.enums import Country, EMSubType, ExperimentType, FileType from onedep_lib.exceptions import ApiError, DepositApiException, OneDepError @@ -15,6 +15,7 @@ "deposit_init", "deposit_resume", "list_sessions", + "check_auth_key", "Deposition", # check result types "CheckReport", diff --git a/src/onedep_lib/dsp.py b/src/onedep_lib/dsp.py index b52ec71..399dce6 100644 --- a/src/onedep_lib/dsp.py +++ b/src/onedep_lib/dsp.py @@ -53,8 +53,10 @@ def list_sessions(base_dir: Path | None = None) -> list[tuple[LocalSession, list return results -def check_auth_key(config) -> bool: +def check_auth_key(config: DepositConfig = None) -> bool: """Return True if the configured credentials are valid, False otherwise.""" + config = config or DepositConfig.load() + try: api_client = HttpApiClient(config, auth_provider=TokenStore(config)) api_client.get_all_depositions()