gabriel / musehub public
test_musehub_revocation_cache.py python
113 lines 3.9 KB
7923a405 test(supercharge): comprehensive test suite overhaul — all 11 points Gabriel Cardona <gabriel@tellurstori.com> 6d ago
1 """Unit tests for the in-memory token revocation cache.
2
3 musehub/auth/revocation_cache.py is a tiny but security-critical module that
4 had zero test coverage. These tests verify:
5
6 - Basic get/set/clear lifecycle
7 - TTL expiry (entries become None after TTL elapses)
8 - Cache hit prevents redundant DB lookups (behaviour contract)
9 - Overwrite semantics
10 - hash_token determinism
11 """
12 from __future__ import annotations
13
14 import time
15 from unittest.mock import patch
16
17 import pytest
18
19 from musehub.auth.revocation_cache import (
20 _cache,
21 clear_revocation_cache,
22 get_revocation_status,
23 set_revocation_status,
24 )
25 from musehub.auth.tokens import hash_token
26
27
28 @pytest.fixture(autouse=True)
29 def _clean_cache():
30 """Ensure a pristine cache before and after every test."""
31 clear_revocation_cache()
32 yield
33 clear_revocation_cache()
34
35
36 class TestGetRevocationStatus:
37 def test_returns_none_on_miss(self) -> None:
38 assert get_revocation_status("nonexistent") is None
39
40 def test_returns_false_for_valid_cached_token(self) -> None:
41 set_revocation_status("tok-a", revoked=False)
42 assert get_revocation_status("tok-a") is False
43
44 def test_returns_true_for_revoked_cached_token(self) -> None:
45 set_revocation_status("tok-b", revoked=True)
46 assert get_revocation_status("tok-b") is True
47
48 def test_expired_entry_returns_none_and_is_evicted(self) -> None:
49 # Insert an entry that already expired (expires_at = now - 1s)
50 _cache["tok-expired"] = (False, time.monotonic() - 1.0)
51 assert get_revocation_status("tok-expired") is None
52 assert "tok-expired" not in _cache, "Expired entry should be evicted on read"
53
54 def test_not_yet_expired_entry_returned(self) -> None:
55 # Entry expires far in the future
56 _cache["tok-future"] = (True, time.monotonic() + 9999.0)
57 assert get_revocation_status("tok-future") is True
58
59
60 class TestSetRevocationStatus:
61 def test_set_and_get_roundtrip(self) -> None:
62 set_revocation_status("tok-c", revoked=True)
63 assert get_revocation_status("tok-c") is True
64
65 def test_overwrite_false_to_true(self) -> None:
66 set_revocation_status("tok-d", revoked=False)
67 set_revocation_status("tok-d", revoked=True)
68 assert get_revocation_status("tok-d") is True
69
70 def test_overwrite_true_to_false(self) -> None:
71 set_revocation_status("tok-e", revoked=True)
72 set_revocation_status("tok-e", revoked=False)
73 assert get_revocation_status("tok-e") is False
74
75 def test_ttl_respected(self) -> None:
76 """Entry created with a very small TTL expires quickly."""
77 with patch("musehub.auth.revocation_cache.settings") as mock_settings:
78 mock_settings.token_revocation_cache_ttl_seconds = 0.01
79 set_revocation_status("tok-ttl", revoked=False)
80 time.sleep(0.05)
81 assert get_revocation_status("tok-ttl") is None
82
83
84 class TestClearRevocationCache:
85 def test_clear_removes_all(self) -> None:
86 set_revocation_status("tok-f", revoked=False)
87 set_revocation_status("tok-g", revoked=True)
88 clear_revocation_cache()
89 assert get_revocation_status("tok-f") is None
90 assert get_revocation_status("tok-g") is None
91
92 def test_clear_idempotent_on_empty_cache(self) -> None:
93 clear_revocation_cache()
94 clear_revocation_cache() # should not raise
95
96
97 class TestHashToken:
98 def test_deterministic(self) -> None:
99 tok = "some.jwt.value"
100 assert hash_token(tok) == hash_token(tok)
101
102 def test_produces_64_char_hex(self) -> None:
103 result = hash_token("anything")
104 assert len(result) == 64
105 assert all(c in "0123456789abcdef" for c in result)
106
107 def test_distinct_inputs_produce_distinct_hashes(self) -> None:
108 assert hash_token("token-1") != hash_token("token-2")
109
110 def test_empty_string(self) -> None:
111 # Should not raise; SHA-256 of empty bytes is well-defined
112 result = hash_token("")
113 assert len(result) == 64