Pull cache/keys.py (HMAC cache key derivation) and mwt.py (Mizan Web Token) out of backends/mizan-django and into a new cores/mizan-python package. mizan-django re-imports them via the new mizan_core module. Naming: directory cores/mizan-python/, distribution mizan-core, importable module mizan_core. mizan-django keeps its existing 'mizan' distribution slot on PyPI; the two coexist as distinct packages. Wiring: - backends/mizan-django/pyproject.toml gains a 'mizan-core' dep with a [tool.uv.sources] path entry (editable install from ../../cores/mizan-python). - Makefile install target prepends 'cd cores/mizan-python && uv pip install -e .' - 3 import sites in mizan-django updated: cache/__init__.py, jwt/functions.py, client/executor.py — all now import from mizan_core. Test split: - 3 unit-test classes (CacheKeyDerivationTests, MWTCreationTests, PermissionKeyTests) move to cores/mizan-python/tests/, rewritten against unittest.TestCase (no Django dep). The cross-language pin test (pinned HMAC hex digests against mizan-ts) moves with CacheKeyDerivationTests. - Integration tests stay in mizan-django (CacheBackendTests, CachePurgeTests, CacheIntegrationTests, RevParameterTests, MWTAuthIntegrationTests) — they need the Django request flow. Verified: - mizan-core: 15/15 pass (incl. cross-language pin) - mizan-django: 348 pass, 21 skip, 0 fail - mizan-ts: edge-compat 34/34 pass — protocol invariant holds, the moved Python derive_cache_key still produces the exact hex digests TS pins against. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
"""Unit tests for MWT creation, decoding, and permission key derivation."""
|
|
|
|
from unittest import TestCase
|
|
from unittest.mock import MagicMock
|
|
|
|
from mizan_core.mwt import (
|
|
MWTUser,
|
|
compute_permission_key,
|
|
create_mwt,
|
|
decode_mwt,
|
|
)
|
|
|
|
|
|
def _make_user(**kwargs):
|
|
user = MagicMock()
|
|
user.pk = kwargs.get("pk", 1)
|
|
user.is_staff = kwargs.get("is_staff", False)
|
|
user.is_superuser = kwargs.get("is_superuser", False)
|
|
user.get_all_permissions = MagicMock(return_value=kwargs.get("perms", set()))
|
|
return user
|
|
|
|
|
|
class MWTCreationTests(TestCase):
|
|
"""Tests for MWT creation and decoding."""
|
|
|
|
SECRET = "test-mwt-secret-that-is-32bytes!"
|
|
|
|
def test_create_and_decode(self):
|
|
"""Create an MWT and decode it successfully."""
|
|
user = _make_user(pk=42, is_staff=True)
|
|
token = create_mwt(user, self.SECRET, ttl=300)
|
|
payload = decode_mwt(token, self.SECRET)
|
|
|
|
self.assertIsNotNone(payload)
|
|
self.assertEqual(payload.sub, "42")
|
|
self.assertTrue(payload.staff)
|
|
self.assertFalse(payload.super)
|
|
self.assertEqual(payload.kid, "v1")
|
|
self.assertEqual(len(payload.pkey), 64)
|
|
|
|
def test_decode_expired(self):
|
|
"""Expired MWT returns None."""
|
|
user = _make_user()
|
|
token = create_mwt(user, self.SECRET, ttl=-1)
|
|
payload = decode_mwt(token, self.SECRET)
|
|
self.assertIsNone(payload)
|
|
|
|
def test_decode_wrong_secret(self):
|
|
"""MWT signed with wrong secret returns None."""
|
|
user = _make_user()
|
|
token = create_mwt(user, self.SECRET)
|
|
payload = decode_mwt(token, "wrong-secret")
|
|
self.assertIsNone(payload)
|
|
|
|
def test_decode_wrong_audience(self):
|
|
"""MWT with wrong audience returns None."""
|
|
user = _make_user()
|
|
token = create_mwt(user, self.SECRET, audience="app1")
|
|
payload = decode_mwt(token, self.SECRET, audience="app2")
|
|
self.assertIsNone(payload)
|
|
|
|
def test_mwt_user_has_pkey(self):
|
|
"""MWTUser carries the permission key."""
|
|
user = _make_user(pk=5, perms={"app.view_thing"})
|
|
token = create_mwt(user, self.SECRET)
|
|
payload = decode_mwt(token, self.SECRET)
|
|
mwt_user = MWTUser(payload)
|
|
|
|
self.assertEqual(mwt_user.pk, 5)
|
|
self.assertTrue(mwt_user.is_authenticated)
|
|
self.assertEqual(len(mwt_user.pkey), 64)
|
|
|
|
|
|
class PermissionKeyTests(TestCase):
|
|
"""Tests for pkey determinism and sensitivity."""
|
|
|
|
def test_deterministic(self):
|
|
"""Same permissions produce same pkey."""
|
|
user = _make_user(perms={"app.view_thing", "app.add_thing"})
|
|
pkey1 = compute_permission_key(user)
|
|
pkey2 = compute_permission_key(user)
|
|
self.assertEqual(pkey1, pkey2)
|
|
|
|
def test_changes_on_permission_change(self):
|
|
"""Different permissions produce different pkey."""
|
|
user1 = _make_user(perms={"app.view_thing"})
|
|
user2 = _make_user(perms={"app.view_thing", "app.add_thing"})
|
|
self.assertNotEqual(compute_permission_key(user1), compute_permission_key(user2))
|
|
|
|
def test_changes_on_staff_change(self):
|
|
"""Staff status change produces different pkey."""
|
|
user_normal = _make_user(is_staff=False)
|
|
user_staff = _make_user(is_staff=True)
|
|
self.assertNotEqual(
|
|
compute_permission_key(user_normal),
|
|
compute_permission_key(user_staff),
|
|
)
|