v0.7.1: in-app password change, custom labels, always-on folders

- Settings -> Account: change your own password (current + new), session
  preserved via update_session_auth_hash.
- Custom labels: per-user palette in Settings -> Labels; apply/remove from a
  message's ... menu (menu stays open, checkmark = applied). Labels are unified
  with tags; chip colours sync to the global TagColor. New Label model +
  migration 0017; POST /a/{id}/label (session, owner-scoped, palette-validated).
- Sidebar Keep/Deleted folders are now always shown; count badge only when
  non-empty. Updates the stale 'hide when empty' note in CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-08 15:19:54 +01:00
parent fcd7da853f
commit c2a354809a
14 changed files with 562 additions and 23 deletions

View file

@ -0,0 +1,54 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
User = get_user_model()
class ChangePasswordViewTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="alice", password="oldpassword")
self.client.force_login(self.user)
def _post(self, old, new1, new2):
return self.client.post(
reverse("settings-account"),
{"old_password": old, "new_password1": new1, "new_password2": new2},
)
def test_get_renders_form(self):
response = self.client.get(reverse("settings-account"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Change password")
self.assertContains(response, "old_password")
def test_requires_login(self):
self.client.logout()
response = self.client.get(reverse("settings-account"))
self.assertEqual(response.status_code, 302)
def test_valid_change_updates_password(self):
response = self._post("oldpassword", "brandnewpass", "brandnewpass")
self.assertRedirects(response, reverse("settings-account"))
self.user.refresh_from_db()
self.assertTrue(self.user.check_password("brandnewpass"))
self.assertFalse(self.user.check_password("oldpassword"))
def test_valid_change_keeps_session(self):
self._post("oldpassword", "brandnewpass", "brandnewpass")
# Session must survive the password change (update_session_auth_hash),
# so a follow-up authenticated request still succeeds.
response = self.client.get(reverse("settings-account"))
self.assertEqual(response.status_code, 200)
def test_wrong_current_password_rejected(self):
response = self._post("WRONG", "brandnewpass", "brandnewpass")
self.assertEqual(response.status_code, 200) # re-render with errors, no redirect
self.user.refresh_from_db()
self.assertTrue(self.user.check_password("oldpassword"))
def test_mismatched_new_password_rejected(self):
response = self._post("oldpassword", "brandnewpass", "different")
self.assertEqual(response.status_code, 200)
self.user.refresh_from_db()
self.assertTrue(self.user.check_password("oldpassword"))