Added user profile modification
This commit is contained in:
@@ -343,6 +343,297 @@ class TestAuthServiceTokenManagement(object):
|
||||
with pytest.raises(ExpiredTokenError):
|
||||
auth_service.get_current_user("expired_access_jwt")
|
||||
|
||||
|
||||
class TestAuthServiceUserUpdate(object):
|
||||
"""Tests for user update operations."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_user(self, auth_service: AuthService, test_user_data_create: UserCreate):
|
||||
"""Sets up a registered user for update tests."""
|
||||
|
||||
pm = auth_service.password_manager
|
||||
original_hash = pm.hash_password.return_value
|
||||
|
||||
# Temporarily set hash for setup
|
||||
pm.hash_password.return_value = "HASHED_PASS"
|
||||
user = auth_service.register(test_user_data_create)
|
||||
self.user = user
|
||||
self.original_email = user.email
|
||||
self.original_username = user.username
|
||||
|
||||
# Restore hash mock
|
||||
pm.hash_password.return_value = original_hash
|
||||
|
||||
def test_i_can_update_user_email(self, auth_service: AuthService):
|
||||
"""Success: Email can be updated and is_verified is automatically set to False."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
new_email = "updated.email@example.com"
|
||||
updates = UserUpdate(email=new_email)
|
||||
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
assert updated_user.email == new_email
|
||||
assert updated_user.is_verified is False
|
||||
|
||||
def test_i_can_update_user_username(self, auth_service: AuthService):
|
||||
"""Success: Username can be updated."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
new_username = "UpdatedUsername"
|
||||
updates = UserUpdate(username=new_username)
|
||||
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
assert updated_user.username == new_username
|
||||
|
||||
def test_i_can_update_user_password(self, auth_service: AuthService):
|
||||
"""Success: Password can be updated and is properly hashed."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
pm = auth_service.password_manager
|
||||
new_password = "NewSecurePass123!"
|
||||
|
||||
with patch.object(pm, 'hash_password', return_value="NEW_HASHED_PASSWORD") as mock_hash:
|
||||
updates = UserUpdate(password=new_password)
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
mock_hash.assert_called_once_with(new_password)
|
||||
assert updated_user.hashed_password == "NEW_HASHED_PASSWORD"
|
||||
|
||||
def test_i_can_update_user_password_and_all_refresh_tokens_are_revoked(
|
||||
self,
|
||||
auth_service: AuthService
|
||||
):
|
||||
"""Success: Updating password revokes all refresh tokens when no current token provided."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
# Setup: Create some refresh tokens for the user
|
||||
from myauth.models.token import TokenData
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
token1 = TokenData(
|
||||
token="refresh_token_1",
|
||||
token_type="refresh",
|
||||
user_id=self.user.id,
|
||||
expires_at=datetime.now() + timedelta(days=1),
|
||||
created_at=datetime.now(),
|
||||
is_revoked=False
|
||||
)
|
||||
token2 = TokenData(
|
||||
token="refresh_token_2",
|
||||
token_type="refresh",
|
||||
user_id=self.user.id,
|
||||
expires_at=datetime.now() + timedelta(days=1),
|
||||
created_at=datetime.now(),
|
||||
is_revoked=False
|
||||
)
|
||||
auth_service.token_repository.save_token(token1)
|
||||
auth_service.token_repository.save_token(token2)
|
||||
|
||||
# Execute: Update password without providing current token
|
||||
updates = UserUpdate(password="NewPassword123!")
|
||||
auth_service.update_user(self.user.id, updates)
|
||||
|
||||
# Verify: Both tokens are revoked
|
||||
token1_after = auth_service.token_repository.get_token("refresh_token_1", "refresh")
|
||||
token2_after = auth_service.token_repository.get_token("refresh_token_2", "refresh")
|
||||
|
||||
assert token1_after.is_revoked is True
|
||||
assert token2_after.is_revoked is True
|
||||
|
||||
def test_i_can_update_user_password_and_preserve_current_session(
|
||||
self,
|
||||
auth_service: AuthService
|
||||
):
|
||||
"""Success: Updating password preserves the current refresh token when provided."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
from myauth.models.token import TokenData
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Setup: Create some refresh tokens
|
||||
current_token = TokenData(
|
||||
token="current_refresh_token",
|
||||
token_type="refresh",
|
||||
user_id=self.user.id,
|
||||
expires_at=datetime.now() + timedelta(days=1),
|
||||
created_at=datetime.now(),
|
||||
is_revoked=False
|
||||
)
|
||||
other_token = TokenData(
|
||||
token="other_refresh_token",
|
||||
token_type="refresh",
|
||||
user_id=self.user.id,
|
||||
expires_at=datetime.now() + timedelta(days=1),
|
||||
created_at=datetime.now(),
|
||||
is_revoked=False
|
||||
)
|
||||
auth_service.token_repository.save_token(current_token)
|
||||
auth_service.token_repository.save_token(other_token)
|
||||
|
||||
# Execute: Update password while providing current token
|
||||
updates = UserUpdate(password="NewPassword123!")
|
||||
auth_service.update_user(self.user.id, updates, refresh_token="current_refresh_token")
|
||||
|
||||
# Verify: Current token is preserved, other is revoked
|
||||
current_after = auth_service.token_repository.get_token("current_refresh_token", "refresh")
|
||||
other_after = auth_service.token_repository.get_token("other_refresh_token", "refresh")
|
||||
|
||||
assert current_after.is_revoked is False
|
||||
assert other_after.is_revoked is True
|
||||
|
||||
def test_i_can_update_multiple_fields_at_once(self, auth_service: AuthService):
|
||||
"""Success: Multiple fields can be updated simultaneously."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
updates = UserUpdate(
|
||||
username="MultiUpdateUser",
|
||||
roles=["admin", "member"],
|
||||
user_settings={"theme": "light", "language": "en"}
|
||||
)
|
||||
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
assert updated_user.username == "MultiUpdateUser"
|
||||
assert updated_user.roles == ["admin", "member"]
|
||||
assert updated_user.user_settings == {"theme": "light", "language": "en"}
|
||||
|
||||
def test_i_can_update_user_roles(self, auth_service: AuthService):
|
||||
"""Success: User roles can be updated."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
new_roles = ["admin", "moderator"]
|
||||
updates = UserUpdate(roles=new_roles)
|
||||
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
assert updated_user.roles == new_roles
|
||||
|
||||
def test_i_can_update_user_settings(self, auth_service: AuthService):
|
||||
"""Success: User settings can be updated."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
new_settings = {"theme": "light", "notifications": True, "language": "fr"}
|
||||
updates = UserUpdate(user_settings=new_settings)
|
||||
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
assert updated_user.user_settings == new_settings
|
||||
|
||||
def test_i_can_update_is_active_status(self, auth_service: AuthService):
|
||||
"""Success: User active status can be updated."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
# Deactivate user
|
||||
updates = UserUpdate(is_active=False)
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
assert updated_user.is_active is False
|
||||
|
||||
# Reactivate user
|
||||
updates = UserUpdate(is_active=True)
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
assert updated_user.is_active is True
|
||||
|
||||
def test_i_cannot_update_user_with_invalid_user_id(self, auth_service: AuthService):
|
||||
"""Failure: Updating a non-existent user raises UserNotFoundError."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
from myauth.exceptions import UserNotFoundError
|
||||
|
||||
updates = UserUpdate(username="ShouldFail")
|
||||
|
||||
with pytest.raises(UserNotFoundError):
|
||||
auth_service.update_user("non_existent_id", updates)
|
||||
|
||||
def test_i_cannot_update_email_to_existing_email(self, auth_service: AuthService):
|
||||
"""Failure: Updating email to an already registered email raises UserAlreadyExistsError."""
|
||||
|
||||
from myauth.models.user import UserCreate, UserUpdate
|
||||
|
||||
# Setup: Create another user with a different email
|
||||
other_user_data = UserCreate(
|
||||
email="other.user@example.com",
|
||||
username="OtherUser",
|
||||
password="OtherPass123!",
|
||||
roles=["member"]
|
||||
)
|
||||
auth_service.register(other_user_data)
|
||||
|
||||
# Execute: Try to update original user's email to the other user's email
|
||||
updates = UserUpdate(email="other.user@example.com")
|
||||
|
||||
with pytest.raises(UserAlreadyExistsError):
|
||||
auth_service.update_user(self.user.id, updates)
|
||||
|
||||
def test_i_cannot_update_email_to_same_email_without_triggering_verification_reset(
|
||||
self,
|
||||
auth_service: AuthService
|
||||
):
|
||||
"""Success: Updating email to the same email does not reset is_verified."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
# Setup: Ensure user is verified
|
||||
verify_updates = UserUpdate(is_verified=True)
|
||||
auth_service.update_user(self.user.id, verify_updates)
|
||||
|
||||
# Execute: Update with the same email
|
||||
updates = UserUpdate(email=self.original_email)
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
# Verify: is_verified should remain True
|
||||
assert updated_user.is_verified is True
|
||||
assert updated_user.email == self.original_email
|
||||
|
||||
def test_i_can_update_with_empty_updates(self, auth_service: AuthService):
|
||||
"""Success: Updating with empty UserUpdate does not cause errors."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
updates = UserUpdate()
|
||||
updated_user = auth_service.update_user(self.user.id, updates)
|
||||
|
||||
# Verify: User is returned and fields are unchanged
|
||||
assert updated_user.id == self.user.id
|
||||
assert updated_user.email == self.original_email
|
||||
assert updated_user.username == self.original_username
|
||||
|
||||
def test_email_verification_reset_only_when_email_actually_changes(
|
||||
self,
|
||||
auth_service: AuthService
|
||||
):
|
||||
"""Success: is_verified is reset to False only when email actually changes."""
|
||||
|
||||
from myauth.models.user import UserUpdate
|
||||
|
||||
# Setup: Set user as verified
|
||||
verify_updates = UserUpdate(is_verified=True)
|
||||
auth_service.update_user(self.user.id, verify_updates)
|
||||
verified_user = auth_service.user_repository.get_user_by_id(self.user.id)
|
||||
assert verified_user.is_verified is True
|
||||
|
||||
# Test 1: Update with same email - verification should remain
|
||||
same_email_updates = UserUpdate(email=self.original_email, username="SameEmailTest")
|
||||
updated_user = auth_service.update_user(self.user.id, same_email_updates)
|
||||
assert updated_user.is_verified is True
|
||||
|
||||
# Test 2: Update with different email - verification should reset
|
||||
different_email_updates = UserUpdate(email="completely.new@example.com")
|
||||
updated_user = auth_service.update_user(self.user.id, different_email_updates)
|
||||
assert updated_user.is_verified is False
|
||||
assert updated_user.email == "completely.new@example.com"
|
||||
|
||||
# class TestAuthServiceResetVerification(object):
|
||||
# """Tests for password reset and email verification flows."""
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user