Files
Clarium/backend/tests/test_passwords.py
T
mroberts 7f5d034a1f feat: auth by default, theme toggle, CoderPad startup, review fixes
- Require a session on every route; public routes opt out with @allow_anonymous
- Split password hashing and pepper loading into passwords.py
- Add a system/light/dark theme toggle backed by light-dark() colors
- Ignore stale 401s from an earlier session, PATCH only changed book fields,
  and block overlapping journal-entry saves
- Add bin/start and CoderPad Vite server settings for the pad's start/restart
- Rewrite README as a mise onboarding guide; expand .gitignore
- Include review-round fixes and tests
2026-10-02 16:49:14 -05:00

70 lines
2.6 KiB
Python

import os
import stat
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from passwords import (
ITERATIONS,
hash_password,
load_pepper,
needs_rehash,
verify_password,
)
PEPPER = b"p" * 32
KNOWN_ANSWER = "pbkdf2_sha256$1000$AAAAAAAAAAAAAAAAAAAAAA==$9ZLLEusnEPU3Km8h+vnd4ue9fw7rHdm4QwuBX5ozynE="
class PasswordHashTests(unittest.TestCase):
def test_known_answer(self):
stored = hash_password(
"correct horse battery staple", PEPPER, salt=bytes(16), iterations=1000
)
self.assertEqual(stored, KNOWN_ANSWER)
def test_round_trip_uses_current_iterations_and_random_salt(self):
first = hash_password("correct horse battery staple", PEPPER)
second = hash_password("correct horse battery staple", PEPPER)
self.assertTrue(first.startswith(f"pbkdf2_sha256${ITERATIONS}$"))
self.assertNotEqual(first, second)
self.assertTrue(verify_password("correct horse battery staple", first, PEPPER))
def test_wrong_password_fails(self):
self.assertFalse(verify_password("wrong password!!", KNOWN_ANSWER, PEPPER))
def test_wrong_pepper_fails(self):
self.assertFalse(
verify_password("correct horse battery staple", KNOWN_ANSWER, b"q" * 32)
)
def test_needs_rehash_below_current_iterations(self):
self.assertTrue(needs_rehash(KNOWN_ANSWER))
self.assertFalse(needs_rehash(hash_password("x" * 12, PEPPER)))
class PepperTests(unittest.TestCase):
def test_env_pepper_must_be_long_enough(self):
with (
mock.patch.dict(os.environ, {"PASSWORD_PEPPER": "short"}),
self.assertRaisesRegex(RuntimeError, "at least 32 characters"),
):
load_pepper(Path("/nonexistent"))
def test_env_pepper_is_used_when_set(self):
with mock.patch.dict(os.environ, {"PASSWORD_PEPPER": "e" * 40}):
self.assertEqual(load_pepper(Path("/nonexistent")), b"e" * 40)
def test_file_fallback_creates_private_stable_pepper(self):
with tempfile.TemporaryDirectory() as tmp, mock.patch.dict(os.environ):
os.environ.pop("PASSWORD_PEPPER", None)
pepper_file = Path(tmp) / ".pepper"
with self.assertLogs("passwords", level="WARNING"):
first = load_pepper(pepper_file)
with self.assertLogs("passwords", level="WARNING"):
second = load_pepper(pepper_file)
self.assertEqual(first, second)
self.assertEqual(len(first), 64)
self.assertEqual(stat.S_IMODE(pepper_file.stat().st_mode), 0o600)