Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/em_deposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion examples/xray_deposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/onedep_lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -15,6 +15,7 @@
"deposit_init",
"deposit_resume",
"list_sessions",
"check_auth_key",
"Deposition",
# check result types
"CheckReport",
Expand Down
13 changes: 8 additions & 5 deletions src/onedep_lib/auths/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/onedep_lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions src/onedep_lib/dsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ def list_sessions(base_dir: Path | None = None) -> list[tuple[LocalSession, list
return results


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()
return True
except Exception: # noqa: BLE001
return False


def deposit_init(
email: str,
users: list[str],
Expand Down Expand Up @@ -180,14 +192,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.

Expand Down
28 changes: 26 additions & 2 deletions tests/auths/test_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -139,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()
18 changes: 16 additions & 2 deletions tests/unit/test_deposition_facade.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down
Loading