"""Tests for the Team / LibraryMembership / TeamWorkspaceAssignment models. ``UserToken``'s hash-at-rest semantics live in ``test_token.py``; this module exercises the new Phase 2 tables introduced by ``docs/DAEDALUS_PALLAS_INTEGRATION_v1.md`` §4 plus the ``allowed_libraries`` JSONField attached to the existing :class:`~mcp_server.models.UserToken`. """ from __future__ import annotations import uuid from django.contrib.auth import get_user_model from django.db import IntegrityError, transaction from django.test import TestCase from mcp_server.models import ( LibraryMembership, MCPSigningKey, UserToken, Team, TeamWorkspaceAssignment, ) User = get_user_model() # --------------------------------------------------------------------------- # LibraryMembership # --------------------------------------------------------------------------- class LibraryMembershipTest(TestCase): def setUp(self): self.user = User.objects.create_user(username="u", password="pw") def test_role_choices_exposed(self): self.assertEqual(LibraryMembership.Role.OWNER, "owner") self.assertEqual(LibraryMembership.Role.MANAGER, "manager") self.assertEqual(LibraryMembership.Role.READER, "reader") def test_unique_per_user_and_library(self): LibraryMembership.objects.create( user=self.user, library_uid="lib-1", role=LibraryMembership.Role.OWNER, ) # Second membership on the same (user, library_uid) must fail # even if the role differs — callers consolidate to the # higher role rather than stacking rows. with transaction.atomic(): with self.assertRaises(IntegrityError): LibraryMembership.objects.create( user=self.user, library_uid="lib-1", role=LibraryMembership.Role.READER, ) def test_same_library_different_users_allowed(self): other = User.objects.create_user(username="u2", password="pw") LibraryMembership.objects.create( user=self.user, library_uid="lib-1", role=LibraryMembership.Role.OWNER, ) LibraryMembership.objects.create( user=other, library_uid="lib-1", role=LibraryMembership.Role.READER, ) self.assertEqual(LibraryMembership.objects.count(), 2) def test_same_user_different_libraries_allowed(self): LibraryMembership.objects.create( user=self.user, library_uid="lib-1", role=LibraryMembership.Role.OWNER, ) LibraryMembership.objects.create( user=self.user, library_uid="lib-2", role=LibraryMembership.Role.MANAGER, ) self.assertEqual( set( LibraryMembership.objects.filter(user=self.user) .values_list("library_uid", flat=True) ), {"lib-1", "lib-2"}, ) # --------------------------------------------------------------------------- # UserToken.allowed_libraries # --------------------------------------------------------------------------- class UserTokenAllowedLibrariesTest(TestCase): def setUp(self): self.user = User.objects.create_user(username="u", password="pw") def test_defaults_to_empty_list(self): token, _ = UserToken.objects.create_token(user=self.user, name="t") self.assertEqual(token.allowed_libraries, []) def test_create_token_accepts_allowed_libraries(self): token, _ = UserToken.objects.create_token( user=self.user, name="t", allowed_libraries=["lib-a", "lib-b"] ) self.assertEqual(token.allowed_libraries, ["lib-a", "lib-b"]) def test_allowed_libraries_round_trips(self): token, _ = UserToken.objects.create_token( user=self.user, name="t", allowed_libraries=["lib-a", "lib-b", "lib-c"], ) token.refresh_from_db() self.assertEqual( token.allowed_libraries, ["lib-a", "lib-b", "lib-c"] ) # --------------------------------------------------------------------------- # Team + rotate_jti / deactivate # --------------------------------------------------------------------------- class TeamTest(TestCase): @classmethod def setUpTestData(cls): cls.owner = get_user_model().objects.create_user( username="alice", password="pw" ) def test_create_with_explicit_uuid(self): tid = uuid.uuid4() team = Team.objects.create(id=tid, name="Harper", owner=self.owner) self.assertEqual(team.id, tid) self.assertTrue(team.active) self.assertIsNone(team.active_jti) def test_rotate_jti_installs_fresh_uuid(self): team = Team.objects.create(id=uuid.uuid4(), name="t", owner=self.owner) first = team.rotate_jti() self.assertIsInstance(first, uuid.UUID) self.assertEqual(team.active_jti, first) second = team.rotate_jti() self.assertNotEqual(first, second) self.assertEqual(team.active_jti, second) def test_rotate_jti_persists(self): team = Team.objects.create(id=uuid.uuid4(), name="t", owner=self.owner) team.rotate_jti() # Reload from DB and make sure the UUID was committed. reloaded = Team.objects.get(pk=team.id) self.assertEqual(reloaded.active_jti, team.active_jti) def test_deactivate_clears_active_jti(self): team = Team.objects.create(id=uuid.uuid4(), name="t", owner=self.owner) team.rotate_jti() self.assertTrue(team.active) team.deactivate() self.assertFalse(team.active) self.assertIsNone(team.active_jti) # And it persisted. reloaded = Team.objects.get(pk=team.id) self.assertFalse(reloaded.active) self.assertIsNone(reloaded.active_jti) # --------------------------------------------------------------------------- # TeamWorkspaceAssignment # --------------------------------------------------------------------------- class TeamWorkspaceAssignmentTest(TestCase): @classmethod def setUpTestData(cls): cls.owner = get_user_model().objects.create_user( username="alice", password="pw" ) def setUp(self): self.team = Team.objects.create( id=uuid.uuid4(), name="t", owner=self.owner ) def test_unique_team_workspace_pair(self): TeamWorkspaceAssignment.objects.create( team=self.team, workspace_id="ws-a" ) with transaction.atomic(): with self.assertRaises(IntegrityError): TeamWorkspaceAssignment.objects.create( team=self.team, workspace_id="ws-a" ) def test_same_workspace_different_teams_allowed(self): other = Team.objects.create( id=uuid.uuid4(), name="t2", owner=self.owner ) TeamWorkspaceAssignment.objects.create( team=self.team, workspace_id="ws-a" ) TeamWorkspaceAssignment.objects.create( team=other, workspace_id="ws-a" ) self.assertEqual(TeamWorkspaceAssignment.objects.count(), 2) def test_cascade_on_team_delete(self): TeamWorkspaceAssignment.objects.create( team=self.team, workspace_id="ws-a" ) self.team.delete() self.assertEqual(TeamWorkspaceAssignment.objects.count(), 0) def test_related_name_workspace_assignments(self): # auth._libraries_for_team reaches through ``team.workspace_assignments``; # make sure that attribute actually works. TeamWorkspaceAssignment.objects.create( team=self.team, workspace_id="ws-a" ) TeamWorkspaceAssignment.objects.create( team=self.team, workspace_id="ws-b" ) ws_ids = sorted( self.team.workspace_assignments.values_list( "workspace_id", flat=True ) ) self.assertEqual(ws_ids, ["ws-a", "ws-b"]) # --------------------------------------------------------------------------- # MCPSigningKey.objects.current() — used by mint_team_jwt # --------------------------------------------------------------------------- class MCPSigningKeyCurrentTest(TestCase): def test_none_when_no_keys(self): self.assertIsNone(MCPSigningKey.objects.current()) def test_returns_active_not_retired(self): retired = MCPSigningKey.objects.create( kid="old", secret_hex="a" * 64, is_active=False ) active = MCPSigningKey.objects.create( kid="new", secret_hex="b" * 64, is_active=True ) self.assertEqual(MCPSigningKey.objects.current().pk, active.pk) self.assertNotEqual(MCPSigningKey.objects.current().pk, retired.pk) def test_returns_newest_when_multiple_active(self): older = MCPSigningKey.objects.create( kid="older", secret_hex="a" * 64, is_active=True ) newer = MCPSigningKey.objects.create( kid="newer", secret_hex="b" * 64, is_active=True ) self.assertEqual(MCPSigningKey.objects.current().pk, newer.pk) # Sanity: older is still active, just older. self.assertTrue( MCPSigningKey.objects.filter(pk=older.pk, is_active=True).exists() )