fix(loyalty): guard feature provider usage methods against None db session

Fixes deployment test failures where get_store_usage() and get_merchant_usage()
were called with db=None but attempted to run queries.

Also adds noqa suppressions for pre-existing security validator findings
in dev-toolbar (innerHTML with trusted content) and test fixtures
(hardcoded test passwords).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 22:31:34 +01:00
parent 29d942322d
commit 93b7279c3a
20 changed files with 1923 additions and 13 deletions

View File

@@ -0,0 +1,91 @@
# app/modules/tenancy/tests/unit/test_user_account_schema.py
"""
Unit tests for user account schemas.
Tests validation rules for UserPasswordChange and UserAccountUpdate.
"""
import pytest
from pydantic import ValidationError
from app.modules.tenancy.schemas.user_account import (
UserAccountUpdate,
UserPasswordChange,
)
@pytest.mark.unit
@pytest.mark.schema
class TestUserPasswordChangeSchema:
"""Tests for UserPasswordChange validation."""
def test_valid_password(self):
schema = UserPasswordChange(
current_password="oldpass123", # noqa: SEC001
new_password="newpass456", # noqa: SEC001
confirm_password="newpass456", # noqa: SEC001
)
assert schema.new_password == "newpass456"
def test_password_too_short(self):
with pytest.raises(ValidationError) as exc_info:
UserPasswordChange(
current_password="old", # noqa: SEC001
new_password="short1", # noqa: SEC001
confirm_password="short1", # noqa: SEC001
)
assert "at least 8" in str(exc_info.value).lower() or "min_length" in str(
exc_info.value
).lower()
def test_password_no_digit(self):
with pytest.raises(ValidationError) as exc_info:
UserPasswordChange(
current_password="oldpass123", # noqa: SEC001
new_password="nodigitss", # noqa: SEC001
confirm_password="nodigitss", # noqa: SEC001
)
assert "digit" in str(exc_info.value).lower()
def test_password_no_letter(self):
with pytest.raises(ValidationError) as exc_info:
UserPasswordChange(
current_password="oldpass123", # noqa: SEC001
new_password="12345678", # noqa: SEC001
confirm_password="12345678", # noqa: SEC001
)
assert "letter" in str(exc_info.value).lower()
@pytest.mark.unit
@pytest.mark.schema
class TestUserAccountUpdateSchema:
"""Tests for UserAccountUpdate validation."""
def test_valid_update(self):
schema = UserAccountUpdate(
first_name="John",
last_name="Doe",
email="john@example.com",
preferred_language="en",
)
assert schema.first_name == "John"
def test_partial_update(self):
schema = UserAccountUpdate(first_name="John")
assert schema.first_name == "John"
assert schema.last_name is None
assert schema.email is None
def test_email_validation(self):
with pytest.raises(ValidationError):
UserAccountUpdate(email="not-an-email")
def test_language_validation(self):
with pytest.raises(ValidationError):
UserAccountUpdate(preferred_language="xx")
def test_valid_languages(self):
for lang in ("en", "fr", "de", "lb"):
schema = UserAccountUpdate(preferred_language=lang)
assert schema.preferred_language == lang

View File

@@ -0,0 +1,146 @@
# app/modules/tenancy/tests/unit/test_user_account_service.py
"""
Unit tests for UserAccountService.
Tests self-service account operations: get, update, change password.
"""
import uuid
import pytest
from app.modules.tenancy.exceptions import (
InvalidCredentialsException,
UserAlreadyExistsException,
UserNotFoundException,
)
from app.modules.tenancy.models import User
from app.modules.tenancy.services.user_account_service import UserAccountService
from middleware.auth import AuthManager
@pytest.fixture
def auth_mgr():
return AuthManager()
@pytest.fixture
def account_service():
return UserAccountService()
@pytest.fixture
def acct_user(db, auth_mgr):
"""Create a user for account service tests."""
uid = uuid.uuid4().hex[:8]
user = User(
email=f"acct_{uid}@test.com",
username=f"acct_{uid}",
hashed_password=auth_mgr.hash_password("oldpass123"),
first_name="Test",
last_name="User",
role="super_admin",
is_active=True,
is_email_verified=True,
preferred_language="en",
)
db.add(user)
db.commit()
db.refresh(user)
return user
@pytest.mark.unit
@pytest.mark.tenancy
class TestUserAccountService:
"""Tests for UserAccountService."""
def test_get_account_returns_user(self, db, account_service, acct_user):
result = account_service.get_account(db, acct_user.id)
assert result.id == acct_user.id
assert result.email == acct_user.email
def test_get_account_not_found_raises(self, db, account_service):
with pytest.raises(UserNotFoundException):
account_service.get_account(db, 999999)
def test_update_account_first_last_name(self, db, account_service, acct_user):
result = account_service.update_account(
db, acct_user.id, {"first_name": "New", "last_name": "Name"}
)
assert result.first_name == "New"
assert result.last_name == "Name"
def test_update_account_email_uniqueness_conflict(
self, db, account_service, acct_user, auth_mgr
):
uid2 = uuid.uuid4().hex[:8]
other = User(
email=f"other_{uid2}@test.com",
username=f"other_{uid2}",
hashed_password=auth_mgr.hash_password("pass1234"),
role="store_member",
is_active=True,
)
db.add(other)
db.commit()
with pytest.raises(UserAlreadyExistsException):
account_service.update_account(
db, acct_user.id, {"email": other.email}
)
def test_update_account_preferred_language(self, db, account_service, acct_user):
result = account_service.update_account(
db, acct_user.id, {"preferred_language": "fr"}
)
assert result.preferred_language == "fr"
def test_change_password_success(self, db, account_service, acct_user, auth_mgr):
account_service.change_password(
db,
acct_user.id,
{
"current_password": "oldpass123",
"new_password": "newpass456",
"confirm_password": "newpass456",
},
)
db.refresh(acct_user)
assert auth_mgr.verify_password("newpass456", acct_user.hashed_password)
def test_change_password_wrong_current(self, db, account_service, acct_user):
with pytest.raises(InvalidCredentialsException):
account_service.change_password(
db,
acct_user.id,
{
"current_password": "wrongpass",
"new_password": "newpass456",
"confirm_password": "newpass456",
},
)
def test_change_password_mismatch_confirm(self, db, account_service, acct_user):
with pytest.raises(InvalidCredentialsException):
account_service.change_password(
db,
acct_user.id,
{
"current_password": "oldpass123",
"new_password": "newpass456",
"confirm_password": "different789",
},
)
def test_change_password_same_as_current(self, db, account_service, acct_user):
with pytest.raises(InvalidCredentialsException):
account_service.change_password(
db,
acct_user.id,
{
"current_password": "oldpass123",
"new_password": "oldpass123",
"confirm_password": "oldpass123",
},
)