Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Tests package initialization
227 changes: 227 additions & 0 deletions backend/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
"""
Comprehensive unit tests for app.core.config module.
Tests boundary values, equivalence classes, and actual behavior of Settings class.
"""
import pytest
from pydantic import ValidationError
from app.core.config import Settings


class TestSettingsDefaults:
"""Test default values of Settings class."""

def test_project_name_default(self, monkeypatch):
"""Test PROJECT_NAME has correct default value."""
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.PROJECT_NAME == "Insurance Management Platform"

def test_api_v1_str_default(self, monkeypatch):
"""Test API_V1_STR has correct default value."""
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.API_V1_STR == "/api/v1"

def test_access_token_expire_minutes_default(self, monkeypatch):
"""Test ACCESS_TOKEN_EXPIRE_MINUTES has correct default value (7 days)."""
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 60 * 24 * 7
assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 10080

def test_cors_origins_default(self, monkeypatch):
"""Test CORS_ORIGINS has correct default value."""
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.CORS_ORIGINS == "http://localhost:3000"


class TestSettingsRequiredFields:
"""Test required fields validation."""

def test_secret_key_required(self, monkeypatch):
"""Test SECRET_KEY is required and raises error when missing."""
monkeypatch.delenv("SECRET_KEY", raising=False)
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
with pytest.raises(ValidationError) as exc_info:
Settings()
errors = exc_info.value.errors()
assert any(error['loc'] == ('SECRET_KEY',) for error in errors)

def test_database_url_required(self, monkeypatch):
"""Test DATABASE_URL is required and raises error when missing."""
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.delenv("DATABASE_URL", raising=False)
with pytest.raises(ValidationError) as exc_info:
Settings()
errors = exc_info.value.errors()
assert any(error['loc'] == ('DATABASE_URL',) for error in errors)


class TestSettingsEnvironmentVariables:
"""Test environment variable loading and overriding."""

def test_secret_key_from_env(self, monkeypatch):
"""Test SECRET_KEY loads from environment variable."""
test_secret = "my-super-secret-key-12345"
monkeypatch.setenv("SECRET_KEY", test_secret)
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.SECRET_KEY == test_secret

def test_database_url_from_env(self, monkeypatch):
"""Test DATABASE_URL loads from environment variable."""
test_db_url = "postgresql+asyncpg://user:pass@localhost:5432/insurance_db"
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", test_db_url)
settings = Settings()
assert settings.DATABASE_URL == test_db_url

def test_project_name_override(self, monkeypatch):
"""Test PROJECT_NAME can be overridden by environment variable."""
custom_name = "Custom Insurance Platform"
monkeypatch.setenv("PROJECT_NAME", custom_name)
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.PROJECT_NAME == custom_name

def test_api_v1_str_override(self, monkeypatch):
"""Test API_V1_STR can be overridden by environment variable."""
custom_api_path = "/api/v2"
monkeypatch.setenv("API_V1_STR", custom_api_path)
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.API_V1_STR == custom_api_path

def test_cors_origins_override(self, monkeypatch):
"""Test CORS_ORIGINS can be overridden by environment variable."""
custom_origins = "http://example.com,https://app.example.com"
monkeypatch.setenv("CORS_ORIGINS", custom_origins)
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.CORS_ORIGINS == custom_origins

def test_access_token_expire_minutes_override(self, monkeypatch):
"""Test ACCESS_TOKEN_EXPIRE_MINUTES can be overridden by environment variable."""
custom_expire = 30
monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", str(custom_expire))
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == custom_expire


class TestSettingsBoundaryValues:
"""Test boundary values for Settings fields."""

@pytest.mark.parametrize("expire_minutes", [
0, # minimum/zero
1, # minimum positive
60, # 1 hour
1440, # 1 day
10080, # 7 days (default)
43200, # 30 days
525600, # 1 year
])
def test_access_token_expire_minutes_boundary_values(self, expire_minutes, monkeypatch):
"""Test ACCESS_TOKEN_EXPIRE_MINUTES with various boundary values."""
monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", str(expire_minutes))
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == expire_minutes

def test_empty_secret_key(self, monkeypatch):
"""Test empty SECRET_KEY is accepted (though not recommended)."""
monkeypatch.setenv("SECRET_KEY", "")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.SECRET_KEY == ""

def test_very_long_secret_key(self, monkeypatch):
"""Test very long SECRET_KEY is accepted."""
long_secret = "a" * 1000
monkeypatch.setenv("SECRET_KEY", long_secret)
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.SECRET_KEY == long_secret
assert len(settings.SECRET_KEY) == 1000

def test_secret_key_with_special_characters(self, monkeypatch):
"""Test SECRET_KEY with special characters."""
special_secret = "!@#$%^&*()_+-=[]{}|;:',.<>?/~`"
monkeypatch.setenv("SECRET_KEY", special_secret)
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.SECRET_KEY == special_secret


class TestSettingsEquivalenceClasses:
"""Test equivalence classes for different input types."""

@pytest.mark.parametrize("db_url,expected", [
("postgresql://user:pass@localhost/db", "postgresql://user:pass@localhost/db"),
("postgresql+asyncpg://user:pass@localhost/db", "postgresql+asyncpg://user:pass@localhost/db"),
("sqlite:///./test.db", "sqlite:///./test.db"),
("postgresql://user:pass@host:5432/db", "postgresql://user:pass@host:5432/db"),
])
def test_database_url_equivalence_classes(self, db_url, expected, monkeypatch):
"""Test different valid DATABASE_URL formats."""
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", db_url)
settings = Settings()
assert settings.DATABASE_URL == expected

@pytest.mark.parametrize("api_path", [
"/api/v1",
"/api/v2",
"/v1",
"/api",
"",
"/custom/api/path",
])
def test_api_v1_str_equivalence_classes(self, api_path, monkeypatch):
"""Test different API path formats."""
monkeypatch.setenv("API_V1_STR", api_path)
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
settings = Settings()
assert settings.API_V1_STR == api_path


class TestSettingsInstance:
"""Test the global settings instance."""

def test_settings_instance_exists(self):
"""Test that global settings instance is created."""
from app.core.config import settings
assert settings is not None
assert isinstance(settings, Settings)

def test_settings_instance_is_singleton_like(self):
"""Test that settings instance behaves consistently."""
from app.core.config import settings as settings1
from app.core.config import settings as settings2
assert settings1 is settings2


class TestSettingsModelConfig:
"""Test model configuration behavior."""

def test_extra_fields_ignored(self, monkeypatch):
"""Test that extra fields in environment are ignored due to extra='ignore'."""
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
monkeypatch.setenv("UNKNOWN_FIELD", "should-be-ignored")
monkeypatch.setenv("ANOTHER_UNKNOWN", "also-ignored")
# Should not raise error due to extra="ignore"
settings = Settings()
assert not hasattr(settings, "UNKNOWN_FIELD")
assert not hasattr(settings, "ANOTHER_UNKNOWN")
Loading