"""Unit tests for VoiceSettings value object.""" import pytest from src.domain.models.voice_settings import VoiceSettings class TestVoiceSettings: """Test cases for VoiceSettings value object.""" def test_valid_voice_settings_creation(self): """Test creating valid VoiceSettings instance.""" settings = VoiceSettings( voice_id="en_male_001", speed=1.2, language="en", pitch=0.1, volume=0.8 ) assert settings.voice_id == "en_male_001" assert settings.speed == 1.2 assert settings.language == "en" assert settings.pitch == 0.1 assert settings.volume == 0.8 def test_voice_settings_with_optional_none(self): """Test creating VoiceSettings with optional parameters as None.""" settings = VoiceSettings( voice_id="en_male_001", speed=1.0, language="en" ) assert settings.pitch is None assert settings.volume is None def test_non_string_voice_id_raises_error(self): """Test that non-string voice_id raises TypeError.""" with pytest.raises(TypeError, match="Voice ID must be a string"): VoiceSettings( voice_id=123, # type: ignore speed=1.0, language="en" ) def test_empty_voice_id_raises_error(self): """Test that empty voice_id raises ValueError.""" with pytest.raises(ValueError, match="Voice ID cannot be empty"): VoiceSettings( voice_id="", speed=1.0, language="en" ) def test_whitespace_voice_id_raises_error(self): """Test that whitespace-only voice_id raises ValueError.""" with pytest.raises(ValueError, match="Voice ID cannot be empty"): VoiceSettings( voice_id=" ", speed=1.0, language="en" ) def test_invalid_voice_id_format_raises_error(self): """Test that invalid voice_id format raises ValueError.""" invalid_ids = ["voice id", "voice@id", "voice.id", "voice/id", "voice\\id"] for voice_id in invalid_ids: with pytest.raises(ValueError, match="Invalid voice ID format"): VoiceSettings( voice_id=voice_id, speed=1.0, language="en" ) def test_valid_voice_id_formats(self): """Test valid voice_id formats.""" valid_ids = ["voice1", "voice_1", "voice-1", "en_male_001", "female-voice", "Voice123"] for voice_id in valid_ids: settings = VoiceSettings( voice_id=voice_id, speed=1.0, language="en" ) assert settings.voice_id == voice_id def test_non_numeric_speed_raises_error(self): """Test that non-numeric speed raises TypeError.""" with pytest.raises(TypeError, match="Speed must be a number"): VoiceSettings( voice_id="voice1", speed="fast", # type: ignore language="en" ) def test_speed_too_low_raises_error(self): """Test that speed below 0.1 raises ValueError.""" with pytest.raises(ValueError, match="Speed must be between 0.1 and 3.0"): VoiceSettings( voice_id="voice1", speed=0.05, language="en" ) def test_speed_too_high_raises_error(self): """Test that speed above 3.0 raises ValueError.""" with pytest.raises(ValueError, match="Speed must be between 0.1 and 3.0"): VoiceSettings( voice_id="voice1", speed=3.1, language="en" ) def test_valid_speed_boundaries(self): """Test valid speed boundaries.""" # Test minimum valid speed settings_min = VoiceSettings( voice_id="voice1", speed=0.1, language="en" ) assert settings_min.speed == 0.1 # Test maximum valid speed settings_max = VoiceSettings( voice_id="voice1", speed=3.0, language="en" ) assert settings_max.speed == 3.0 def test_non_string_language_raises_error(self): """Test that non-string language raises TypeError.""" with pytest.raises(TypeError, match="Language must be a string"): VoiceSettings( voice_id="voice1", speed=1.0, language=123 # type: ignore ) def test_empty_language_raises_error(self): """Test that empty language raises ValueError.""" with pytest.raises(ValueError, match="Language cannot be empty"): VoiceSettings( voice_id="voice1", speed=1.0, language="" ) def test_invalid_language_code_format_raises_error(self): """Test that invalid language code format raises ValueError.""" invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"] for code in invalid_codes: with pytest.raises(ValueError, match="Invalid language code format"): VoiceSettings( voice_id="voice1", speed=1.0, language=code ) def test_valid_language_codes(self): """Test valid language code formats.""" valid_codes = ["en", "fr", "de", "es", "zh", "ja", "en-US", "fr-FR", "zh-CN"] for code in valid_codes: settings = VoiceSettings( voice_id="voice1", speed=1.0, language=code ) assert settings.language == code def test_non_numeric_pitch_raises_error(self): """Test that non-numeric pitch raises TypeError.""" with pytest.raises(TypeError, match="Pitch must be a number"): VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch="high" # type: ignore ) def test_pitch_too_low_raises_error(self): """Test that pitch below -2.0 raises ValueError.""" with pytest.raises(ValueError, match="Pitch must be between -2.0 and 2.0"): VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=-2.1 ) def test_pitch_too_high_raises_error(self): """Test that pitch above 2.0 raises ValueError.""" with pytest.raises(ValueError, match="Pitch must be between -2.0 and 2.0"): VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=2.1 ) def test_valid_pitch_boundaries(self): """Test valid pitch boundaries.""" # Test minimum valid pitch settings_min = VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=-2.0 ) assert settings_min.pitch == -2.0 # Test maximum valid pitch settings_max = VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=2.0 ) assert settings_max.pitch == 2.0 def test_non_numeric_volume_raises_error(self): """Test that non-numeric volume raises TypeError.""" with pytest.raises(TypeError, match="Volume must be a number"): VoiceSettings( voice_id="voice1", speed=1.0, language="en", volume="loud" # type: ignore ) def test_volume_too_low_raises_error(self): """Test that volume below 0.0 raises ValueError.""" with pytest.raises(ValueError, match="Volume must be between 0.0 and 2.0"): VoiceSettings( voice_id="voice1", speed=1.0, language="en", volume=-0.1 ) def test_volume_too_high_raises_error(self): """Test that volume above 2.0 raises ValueError.""" with pytest.raises(ValueError, match="Volume must be between 0.0 and 2.0"): VoiceSettings( voice_id="voice1", speed=1.0, language="en", volume=2.1 ) def test_valid_volume_boundaries(self): """Test valid volume boundaries.""" # Test minimum valid volume settings_min = VoiceSettings( voice_id="voice1", speed=1.0, language="en", volume=0.0 ) assert settings_min.volume == 0.0 # Test maximum valid volume settings_max = VoiceSettings( voice_id="voice1", speed=1.0, language="en", volume=2.0 ) assert settings_max.volume == 2.0 def test_is_default_speed_property(self): """Test is_default_speed property.""" # Default speed (1.0) settings_default = VoiceSettings( voice_id="voice1", speed=1.0, language="en" ) assert settings_default.is_default_speed is True # Non-default speed settings_non_default = VoiceSettings( voice_id="voice1", speed=1.5, language="en" ) assert settings_non_default.is_default_speed is False def test_is_default_pitch_property(self): """Test is_default_pitch property.""" # Default pitch (None) settings_none = VoiceSettings( voice_id="voice1", speed=1.0, language="en" ) assert settings_none.is_default_pitch is True # Default pitch (0.0) settings_zero = VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=0.0 ) assert settings_zero.is_default_pitch is True # Non-default pitch settings_non_default = VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=0.5 ) assert settings_non_default.is_default_pitch is False def test_is_default_volume_property(self): """Test is_default_volume property.""" # Default volume (None) settings_none = VoiceSettings( voice_id="voice1", speed=1.0, language="en" ) assert settings_none.is_default_volume is True # Default volume (1.0) settings_one = VoiceSettings( voice_id="voice1", speed=1.0, language="en", volume=1.0 ) assert settings_one.is_default_volume is True # Non-default volume settings_non_default = VoiceSettings( voice_id="voice1", speed=1.0, language="en", volume=0.5 ) assert settings_non_default.is_default_volume is False def test_with_speed_method(self): """Test with_speed method creates new instance.""" original = VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=0.1, volume=0.8 ) new_settings = original.with_speed(1.5) assert new_settings.speed == 1.5 assert new_settings.voice_id == original.voice_id assert new_settings.language == original.language assert new_settings.pitch == original.pitch assert new_settings.volume == original.volume assert new_settings is not original # Different instances def test_with_pitch_method(self): """Test with_pitch method creates new instance.""" original = VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=0.1, volume=0.8 ) new_settings = original.with_pitch(0.5) assert new_settings.pitch == 0.5 assert new_settings.voice_id == original.voice_id assert new_settings.speed == original.speed assert new_settings.language == original.language assert new_settings.volume == original.volume assert new_settings is not original # Different instances def test_with_pitch_none(self): """Test with_pitch method with None value.""" original = VoiceSettings( voice_id="voice1", speed=1.0, language="en", pitch=0.1 ) new_settings = original.with_pitch(None) assert new_settings.pitch is None def test_voice_settings_is_immutable(self): """Test that VoiceSettings is immutable (frozen dataclass).""" settings = VoiceSettings( voice_id="voice1", speed=1.0, language="en" ) with pytest.raises(AttributeError): settings.speed = 1.5 # type: ignore