From b1252021f2f212c622c781170a05b499ed583201 Mon Sep 17 00:00:00 2001 From: TestKraft Bot Date: Mon, 30 Mar 2026 13:27:12 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20Add=20comprehensive=20unit=20tes?= =?UTF-8?q?ts=20via=20TestKraft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive unit tests for backend/app/core/config.py - Cover all configuration settings and defaults - Test environment variable loading and validation - Include boundary value analysis and edge cases - Test required field validation (SECRET_KEY, DATABASE_URL) - Parametrized tests for equivalence class partitioning Generated by TestKraft - AI-Powered Test Generation --- backend/tests/__init__.py | 3 + backend/tests/test_config.py | 478 +++++++++++++++++++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/test_config.py diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..105525a --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests package for Insurance Management Platform backend. +""" diff --git a/backend/tests/test_config.py b/backend/tests/test_config.py new file mode 100644 index 0000000..f70644d --- /dev/null +++ b/backend/tests/test_config.py @@ -0,0 +1,478 @@ +""" +Comprehensive unit tests for app.core.config module. + +Tests cover: +- Default values and configuration +- Environment variable loading +- Required field validation +- Boundary value analysis +- Edge cases and error conditions +""" + +import os +import pytest +from pydantic import ValidationError +from app.core.config import Settings + + +class TestSettingsDefaults: + """Test default values for Settings class.""" + + def test_project_name_default(self, monkeypatch): + """Test default PROJECT_NAME value.""" + monkeypatch.setenv("SECRET_KEY", "test-secret-key") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + assert settings.PROJECT_NAME == "Insurance Management Platform" + + def test_api_v1_str_default(self, monkeypatch): + """Test default API_V1_STR value.""" + monkeypatch.setenv("SECRET_KEY", "test-secret-key") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + assert settings.API_V1_STR == "/api/v1" + + def test_access_token_expire_minutes_default(self, monkeypatch): + """Test default ACCESS_TOKEN_EXPIRE_MINUTES value (7 days).""" + monkeypatch.setenv("SECRET_KEY", "test-secret-key") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + expected_minutes = 60 * 24 * 7 # 7 days + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == expected_minutes + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 10080 + + def test_cors_origins_default(self, monkeypatch): + """Test default CORS_ORIGINS value.""" + monkeypatch.setenv("SECRET_KEY", "test-secret-key") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + assert settings.CORS_ORIGINS == "http://localhost:3000" + + +class TestSettingsRequiredFields: + """Test required field validation.""" + + def test_missing_secret_key_raises_validation_error(self, monkeypatch): + """Test that missing SECRET_KEY raises ValidationError.""" + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.delenv("SECRET_KEY", raising=False) + + with pytest.raises(ValidationError) as exc_info: + Settings() + + errors = exc_info.value.errors() + assert any(error["loc"] == ("SECRET_KEY",) for error in errors) + assert any(error["type"] == "missing" for error in errors) + + def test_missing_database_url_raises_validation_error(self, monkeypatch): + """Test that missing DATABASE_URL raises ValidationError.""" + 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) + assert any(error["type"] == "missing" for error in errors) + + def test_both_required_fields_missing_raises_validation_error(self, monkeypatch): + """Test that missing both required fields raises ValidationError.""" + monkeypatch.delenv("SECRET_KEY", raising=False) + monkeypatch.delenv("DATABASE_URL", raising=False) + + with pytest.raises(ValidationError) as exc_info: + Settings() + + errors = exc_info.value.errors() + assert len(errors) >= 2 + error_locs = [error["loc"][0] for error in errors] + assert "SECRET_KEY" in error_locs + assert "DATABASE_URL" in error_locs + + +class TestSettingsEnvironmentVariables: + """Test loading configuration from environment variables.""" + + def test_load_secret_key_from_env(self, monkeypatch): + """Test loading SECRET_KEY from environment variable.""" + test_secret = "super-secret-key-12345" + monkeypatch.setenv("SECRET_KEY", test_secret) + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + assert settings.SECRET_KEY == test_secret + + def test_load_database_url_from_env(self, monkeypatch): + """Test loading DATABASE_URL from environment variable.""" + test_db_url = "postgresql://user:pass@localhost:5432/testdb" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", test_db_url) + settings = Settings() + assert settings.DATABASE_URL == test_db_url + + def test_override_project_name_from_env(self, monkeypatch): + """Test overriding PROJECT_NAME from environment variable.""" + custom_name = "Custom Insurance Platform" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("PROJECT_NAME", custom_name) + settings = Settings() + assert settings.PROJECT_NAME == custom_name + + def test_override_api_v1_str_from_env(self, monkeypatch): + """Test overriding API_V1_STR from environment variable.""" + custom_api_path = "/api/v2" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("API_V1_STR", custom_api_path) + settings = Settings() + assert settings.API_V1_STR == custom_api_path + + def test_override_access_token_expire_minutes_from_env(self, monkeypatch): + """Test overriding ACCESS_TOKEN_EXPIRE_MINUTES from environment variable.""" + custom_minutes = 1440 # 1 day + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", str(custom_minutes)) + settings = Settings() + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == custom_minutes + + def test_override_cors_origins_from_env(self, monkeypatch): + """Test overriding CORS_ORIGINS from environment variable.""" + custom_origins = "http://example.com,https://example.com" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("CORS_ORIGINS", custom_origins) + settings = Settings() + assert settings.CORS_ORIGINS == custom_origins + + +class TestSettingsBoundaryValues: + """Test boundary value analysis for numeric and string fields.""" + + def test_access_token_expire_minutes_zero(self, monkeypatch): + """Test ACCESS_TOKEN_EXPIRE_MINUTES with zero value.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", "0") + settings = Settings() + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 0 + + def test_access_token_expire_minutes_negative(self, monkeypatch): + """Test ACCESS_TOKEN_EXPIRE_MINUTES with negative value.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", "-1") + settings = Settings() + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == -1 + + def test_access_token_expire_minutes_one(self, monkeypatch): + """Test ACCESS_TOKEN_EXPIRE_MINUTES with minimum positive value.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", "1") + settings = Settings() + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 1 + + def test_access_token_expire_minutes_very_large(self, monkeypatch): + """Test ACCESS_TOKEN_EXPIRE_MINUTES with very large value.""" + large_value = 525600 # 1 year in minutes + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", str(large_value)) + settings = Settings() + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == large_value + + def test_secret_key_empty_string(self, monkeypatch): + """Test SECRET_KEY with empty string.""" + monkeypatch.setenv("SECRET_KEY", "") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + + with pytest.raises(ValidationError): + Settings() + + def test_database_url_empty_string(self, monkeypatch): + """Test DATABASE_URL with empty string.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "") + + with pytest.raises(ValidationError): + Settings() + + def test_secret_key_single_character(self, monkeypatch): + """Test SECRET_KEY with single character.""" + monkeypatch.setenv("SECRET_KEY", "a") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + assert settings.SECRET_KEY == "a" + + def test_secret_key_very_long(self, monkeypatch): + """Test SECRET_KEY with very long string.""" + long_key = "a" * 1000 + monkeypatch.setenv("SECRET_KEY", long_key) + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + assert settings.SECRET_KEY == long_key + assert len(settings.SECRET_KEY) == 1000 + + def test_project_name_empty_string(self, monkeypatch): + """Test PROJECT_NAME with empty string override.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("PROJECT_NAME", "") + settings = Settings() + assert settings.PROJECT_NAME == "" + + def test_cors_origins_empty_string(self, monkeypatch): + """Test CORS_ORIGINS with empty string.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("CORS_ORIGINS", "") + settings = Settings() + assert settings.CORS_ORIGINS == "" + + +class TestSettingsSpecialCharacters: + """Test handling of special characters in string fields.""" + + def test_secret_key_with_special_characters(self, monkeypatch): + """Test SECRET_KEY with special characters.""" + special_key = "!@#$%^&*()_+-=[]{}|;:',.<>?/~`" + monkeypatch.setenv("SECRET_KEY", special_key) + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + assert settings.SECRET_KEY == special_key + + def test_database_url_with_special_characters(self, monkeypatch): + """Test DATABASE_URL with special characters in password.""" + db_url = "postgresql://user:p@ss!word#123@localhost:5432/db" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", db_url) + settings = Settings() + assert settings.DATABASE_URL == db_url + + def test_project_name_with_unicode(self, monkeypatch): + """Test PROJECT_NAME with unicode characters.""" + unicode_name = "Insurance 保险 Platform" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("PROJECT_NAME", unicode_name) + settings = Settings() + assert settings.PROJECT_NAME == unicode_name + + def test_cors_origins_multiple_urls(self, monkeypatch): + """Test CORS_ORIGINS with multiple comma-separated URLs.""" + origins = "http://localhost:3000,https://example.com,http://192.168.1.1:8080" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("CORS_ORIGINS", origins) + settings = Settings() + assert settings.CORS_ORIGINS == origins + + def test_api_v1_str_with_slash_prefix(self, monkeypatch): + """Test API_V1_STR with leading slash.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("API_V1_STR", "/api/v1") + settings = Settings() + assert settings.API_V1_STR.startswith("/") + + def test_api_v1_str_without_slash_prefix(self, monkeypatch): + """Test API_V1_STR without leading slash.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("API_V1_STR", "api/v1") + settings = Settings() + assert settings.API_V1_STR == "api/v1" + + +class TestSettingsEquivalenceClasses: + """Test equivalence class partitioning for different input types.""" + + @pytest.mark.parametrize("valid_db_url", [ + "postgresql://localhost/db", + "postgresql://user@localhost/db", + "postgresql://user:pass@localhost/db", + "postgresql://user:pass@localhost:5432/db", + "postgresql+asyncpg://user:pass@localhost:5432/db", + "sqlite:///./test.db", + ]) + def test_database_url_valid_formats(self, valid_db_url, monkeypatch): + """Test DATABASE_URL with various valid formats.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", valid_db_url) + settings = Settings() + assert settings.DATABASE_URL == valid_db_url + + @pytest.mark.parametrize("valid_minutes", [1, 60, 1440, 10080, 525600]) + def test_access_token_expire_minutes_valid_values(self, valid_minutes, monkeypatch): + """Test ACCESS_TOKEN_EXPIRE_MINUTES with various valid values.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", str(valid_minutes)) + settings = Settings() + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == valid_minutes + + @pytest.mark.parametrize("valid_cors", [ + "http://localhost:3000", + "https://example.com", + "http://localhost:3000,https://example.com", + "*", + "http://192.168.1.1:8080", + ]) + def test_cors_origins_valid_formats(self, valid_cors, monkeypatch): + """Test CORS_ORIGINS with various valid formats.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("CORS_ORIGINS", valid_cors) + settings = Settings() + assert settings.CORS_ORIGINS == valid_cors + + @pytest.mark.parametrize("valid_api_path", [ + "/api/v1", + "/api/v2", + "/v1", + "/", + "", + "api", + ]) + def test_api_v1_str_valid_formats(self, valid_api_path, monkeypatch): + """Test API_V1_STR with various valid formats.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("API_V1_STR", valid_api_path) + settings = Settings() + assert settings.API_V1_STR == valid_api_path + + +class TestSettingsModelConfig: + """Test Settings model configuration.""" + + def test_extra_fields_ignored(self, monkeypatch): + """Test that extra environment variables are ignored.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("EXTRA_FIELD_NOT_DEFINED", "should-be-ignored") + monkeypatch.setenv("RANDOM_VAR", "also-ignored") + + # Should not raise an error + settings = Settings() + assert not hasattr(settings, "EXTRA_FIELD_NOT_DEFINED") + assert not hasattr(settings, "RANDOM_VAR") + + def test_settings_immutable_after_creation(self, monkeypatch): + """Test that Settings instance is immutable after creation.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + + # Pydantic v2 allows assignment but validates it + # Test that we can read the values + assert settings.PROJECT_NAME == "Insurance Management Platform" + assert settings.SECRET_KEY == "test-secret" + + def test_model_config_env_file_setting(self, monkeypatch): + """Test that model_config specifies .env file.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + settings = Settings() + + # Check that model_config is set correctly + assert hasattr(Settings, "model_config") + assert Settings.model_config.get("env_file") == ".env" + assert Settings.model_config.get("extra") == "ignore" + + +class TestSettingsInvalidInputs: + """Test error handling for invalid inputs.""" + + def test_access_token_expire_minutes_invalid_string(self, monkeypatch): + """Test ACCESS_TOKEN_EXPIRE_MINUTES with non-numeric string.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", "invalid") + + with pytest.raises(ValidationError) as exc_info: + Settings() + + errors = exc_info.value.errors() + assert any("ACCESS_TOKEN_EXPIRE_MINUTES" in str(error["loc"]) for error in errors) + + def test_access_token_expire_minutes_float_value(self, monkeypatch): + """Test ACCESS_TOKEN_EXPIRE_MINUTES with float value.""" + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://test") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60.5") + + # Pydantic should convert float to int + settings = Settings() + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 60 + + +class TestSettingsIntegration: + """Integration tests for Settings with realistic scenarios.""" + + def test_production_like_configuration(self, monkeypatch): + """Test with production-like configuration.""" + monkeypatch.setenv("SECRET_KEY", "prod-secret-key-very-long-and-secure-12345") + monkeypatch.setenv("DATABASE_URL", "postgresql://prod_user:prod_pass@db.example.com:5432/insurance_prod") + monkeypatch.setenv("PROJECT_NAME", "Production Insurance Platform") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", "43200") # 30 days + monkeypatch.setenv("CORS_ORIGINS", "https://insurance.example.com,https://www.insurance.example.com") + + settings = Settings() + + assert settings.SECRET_KEY == "prod-secret-key-very-long-and-secure-12345" + assert "prod_user" in settings.DATABASE_URL + assert settings.PROJECT_NAME == "Production Insurance Platform" + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 43200 + assert "https://insurance.example.com" in settings.CORS_ORIGINS + + def test_development_configuration(self, monkeypatch): + """Test with development configuration.""" + monkeypatch.setenv("SECRET_KEY", "dev-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/insurance_dev") + monkeypatch.setenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:3001") + + settings = Settings() + + assert settings.SECRET_KEY == "dev-secret" + assert "localhost" in settings.DATABASE_URL + assert settings.CORS_ORIGINS == "http://localhost:3000,http://localhost:3001" + assert settings.PROJECT_NAME == "Insurance Management Platform" # Uses default + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 10080 # Uses default + + def test_minimal_required_configuration(self, monkeypatch): + """Test with only required fields set.""" + monkeypatch.setenv("SECRET_KEY", "minimal-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/db") + + settings = Settings() + + # Required fields are set + assert settings.SECRET_KEY == "minimal-secret" + assert settings.DATABASE_URL == "postgresql://localhost/db" + + # Defaults are used for optional fields + assert settings.PROJECT_NAME == "Insurance Management Platform" + assert settings.API_V1_STR == "/api/v1" + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 10080 + assert settings.CORS_ORIGINS == "http://localhost:3000" + + def test_all_fields_overridden(self, monkeypatch): + """Test with all fields overridden from environment.""" + monkeypatch.setenv("SECRET_KEY", "custom-secret") + monkeypatch.setenv("DATABASE_URL", "postgresql://custom/db") + monkeypatch.setenv("PROJECT_NAME", "Custom Project") + monkeypatch.setenv("API_V1_STR", "/custom/api") + monkeypatch.setenv("ACCESS_TOKEN_EXPIRE_MINUTES", "9999") + monkeypatch.setenv("CORS_ORIGINS", "https://custom.com") + + settings = Settings() + + assert settings.SECRET_KEY == "custom-secret" + assert settings.DATABASE_URL == "postgresql://custom/db" + assert settings.PROJECT_NAME == "Custom Project" + assert settings.API_V1_STR == "/custom/api" + assert settings.ACCESS_TOKEN_EXPIRE_MINUTES == 9999 + assert settings.CORS_ORIGINS == "https://custom.com"