import tempfile import time import unittest from unittest.mock import patch import jwt from mdrsclient.api.utils import token_check from mdrsclient.cache import CacheFile from mdrsclient.config import normalize_url from mdrsclient.connection import MDRSConnection from mdrsclient.exceptions import IllegalArgumentException from mdrsclient.models import Token REMOTE = "unittest" def make_token(access_offset: int, refresh_offset: int, label: str) -> Token: now = int(time.time()) def encode(token_type: str, offset: int) -> str: return jwt.encode( { "token_type": token_type, "exp": now + offset, "iat": now, "jti": f"{label}-{token_type}", "user_id": 1, }, "unittest-signing-key-not-verified-anywhere", ) return Token(access=encode("access", access_offset), refresh=encode("refresh", refresh_offset)) class TestTokenCache(unittest.TestCase): def setUp(self) -> None: self.tmp = tempfile.TemporaryDirectory() patcher = patch("mdrsclient.cache.CONFIG_DIRNAME", self.tmp.name) patcher.start() self.addCleanup(patcher.stop) self.addCleanup(self.tmp.cleanup) def test_reload_picks_up_a_write_from_another_holder(self) -> None: """The cache is shared, so a value read earlier can already be out of date.""" reader = CacheFile(REMOTE) writer = CacheFile(REMOTE) first = make_token(3600, 86400, "first") writer.token = first self.assertEqual(reader.token, first) second = make_token(3600, 86400, "second") writer.token = second reader.reload() self.assertEqual(reader.token, second) def test_token_check_uses_the_token_another_holder_just_wrote(self) -> None: """A rotating provider drops the token it replaces, so the refresh must not be sent again once someone else has already made the round trip.""" connection = MDRSConnection(REMOTE, "http://localhost:8000/api/") connection.token = make_token(-60, 86400, "stale") # Another process refreshes while this one is between requests. other = CacheFile(REMOTE) rotated = make_token(3600, 86400, "rotated") other.token = rotated with patch("mdrsclient.api.utils.UsersApi") as users_api: token_check(connection) users_api.assert_not_called() self.assertEqual(connection.token, rotated) def test_token_check_refreshes_when_nothing_else_has(self) -> None: connection = MDRSConnection(REMOTE, "http://localhost:8000/api/") connection.token = make_token(-60, 86400, "stale") rotated = make_token(3600, 86400, "rotated") with patch("mdrsclient.api.utils.UsersApi") as users_api: users_api.return_value.tokenRefresh.return_value = rotated token_check(connection) users_api.return_value.tokenRefresh.assert_called_once() self.assertEqual(connection.token, rotated) def test_token_check_logs_out_once_the_refresh_token_expires(self) -> None: connection = MDRSConnection(REMOTE, "http://localhost:8000/api/") connection.token = make_token(-3600, -60, "dead") with patch("mdrsclient.api.utils.UsersApi") as users_api: token_check(connection) users_api.assert_not_called() self.assertIsNone(connection.token) class TestUrlNormalization(unittest.TestCase): """Both clients share config.ini, so they have to agree on what a remote URL is.""" def test_trailing_slash_is_dropped(self) -> None: self.assertEqual(normalize_url("http://127.0.0.1:8000/api/"), "http://127.0.0.1:8000/api") self.assertEqual(normalize_url("https://neurodata.riken.jp/api/"), "https://neurodata.riken.jp/api") def test_bare_hostname_is_accepted(self) -> None: self.assertEqual(normalize_url("http://localhost:8000/api"), "http://localhost:8000/api") def test_only_http_schemes_are_accepted(self) -> None: for url in ("ftp://x.example.com/", "file:///etc/passwd", "not-a-url", "http://"): with self.subTest(url=url), self.assertRaises(IllegalArgumentException): normalize_url(url) if __name__ == "__main__": unittest.main()