""" Comprehensive unit tests for Account model. This test suite covers: - Account model validation - Password hashing/verification - Account status transitions - Tenant relationship integrity - Email uniqueness constraints """ import base64 import secrets from datetime import UTC, datetime from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest from sqlalchemy.orm import Session from libs.password import compare_password, hash_password, valid_password from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole class TestAccountModelValidation: """Test suite for Account model validation and basic operations.""" def test_account_creation_with_required_fields(self): """Test creating an account with all required fields.""" # Arrange & Act account = Account( name="Test User", email="test@example.com", password="hashed_password", password_salt="salt_value", ) # Assert assert account.name == "Test User" assert account.email == "test@example.com" assert account.password == "hashed_password" assert account.password_salt == "salt_value" assert account.status == "active" # Default value def test_account_creation_with_optional_fields(self): """Test creating an account with optional fields.""" # Arrange & Act account = Account( name="Test User", email="test@example.com", avatar="https://example.com/avatar.png", interface_language="en-US", interface_theme="dark", timezone="America/New_York", ) # Assert assert account.avatar == "https://example.com/avatar.png" assert account.interface_language == "en-US" assert account.interface_theme == "dark" assert account.timezone == "America/New_York" def test_account_creation_without_password(self): """Test creating an account without password (for invite-based registration).""" # Arrange & Act account = Account( name="Invited User", email="invited@example.com", ) # Assert assert account.password is None assert account.password_salt is None assert not account.is_password_set def test_account_is_password_set_property(self): """Test the is_password_set property.""" # Arrange account_with_password = Account( name="User With Password", email="withpass@example.com", password="hashed_password", ) account_without_password = Account( name="User Without Password", email="nopass@example.com", ) # Assert assert account_with_password.is_password_set assert not account_without_password.is_password_set def test_account_default_status(self): """Test that account has default status of 'active'.""" # Arrange & Act account = Account( name="Test User", email="test@example.com", ) # Assert assert account.status == AccountStatus.ACTIVE def test_account_get_status_method(self): """Test the get_status method returns AccountStatus enum.""" # Arrange account = Account( name="Test User", email="test@example.com", status=AccountStatus.PENDING, ) # Act status = account.get_status() # Assert assert status == AccountStatus.PENDING assert isinstance(status, AccountStatus) class TestPasswordHashingAndVerification: """Test suite for password hashing and verification functionality.""" def test_password_hashing_produces_consistent_result(self): """Test that hashing the same password with the same salt produces the same result.""" # Arrange password = "TestPassword123" salt = secrets.token_bytes(16) # Act hash1 = hash_password(password, salt) hash2 = hash_password(password, salt) # Assert assert hash1 == hash2 def test_password_hashing_different_salts_produce_different_hashes(self): """Test that different salts produce different hashes for the same password.""" # Arrange password = "TestPassword123" salt1 = secrets.token_bytes(16) salt2 = secrets.token_bytes(16) # Act hash1 = hash_password(password, salt1) hash2 = hash_password(password, salt2) # Assert assert hash1 != hash2 def test_password_comparison_success(self): """Test successful password comparison.""" # Arrange password = "TestPassword123" salt = secrets.token_bytes(16) password_hashed = hash_password(password, salt) # Encode to base64 as done in the application base64_salt = base64.b64encode(salt).decode() base64_password_hashed = base64.b64encode(password_hashed).decode() # Act result = compare_password(password, base64_password_hashed, base64_salt) # Assert assert result is True def test_password_comparison_failure(self): """Test password comparison with wrong password.""" # Arrange correct_password = "TestPassword123" wrong_password = "WrongPassword456" salt = secrets.token_bytes(16) password_hashed = hash_password(correct_password, salt) # Encode to base64 base64_salt = base64.b64encode(salt).decode() base64_password_hashed = base64.b64encode(password_hashed).decode() # Act result = compare_password(wrong_password, base64_password_hashed, base64_salt) # Assert assert result is False def test_valid_password_with_correct_format(self): """Test password validation with correct format.""" # Arrange valid_passwords = [ "Password123", "Test1234", "MySecure1Pass", "abcdefgh1", ] # Act & Assert for password in valid_passwords: result = valid_password(password) assert result == password def test_valid_password_with_incorrect_format(self): """Test password validation with incorrect format.""" # Arrange invalid_passwords = [ "short1", # Too short "NoNumbers", # No numbers "12345678", # No letters "Pass1", # Too short ] # Act & Assert for password in invalid_passwords: with pytest.raises(ValueError, match="Password must contain letters and numbers"): valid_password(password) def test_password_hashing_integration_with_account(self): """Test password hashing integration with Account model.""" # Arrange password = "SecurePass123" salt = secrets.token_bytes(16) base64_salt = base64.b64encode(salt).decode() password_hashed = hash_password(password, salt) base64_password_hashed = base64.b64encode(password_hashed).decode() # Act account = Account( name="Test User", email="test@example.com", password=base64_password_hashed, password_salt=base64_salt, ) # Assert assert account.is_password_set assert compare_password(password, account.password, account.password_salt) class TestAccountStatusTransitions: """Test suite for account status transitions.""" def test_account_status_enum_values(self): """Test that AccountStatus enum has all expected values.""" # Assert assert AccountStatus.PENDING == "pending" assert AccountStatus.UNINITIALIZED == "uninitialized" assert AccountStatus.ACTIVE == "active" assert AccountStatus.BANNED == "banned" assert AccountStatus.CLOSED == "closed" def test_account_status_transition_pending_to_active(self): """Test transitioning account status from pending to active.""" # Arrange account = Account( name="Test User", email="test@example.com", status=AccountStatus.PENDING, ) # Act account.status = AccountStatus.ACTIVE account.initialized_at = datetime.now(UTC) # Assert assert account.get_status() == AccountStatus.ACTIVE assert account.initialized_at is not None def test_account_status_transition_active_to_banned(self): """Test transitioning account status from active to banned.""" # Arrange account = Account( name="Test User", email="test@example.com", status=AccountStatus.ACTIVE, ) # Act account.status = AccountStatus.BANNED # Assert assert account.get_status() == AccountStatus.BANNED def test_account_status_transition_active_to_closed(self): """Test transitioning account status from active to closed.""" # Arrange account = Account( name="Test User", email="test@example.com", status=AccountStatus.ACTIVE, ) # Act account.status = AccountStatus.CLOSED # Assert assert account.get_status() == AccountStatus.CLOSED def test_account_status_uninitialized(self): """Test account with uninitialized status.""" # Arrange & Act account = Account( name="Test User", email="test@example.com", status=AccountStatus.UNINITIALIZED, ) # Assert assert account.get_status() == AccountStatus.UNINITIALIZED assert account.initialized_at is None class TestTenantRelationshipIntegrity: """Test suite for tenant relationship integrity.""" def test_account_current_tenant_id_property(self): """Test the current_tenant_id property.""" # Arrange account = Account( name="Test User", email="test@example.com", ) tenant = Tenant(name="Test Tenant") tenant.id = str(uuid4()) # Act - with tenant account._current_tenant = tenant tenant_id = account.current_tenant_id # Assert assert tenant_id == tenant.id # Act - without tenant account._current_tenant = None tenant_id_none = account.current_tenant_id # Assert assert tenant_id_none is None def test_set_current_tenant_with_session_uses_caller_session(self): account = Account(name="Test User", email="test@example.com") account.id = str(uuid4()) tenant = Tenant(name="Test Tenant") tenant.id = str(uuid4()) join = MagicMock(role=TenantAccountRole.OWNER) session = MagicMock(spec=Session) session.scalar.return_value = join session.scalars.return_value.one.return_value = tenant with patch("models.account.Session") as session_class: account.set_current_tenant_with_session(tenant, session=session) session_class.assert_not_called() assert account.current_tenant is tenant assert account.role == TenantAccountRole.OWNER def test_set_tenant_id_with_session_uses_caller_session(self): account = Account(name="Test User", email="test@example.com") account.id = str(uuid4()) tenant = Tenant(name="Test Tenant") tenant.id = str(uuid4()) join = MagicMock(role=TenantAccountRole.ADMIN) session = MagicMock(spec=Session) session.execute.return_value.first.return_value = (tenant, join) with patch("models.account.Session") as session_class: account.set_tenant_id_with_session(tenant.id, session=session) session_class.assert_not_called() assert account.current_tenant is tenant assert account.role == TenantAccountRole.ADMIN class TestAccountRolePermissions: """Test suite for account role permissions.""" def test_is_admin_or_owner_with_admin_role(self): """Test is_admin_or_owner property with admin role.""" # Arrange account = Account( name="Test User", email="test@example.com", ) account.role = TenantAccountRole.ADMIN # Act & Assert with patch("models.account.dify_config.RBAC_ENABLED", False): assert account.is_admin_or_owner def test_is_admin_or_owner_with_rbac_enabled(self): account = Account(name="Test User", email="test@example.com") account.role = TenantAccountRole.NORMAL with patch("models.account.dify_config.RBAC_ENABLED", True): assert account.is_admin_or_owner def test_is_admin_or_owner_with_owner_role(self): """Test is_admin_or_owner property with owner role.""" # Arrange account = Account( name="Test User", email="test@example.com", ) account.role = TenantAccountRole.OWNER # Act & Assert assert account.is_admin_or_owner def test_is_admin_or_owner_with_normal_role(self): """Test is_admin_or_owner property with normal role.""" # Arrange account = Account( name="Test User", email="test@example.com", ) account.role = TenantAccountRole.NORMAL # Act & Assert assert not account.is_admin_or_owner def test_is_admin_property(self): """Test is_admin property.""" # Arrange admin_account = Account(name="Admin", email="admin@example.com") admin_account.role = TenantAccountRole.ADMIN owner_account = Account(name="Owner", email="owner@example.com") owner_account.role = TenantAccountRole.OWNER # Act & Assert with patch("models.account.dify_config.RBAC_ENABLED", False): assert admin_account.is_admin assert not owner_account.is_admin def test_is_admin_with_rbac_enabled(self): account = Account(name="Test User", email="test@example.com") account.role = TenantAccountRole.NORMAL with patch("models.account.dify_config.RBAC_ENABLED", True): assert account.is_admin def test_has_edit_permission_with_editing_roles(self): """Test has_edit_permission property with roles that have edit permission.""" # Arrange roles_with_edit = [ TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR, ] for role in roles_with_edit: account = Account(name="Test User", email=f"test_{role}@example.com") account.role = role # Act & Assert with patch("models.account.dify_config.RBAC_ENABLED", False): assert account.has_edit_permission, f"Role {role} should have edit permission" def test_has_edit_permission_with_rbac_enabled(self): account = Account(name="Test User", email="test@example.com") account.role = TenantAccountRole.NORMAL with patch("models.account.dify_config.RBAC_ENABLED", True): assert account.has_edit_permission def test_has_edit_permission_without_editing_roles(self): """Test has_edit_permission property with roles that don't have edit permission.""" # Arrange roles_without_edit = [ TenantAccountRole.NORMAL, TenantAccountRole.DATASET_OPERATOR, ] for role in roles_without_edit: account = Account(name="Test User", email=f"test_{role}@example.com") account.role = role # Act & Assert with patch("models.account.dify_config.RBAC_ENABLED", False): assert not account.has_edit_permission, f"Role {role} should not have edit permission" def test_is_dataset_editor_property(self): """Test is_dataset_editor property.""" # Arrange dataset_roles = [ TenantAccountRole.OWNER, TenantAccountRole.ADMIN, TenantAccountRole.EDITOR, TenantAccountRole.DATASET_OPERATOR, ] for role in dataset_roles: account = Account(name="Test User", email=f"test_{role}@example.com") account.role = role # Act & Assert with patch("models.account.dify_config.RBAC_ENABLED", False): assert account.is_dataset_editor, f"Role {role} should have dataset edit permission" # Test normal role doesn't have dataset edit permission normal_account = Account(name="Normal User", email="normal@example.com") normal_account.role = TenantAccountRole.NORMAL with patch("models.account.dify_config.RBAC_ENABLED", False): assert not normal_account.is_dataset_editor def test_is_dataset_editor_with_rbac_enabled(self): account = Account(name="Test User", email="test@example.com") account.role = TenantAccountRole.NORMAL with patch("models.account.dify_config.RBAC_ENABLED", True): assert account.is_dataset_editor def test_is_dataset_operator_property(self): """Test is_dataset_operator property.""" # Arrange dataset_operator = Account(name="Dataset Operator", email="operator@example.com") dataset_operator.role = TenantAccountRole.DATASET_OPERATOR normal_account = Account(name="Normal User", email="normal@example.com") normal_account.role = TenantAccountRole.NORMAL # Act & Assert with patch("models.account.dify_config.RBAC_ENABLED", False): assert dataset_operator.is_dataset_operator assert not normal_account.is_dataset_operator def test_is_dataset_operator_with_rbac_enabled(self): account = Account(name="Test User", email="test@example.com") account.role = TenantAccountRole.NORMAL with patch("models.account.dify_config.RBAC_ENABLED", True): assert account.is_dataset_operator def test_current_role_property(self): """Test current_role property.""" # Arrange account = Account(name="Test User", email="test@example.com") account.role = TenantAccountRole.EDITOR # Act current_role = account.current_role # Assert assert current_role == TenantAccountRole.EDITOR class TestTenantAccountJoinModel: """Test suite for TenantAccountJoin model.""" def test_tenant_account_join_creation(self): """Test creating a TenantAccountJoin record.""" # Arrange tenant_id = str(uuid4()) account_id = str(uuid4()) # Act join = TenantAccountJoin( tenant_id=tenant_id, account_id=account_id, role=TenantAccountRole.NORMAL, current=True, ) # Assert assert join.tenant_id == tenant_id assert join.account_id == account_id assert join.role == TenantAccountRole.NORMAL assert join.current is True def test_tenant_account_join_default_values(self): """Test default values for TenantAccountJoin.""" # Arrange tenant_id = str(uuid4()) account_id = str(uuid4()) # Act join = TenantAccountJoin( tenant_id=tenant_id, account_id=account_id, ) # Assert assert join.current is False # Default value assert join.role == "normal" # Default value assert join.invited_by is None # Default value def test_tenant_account_join_with_invited_by(self): """Test TenantAccountJoin with invited_by field.""" # Arrange tenant_id = str(uuid4()) account_id = str(uuid4()) inviter_id = str(uuid4()) # Act join = TenantAccountJoin( tenant_id=tenant_id, account_id=account_id, role=TenantAccountRole.EDITOR, invited_by=inviter_id, ) # Assert assert join.invited_by == inviter_id class TestTenantModel: """Test suite for Tenant model.""" def test_tenant_creation(self): """Test creating a Tenant.""" # Arrange & Act tenant = Tenant(name="Test Workspace") # Assert assert tenant.name == "Test Workspace" assert tenant.status == "normal" # Default value assert tenant.plan == "basic" # Default value def test_tenant_custom_config_dict_property(self): """Test custom_config_dict property getter.""" # Arrange tenant = Tenant(name="Test Workspace") config = {"feature1": True, "feature2": "value"} tenant.custom_config = '{"feature1": true, "feature2": "value"}' # Act result = tenant.custom_config_dict # Assert assert result["feature1"] is True assert result["feature2"] == "value" def test_tenant_custom_config_dict_property_empty(self): """Test custom_config_dict property with empty config.""" # Arrange tenant = Tenant(name="Test Workspace") tenant.custom_config = None # Act result = tenant.custom_config_dict # Assert assert result == {} def test_tenant_custom_config_dict_setter(self): """Test custom_config_dict property setter.""" # Arrange tenant = Tenant(name="Test Workspace") config = {"feature1": True, "feature2": "value"} # Act tenant.custom_config_dict = config # Assert assert tenant.custom_config == '{"feature1": true, "feature2": "value"}' class TestTenantStatusEnum: """Test suite for TenantStatus enum.""" def test_tenant_status_enum_values(self): """Test TenantStatus enum values.""" # Arrange & Act from models.account import TenantStatus # Assert assert TenantStatus.NORMAL == "normal" assert TenantStatus.ARCHIVE == "archive" class TestAccountIntegration: """Integration tests for Account model with related models.""" def test_account_with_multiple_tenants(self): """Test account associated with multiple tenants.""" # Arrange account = Account(name="Multi-Tenant User", email="multi@example.com") account.id = str(uuid4()) tenant1_id = str(uuid4()) tenant2_id = str(uuid4()) join1 = TenantAccountJoin( tenant_id=tenant1_id, account_id=account.id, role=TenantAccountRole.OWNER, current=True, ) join2 = TenantAccountJoin( tenant_id=tenant2_id, account_id=account.id, role=TenantAccountRole.NORMAL, current=False, ) # Assert - verify the joins are created correctly assert join1.account_id == account.id assert join2.account_id == account.id assert join1.current is True assert join2.current is False def test_account_last_login_tracking(self): """Test account last login tracking.""" # Arrange account = Account(name="Test User", email="test@example.com") login_time = datetime.now(UTC) login_ip = "192.168.1.1" # Act account.last_login_at = login_time account.last_login_ip = login_ip # Assert assert account.last_login_at == login_time assert account.last_login_ip == login_ip def test_account_initialization_tracking(self): """Test account initialization tracking.""" # Arrange account = Account( name="Test User", email="test@example.com", status=AccountStatus.PENDING, ) # Act - simulate initialization account.status = AccountStatus.ACTIVE account.initialized_at = datetime.now(UTC) # Assert assert account.get_status() == AccountStatus.ACTIVE assert account.initialized_at is not None