A local HTTP service that accepts text via POST and speaks it through system speakers using Piper TTS neural voice synthesis. Features: - POST /notify - Queue text for TTS playback - GET /health - Health check with TTS/audio/queue status - GET /voices - List installed voice models - Async queue processing (no overlapping audio) - Non-blocking audio via sounddevice - 73 tests covering API contract Tech stack: - FastAPI + Uvicorn - Piper TTS (neural voices, offline) - sounddevice (PortAudio) - Pydantic for validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
301 lines
8.1 KiB
Python
301 lines
8.1 KiB
Python
"""
|
|
TDD Tests for configuration loading.
|
|
|
|
These tests define the expected behavior for the Settings class which loads
|
|
configuration from environment variables with sensible defaults.
|
|
|
|
Test Coverage:
|
|
- Default values when no environment variables are set
|
|
- Environment variable overrides
|
|
- Validation of configuration values
|
|
- Path handling for model directory
|
|
"""
|
|
|
|
import os
|
|
import pytest
|
|
|
|
|
|
class TestSettingsDefaults:
|
|
"""Tests for default configuration values."""
|
|
|
|
def test_default_host(self, monkeypatch):
|
|
"""
|
|
Default host should be 0.0.0.0 (listen on all interfaces).
|
|
"""
|
|
# Clear any existing env vars
|
|
monkeypatch.delenv("HOST", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.host == "0.0.0.0"
|
|
|
|
def test_default_port(self, monkeypatch):
|
|
"""
|
|
Default port should be 8888.
|
|
"""
|
|
monkeypatch.delenv("PORT", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.port == 8888
|
|
|
|
def test_default_model_dir(self, monkeypatch):
|
|
"""
|
|
Default model directory should be ./models.
|
|
"""
|
|
monkeypatch.delenv("MODEL_DIR", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.model_dir == "./models"
|
|
|
|
def test_default_voice(self, monkeypatch):
|
|
"""
|
|
Default voice should be en_US-lessac-medium.
|
|
"""
|
|
monkeypatch.delenv("DEFAULT_VOICE", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.default_voice == "en_US-lessac-medium"
|
|
|
|
def test_default_rate(self, monkeypatch):
|
|
"""
|
|
Default speech rate should be 170 WPM.
|
|
"""
|
|
monkeypatch.delenv("DEFAULT_RATE", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.default_rate == 170
|
|
|
|
def test_default_queue_max_size(self, monkeypatch):
|
|
"""
|
|
Default queue max size should be 50.
|
|
"""
|
|
monkeypatch.delenv("QUEUE_MAX_SIZE", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.queue_max_size == 50
|
|
|
|
def test_default_request_timeout(self, monkeypatch):
|
|
"""
|
|
Default request timeout should be 60 seconds.
|
|
"""
|
|
monkeypatch.delenv("REQUEST_TIMEOUT_SECONDS", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.request_timeout_seconds == 60
|
|
|
|
def test_default_log_level(self, monkeypatch):
|
|
"""
|
|
Default log level should be INFO.
|
|
"""
|
|
monkeypatch.delenv("LOG_LEVEL", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.log_level == "INFO"
|
|
|
|
def test_default_voice_enabled(self, monkeypatch):
|
|
"""
|
|
Voice should be enabled by default.
|
|
"""
|
|
monkeypatch.delenv("VOICE_ENABLED", raising=False)
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.voice_enabled is True
|
|
|
|
|
|
class TestSettingsEnvOverrides:
|
|
"""Tests for environment variable overrides."""
|
|
|
|
def test_host_override(self, monkeypatch):
|
|
"""
|
|
HOST environment variable should override default.
|
|
"""
|
|
monkeypatch.setenv("HOST", "127.0.0.1")
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.host == "127.0.0.1"
|
|
|
|
def test_port_override(self, monkeypatch):
|
|
"""
|
|
PORT environment variable should override default.
|
|
"""
|
|
monkeypatch.setenv("PORT", "9000")
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.port == 9000
|
|
|
|
def test_model_dir_override(self, monkeypatch):
|
|
"""
|
|
MODEL_DIR environment variable should override default.
|
|
"""
|
|
monkeypatch.setenv("MODEL_DIR", "/opt/voice-models")
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.model_dir == "/opt/voice-models"
|
|
|
|
def test_default_voice_override(self, monkeypatch):
|
|
"""
|
|
DEFAULT_VOICE environment variable should override default.
|
|
"""
|
|
monkeypatch.setenv("DEFAULT_VOICE", "en_US-libritts-high")
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.default_voice == "en_US-libritts-high"
|
|
|
|
def test_queue_max_size_override(self, monkeypatch):
|
|
"""
|
|
QUEUE_MAX_SIZE environment variable should override default.
|
|
"""
|
|
monkeypatch.setenv("QUEUE_MAX_SIZE", "100")
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.queue_max_size == 100
|
|
|
|
def test_log_level_override(self, monkeypatch):
|
|
"""
|
|
LOG_LEVEL environment variable should override default.
|
|
"""
|
|
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.log_level == "DEBUG"
|
|
|
|
def test_voice_enabled_false(self, monkeypatch):
|
|
"""
|
|
VOICE_ENABLED=false should disable voice output.
|
|
"""
|
|
monkeypatch.setenv("VOICE_ENABLED", "false")
|
|
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.voice_enabled is False
|
|
|
|
|
|
class TestSettingsValidation:
|
|
"""Tests for configuration validation."""
|
|
|
|
def test_port_must_be_positive(self, monkeypatch):
|
|
"""
|
|
Port must be a positive integer.
|
|
"""
|
|
monkeypatch.setenv("PORT", "-1")
|
|
|
|
from pydantic import ValidationError
|
|
from app.config import Settings
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
def test_port_must_be_valid_range(self, monkeypatch):
|
|
"""
|
|
Port must be in valid range (1-65535).
|
|
"""
|
|
monkeypatch.setenv("PORT", "70000")
|
|
|
|
from pydantic import ValidationError
|
|
from app.config import Settings
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
def test_queue_max_size_must_be_positive(self, monkeypatch):
|
|
"""
|
|
Queue max size must be positive.
|
|
"""
|
|
monkeypatch.setenv("QUEUE_MAX_SIZE", "0")
|
|
|
|
from pydantic import ValidationError
|
|
from app.config import Settings
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
def test_request_timeout_must_be_positive(self, monkeypatch):
|
|
"""
|
|
Request timeout must be positive.
|
|
"""
|
|
monkeypatch.setenv("REQUEST_TIMEOUT_SECONDS", "0")
|
|
|
|
from pydantic import ValidationError
|
|
from app.config import Settings
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
def test_default_rate_must_be_in_range(self, monkeypatch):
|
|
"""
|
|
Default rate must be between 50 and 400.
|
|
"""
|
|
monkeypatch.setenv("DEFAULT_RATE", "500")
|
|
|
|
from pydantic import ValidationError
|
|
from app.config import Settings
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
def test_log_level_must_be_valid(self, monkeypatch):
|
|
"""
|
|
Log level must be a valid Python logging level.
|
|
"""
|
|
monkeypatch.setenv("LOG_LEVEL", "INVALID")
|
|
|
|
from pydantic import ValidationError
|
|
from app.config import Settings
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
|
|
class TestGetSettings:
|
|
"""Tests for the get_settings function."""
|
|
|
|
def test_get_settings_returns_settings_instance(self, monkeypatch):
|
|
"""
|
|
get_settings should return a Settings instance.
|
|
"""
|
|
# Clear cache to ensure fresh settings
|
|
from app.config import get_settings, Settings
|
|
|
|
settings = get_settings()
|
|
assert isinstance(settings, Settings)
|
|
|
|
def test_get_settings_is_cached(self, monkeypatch):
|
|
"""
|
|
get_settings should return the same cached instance.
|
|
"""
|
|
from app.config import get_settings
|
|
|
|
settings1 = get_settings()
|
|
settings2 = get_settings()
|
|
assert settings1 is settings2
|