- Rename MCPToken to UserToken across models, views, and tests - Update URL names from mcp-token-* to token-* - Add Daedalus/Pallas integration design doc (v2) - Switch docker-compose to build local mnemosyne:local image via shared build config instead of pulling from git.helu.ca
178 lines
7.1 KiB
Python
178 lines
7.1 KiB
Python
"""View tests for the per-user API token self-service dashboard."""
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
|
|
from mcp_server.models import UserToken
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class TokenListViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(
|
|
username="alice", email="alice@example.com", password="pw"
|
|
)
|
|
self.url = reverse("mcp_server:token-list")
|
|
|
|
def test_login_required(self):
|
|
resp = self.client.get(self.url)
|
|
self.assertEqual(resp.status_code, 302)
|
|
self.assertIn("/login/", resp.url)
|
|
|
|
def test_list_shows_only_own_tokens(self):
|
|
other = User.objects.create_user(username="bob", password="pw")
|
|
UserToken.objects.create_token(user=self.user, name="mine")
|
|
UserToken.objects.create_token(user=other, name="theirs")
|
|
self.client.force_login(self.user)
|
|
resp = self.client.get(self.url)
|
|
self.assertContains(resp, "mine")
|
|
self.assertNotContains(resp, "theirs")
|
|
|
|
def test_empty_state(self):
|
|
self.client.force_login(self.user)
|
|
resp = self.client.get(self.url)
|
|
self.assertContains(resp, "No API tokens yet.")
|
|
|
|
|
|
class TokenCreateViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(username="alice", password="pw")
|
|
self.client.force_login(self.user)
|
|
self.url = reverse("mcp_server:token-create")
|
|
|
|
def test_get_renders_form(self):
|
|
resp = self.client.get(self.url)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, "Generate API Token")
|
|
|
|
def test_post_creates_token_and_shows_plaintext_once(self):
|
|
resp = self.client.post(self.url, {"name": "Claude Desktop"})
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, "Save this token now")
|
|
# Pull the created row, verify the response contained a plaintext that
|
|
# is NOT what we stored.
|
|
token = UserToken.objects.get(user=self.user, name="Claude Desktop")
|
|
self.assertNotContains(resp, token.token_hash) # hash is not what we display
|
|
# And the detail page never renders the plaintext.
|
|
body = resp.content.decode()
|
|
# Find the plaintext from the response: the only long alphanumeric
|
|
# block inside the #mcp-plaintext div.
|
|
import re
|
|
|
|
m = re.search(r'id="mcp-plaintext">([A-Za-z0-9_\-]+)<', body)
|
|
self.assertIsNotNone(m, "plaintext block not found in response")
|
|
plaintext = m.group(1)
|
|
# Sanity: round-tripping the plaintext through hash_token reproduces
|
|
# what's stored.
|
|
from mcp_server.models import hash_token
|
|
|
|
self.assertEqual(hash_token(plaintext), token.token_hash)
|
|
# Detail page must NOT contain the plaintext.
|
|
detail_resp = self.client.get(
|
|
reverse("mcp_server:token-detail", args=[token.pk])
|
|
)
|
|
self.assertNotContains(detail_resp, plaintext)
|
|
|
|
def test_post_invalid_renders_form_again(self):
|
|
resp = self.client.post(self.url, {"name": ""})
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, "Generate API Token")
|
|
self.assertEqual(UserToken.objects.count(), 0)
|
|
|
|
|
|
class TokenDetailViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(username="alice", password="pw")
|
|
self.client.force_login(self.user)
|
|
self.token, _ = UserToken.objects.create_token(user=self.user, name="t")
|
|
|
|
def test_renders_token(self):
|
|
resp = self.client.get(
|
|
reverse("mcp_server:token-detail", args=[self.token.pk])
|
|
)
|
|
self.assertContains(resp, self.token.name)
|
|
self.assertContains(resp, self.token.get_masked_token())
|
|
|
|
def test_cannot_view_other_users_token(self):
|
|
other = User.objects.create_user(username="bob", password="pw")
|
|
other_token, _ = UserToken.objects.create_token(user=other, name="theirs")
|
|
resp = self.client.get(
|
|
reverse("mcp_server:token-detail", args=[other_token.pk])
|
|
)
|
|
self.assertEqual(resp.status_code, 404)
|
|
|
|
|
|
class TokenEditViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(username="alice", password="pw")
|
|
self.client.force_login(self.user)
|
|
self.token, _ = UserToken.objects.create_token(user=self.user, name="t")
|
|
|
|
def test_post_updates_metadata(self):
|
|
resp = self.client.post(
|
|
reverse("mcp_server:token-edit", args=[self.token.pk]),
|
|
{
|
|
"name": "Renamed",
|
|
"is_active": "on",
|
|
"expires_at": "",
|
|
"allowed_tools": ["search"],
|
|
},
|
|
)
|
|
self.assertEqual(resp.status_code, 302)
|
|
self.token.refresh_from_db()
|
|
self.assertEqual(self.token.name, "Renamed")
|
|
self.assertEqual(self.token.allowed_tools, ["search"])
|
|
|
|
def test_cannot_edit_other_users_token(self):
|
|
other = User.objects.create_user(username="bob", password="pw")
|
|
other_token, _ = UserToken.objects.create_token(user=other, name="theirs")
|
|
resp = self.client.post(
|
|
reverse("mcp_server:token-edit", args=[other_token.pk]),
|
|
{"name": "hacked"},
|
|
)
|
|
self.assertEqual(resp.status_code, 404)
|
|
|
|
|
|
class TokenRevokeViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(username="alice", password="pw")
|
|
self.client.force_login(self.user)
|
|
self.token, _ = UserToken.objects.create_token(user=self.user, name="t")
|
|
|
|
def test_revoke_sets_inactive_keeps_row(self):
|
|
url = reverse("mcp_server:token-revoke", args=[self.token.pk])
|
|
resp = self.client.post(url)
|
|
self.assertEqual(resp.status_code, 302)
|
|
self.token.refresh_from_db()
|
|
self.assertFalse(self.token.is_active)
|
|
# Row still exists for audit trail.
|
|
self.assertTrue(UserToken.objects.filter(pk=self.token.pk).exists())
|
|
|
|
def test_get_not_allowed(self):
|
|
url = reverse("mcp_server:token-revoke", args=[self.token.pk])
|
|
resp = self.client.get(url)
|
|
self.assertEqual(resp.status_code, 405)
|
|
|
|
|
|
class TokenDeleteViewTest(TestCase):
|
|
def setUp(self):
|
|
self.user = User.objects.create_user(username="alice", password="pw")
|
|
self.client.force_login(self.user)
|
|
self.token, _ = UserToken.objects.create_token(user=self.user, name="t")
|
|
|
|
def test_delete_removes_row(self):
|
|
url = reverse("mcp_server:token-delete", args=[self.token.pk])
|
|
resp = self.client.post(url)
|
|
self.assertEqual(resp.status_code, 302)
|
|
self.assertFalse(UserToken.objects.filter(pk=self.token.pk).exists())
|
|
|
|
def test_cannot_delete_other_users_token(self):
|
|
other = User.objects.create_user(username="bob", password="pw")
|
|
other_token, _ = UserToken.objects.create_token(user=other, name="theirs")
|
|
url = reverse("mcp_server:token-delete", args=[other_token.pk])
|
|
resp = self.client.post(url)
|
|
self.assertEqual(resp.status_code, 404)
|
|
self.assertTrue(UserToken.objects.filter(pk=other_token.pk).exists())
|