Equalize Access login failure handling
This commit is contained in:
87
tests/test_login_security.py
Normal file
87
tests/test_login_security.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from govoplan_access.backend.api.v1 import auth
|
||||
from govoplan_access.backend.security.passwords import (
|
||||
DUMMY_PASSWORD_HASH,
|
||||
verify_password,
|
||||
)
|
||||
from govoplan_core.api.v1.schemas import LoginRequest
|
||||
|
||||
|
||||
class LoginSecurityTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def _session_with_account(account: object | None) -> MagicMock:
|
||||
session = MagicMock()
|
||||
session.query.return_value.filter.return_value.one_or_none.return_value = (
|
||||
account
|
||||
)
|
||||
return session
|
||||
|
||||
def test_absent_identity_verifies_a_valid_dummy_hash_before_generic_failure(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertTrue(verify_password("not-a-user-password", DUMMY_PASSWORD_HASH))
|
||||
payload = LoginRequest(
|
||||
email="missing@example.test", password="attempted-password"
|
||||
)
|
||||
|
||||
with patch.object(auth, "verify_password", return_value=True) as verifier:
|
||||
with self.assertRaises(HTTPException) as raised:
|
||||
auth._resolve_login_user(self._session_with_account(None), payload)
|
||||
|
||||
verifier.assert_called_once_with(payload.password, DUMMY_PASSWORD_HASH)
|
||||
self.assertEqual(raised.exception.status_code, 401)
|
||||
self.assertEqual(raised.exception.detail, "Invalid login")
|
||||
|
||||
def test_present_identity_verifies_account_hash_before_same_generic_failure(
|
||||
self,
|
||||
) -> None:
|
||||
account_hash = "pbkdf2_sha256$260000$account-salt$account-digest"
|
||||
account = SimpleNamespace(password_hash=account_hash)
|
||||
payload = LoginRequest(email="known@example.test", password="wrong-password")
|
||||
|
||||
with patch.object(auth, "verify_password", return_value=False) as verifier:
|
||||
with self.assertRaises(HTTPException) as raised:
|
||||
auth._resolve_login_user(self._session_with_account(account), payload) # type: ignore[arg-type]
|
||||
|
||||
verifier.assert_called_once_with(payload.password, account_hash)
|
||||
self.assertEqual(raised.exception.status_code, 401)
|
||||
self.assertEqual(raised.exception.detail, "Invalid login")
|
||||
|
||||
def test_passwordless_account_cannot_authenticate_with_dummy_password(self) -> None:
|
||||
account = SimpleNamespace(password_hash=None)
|
||||
payload = LoginRequest(
|
||||
email="passwordless@example.test", password="not-a-user-password"
|
||||
)
|
||||
|
||||
with self.assertRaises(HTTPException) as raised:
|
||||
auth._resolve_login_user(self._session_with_account(account), payload) # type: ignore[arg-type]
|
||||
|
||||
self.assertEqual(raised.exception.status_code, 401)
|
||||
self.assertEqual(raised.exception.detail, "Invalid login")
|
||||
|
||||
def test_account_without_active_membership_uses_same_generic_failure(self) -> None:
|
||||
account = SimpleNamespace(
|
||||
id="account-1",
|
||||
password_hash="pbkdf2_sha256$260000$account-salt$account-digest",
|
||||
)
|
||||
session = self._session_with_account(account)
|
||||
session.query.return_value.join.return_value.filter.return_value.order_by.return_value.first.return_value = None
|
||||
payload = LoginRequest(email="known@example.test", password="correct-password")
|
||||
|
||||
with patch.object(auth, "verify_password", return_value=True):
|
||||
with self.assertRaises(HTTPException) as raised:
|
||||
auth._resolve_login_user(session, payload) # type: ignore[arg-type]
|
||||
|
||||
self.assertEqual(raised.exception.status_code, 401)
|
||||
self.assertEqual(raised.exception.detail, "Invalid login")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user