Files
mnemosyne/mnemosyne/themis/tests/test_forms.py
Robert Helewka 81426327bf feat(mcp): store MCP tokens as SHA-256 hashes instead of plaintext
Replace plaintext token storage with SHA-256 hashes so leaked database
contents cannot be used to authenticate. Plaintext is generated, shown
once at creation time, and never persisted.

- Add `hash_token()` helper and `MCPTokenManager.create_token()` that
  returns `(instance, plaintext)`.
- Replace `token` field with indexed `token_hash`; look up bearers by
  hashing the incoming value.
- Update dashboard, management command, and admin to surface plaintext
  only at creation. Disable admin "add" since it cannot reveal plaintext.
- Migration drops the old `token` column and adds `token_hash`;
  pre-existing tokens are invalidated and must be reissued.
2026-04-27 09:01:36 -04:00

266 lines
9.0 KiB
Python

"""
Tests for Themis forms.
"""
from django.contrib.auth import get_user_model
from django.test import TestCase
from themis.encryption import encrypt_value
from themis.forms import APIKeyCreateForm, APIKeyEditForm, ProfileSettingsForm
from themis.models import UserAPIKey
User = get_user_model()
class ProfileSettingsFormTest(TestCase):
"""Tests for ProfileSettingsForm."""
def setUp(self):
self.user = User.objects.create_user(username="formuser", password="pass123")
self.profile = self.user.profile
def test_valid_data(self):
"""Form is valid with correct data."""
form = ProfileSettingsForm(
data={
"home_timezone": "America/Toronto",
"current_timezone": "",
"date_format": "DD/MM/YYYY",
"time_format": "12-hour",
"thousand_separator": "period",
"week_start": "sunday",
"theme_mode": "dark",
"theme_name": "dracula",
"dark_theme_name": "night",
"notifications_enabled": True,
"notifications_min_level": "info",
"browser_notifications_enabled": False,
"notification_retention_days": 30,
},
instance=self.profile,
)
self.assertTrue(form.is_valid())
def test_save_updates_profile(self):
"""Saving the form updates the profile instance."""
form = ProfileSettingsForm(
data={
"home_timezone": "Europe/London",
"current_timezone": "",
"date_format": "DD.MM.YYYY",
"time_format": "12-hour",
"thousand_separator": "space",
"week_start": "saturday",
"theme_mode": "light",
"theme_name": "cupcake",
"dark_theme_name": "halloween",
"notifications_enabled": True,
"notifications_min_level": "warning",
"browser_notifications_enabled": True,
"notification_retention_days": 60,
},
instance=self.profile,
)
self.assertTrue(form.is_valid())
form.save()
self.profile.refresh_from_db()
self.assertEqual(self.profile.home_timezone, "Europe/London")
self.assertEqual(self.profile.date_format, "DD.MM.YYYY")
self.assertEqual(self.profile.theme_name, "cupcake")
def test_invalid_timezone(self):
"""Form rejects invalid timezone values."""
form = ProfileSettingsForm(
data={
"home_timezone": "Invalid/Timezone",
"current_timezone": "",
"date_format": "YYYY-MM-DD",
"time_format": "24-hour",
"thousand_separator": "comma",
"week_start": "monday",
"theme_mode": "auto",
"theme_name": "corporate",
"dark_theme_name": "business",
},
instance=self.profile,
)
self.assertFalse(form.is_valid())
self.assertIn("home_timezone", form.errors)
def test_correct_fields(self):
"""Form exposes the expected fields."""
form = ProfileSettingsForm(instance=self.profile)
expected_fields = {
"home_timezone",
"current_timezone",
"date_format",
"time_format",
"thousand_separator",
"week_start",
"theme_mode",
"theme_name",
"dark_theme_name",
"notifications_enabled",
"notifications_min_level",
"browser_notifications_enabled",
"notification_retention_days",
}
self.assertEqual(set(form.fields.keys()), expected_fields)
def test_widgets_have_daisyui_classes(self):
"""All widgets should have DaisyUI classes."""
form = ProfileSettingsForm(instance=self.profile)
for field_name, field in form.fields.items():
css_class = field.widget.attrs.get("class", "")
has_daisyui = any(
kw in css_class
for kw in ("select", "toggle", "input")
)
self.assertTrue(
has_daisyui,
f"{field_name} missing DaisyUI class (got: '{css_class}')",
)
class APIKeyCreateFormTest(TestCase):
"""Tests for APIKeyCreateForm."""
def test_valid_data(self):
"""Form is valid with required fields."""
form = APIKeyCreateForm(
data={
"service_name": "OpenAI",
"key_type": "api",
"label": "Work",
"key_value": "sk-abc123",
"instructions": "Get from dashboard",
"help_url": "https://platform.openai.com",
}
)
self.assertTrue(form.is_valid())
def test_valid_minimal_data(self):
"""Form is valid with only required fields."""
form = APIKeyCreateForm(
data={
"service_name": "Test",
"key_type": "api",
"key_value": "my-key",
}
)
self.assertTrue(form.is_valid())
def test_missing_service_name(self):
"""Form requires service_name."""
form = APIKeyCreateForm(
data={
"key_type": "api",
"key_value": "my-key",
}
)
self.assertFalse(form.is_valid())
self.assertIn("service_name", form.errors)
def test_missing_key_value(self):
"""Form requires key_value."""
form = APIKeyCreateForm(
data={
"service_name": "Test",
"key_type": "api",
}
)
self.assertFalse(form.is_valid())
self.assertIn("key_value", form.errors)
def test_invalid_key_type(self):
"""Form rejects invalid key_type."""
form = APIKeyCreateForm(
data={
"service_name": "Test",
"key_type": "invalid_type",
"key_value": "my-key",
}
)
self.assertFalse(form.is_valid())
self.assertIn("key_type", form.errors)
def test_invalid_help_url(self):
"""Form rejects invalid URLs."""
form = APIKeyCreateForm(
data={
"service_name": "Test",
"key_type": "api",
"key_value": "my-key",
"help_url": "not-a-url",
}
)
self.assertFalse(form.is_valid())
self.assertIn("help_url", form.errors)
def test_correct_fields(self):
"""Form has the expected fields."""
form = APIKeyCreateForm()
expected = {"service_name", "key_type", "label", "key_value", "instructions", "help_url"}
self.assertEqual(set(form.fields.keys()), expected)
class APIKeyEditFormTest(TestCase):
"""Tests for APIKeyEditForm."""
def setUp(self):
self.user = User.objects.create_user(username="edituser", password="pass123")
self.key = UserAPIKey.objects.create(
user=self.user,
service_name="OpenAI",
key_type="api",
encrypted_value=encrypt_value("sk-test"),
)
def test_valid_data(self):
"""Form is valid with correct data."""
form = APIKeyEditForm(
data={
"service_name": "Anthropic",
"key_type": "token",
"label": "New Label",
"instructions": "Updated instructions",
"help_url": "https://docs.anthropic.com",
"is_active": True,
},
instance=self.key,
)
self.assertTrue(form.is_valid())
def test_no_key_value_field(self):
"""Edit form does not include key_value or encrypted_value."""
form = APIKeyEditForm(instance=self.key)
self.assertNotIn("key_value", form.fields)
self.assertNotIn("encrypted_value", form.fields)
def test_correct_fields(self):
"""Form exposes metadata fields only."""
form = APIKeyEditForm(instance=self.key)
expected = {"service_name", "key_type", "label", "instructions", "help_url", "is_active"}
self.assertEqual(set(form.fields.keys()), expected)
def test_save_updates_metadata(self):
"""Saving updates metadata without touching encrypted_value."""
original_encrypted = self.key.encrypted_value
form = APIKeyEditForm(
data={
"service_name": "Updated Service",
"key_type": "token",
"label": "Updated",
"instructions": "",
"help_url": "",
"is_active": False,
},
instance=self.key,
)
self.assertTrue(form.is_valid())
form.save()
self.key.refresh_from_db()
self.assertEqual(self.key.service_name, "Updated Service")
self.assertEqual(self.key.key_type, "token")
self.assertFalse(self.key.is_active)
self.assertEqual(self.key.encrypted_value, original_encrypted)