diff --git a/app/tests/test_logging.py b/app/tests/test_logging.py new file mode 100644 index 0000000..eb44417 --- /dev/null +++ b/app/tests/test_logging.py @@ -0,0 +1,78 @@ +"""Tests for logging module.""" + +import logging +import os +from unittest.mock import patch + +import pytest + +from app.utils.logging import get_logger, setup_logging + + +class TestSetupLogging: + """Tests for setup_logging function.""" + + def test_setup_logging_returns_logger(self): + """Test that setup_logging returns a logger instance.""" + logger = setup_logging() + assert isinstance(logger, logging.Logger) + + def test_setup_logging_default_level(self): + """Test setup_logging with default INFO level.""" + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("LOG_LEVEL", None) + logger = setup_logging() + assert logger.level == logging.INFO + + def test_setup_logging_debug_level(self): + """Test setup_logging with DEBUG level.""" + with patch.dict(os.environ, {"LOG_LEVEL": "DEBUG"}, clear=False): + logger = setup_logging() + assert logger.level == logging.DEBUG + + def test_setup_logging_warning_level(self): + """Test setup_logging with WARNING level.""" + with patch.dict(os.environ, {"LOG_LEVEL": "WARNING"}, clear=False): + logger = setup_logging() + assert logger.level == logging.WARNING + + def test_setup_logging_invalid_level_defaults_to_info(self): + """Test setup_logging with invalid level defaults to INFO.""" + with patch.dict(os.environ, {"LOG_LEVEL": "INVALID"}, clear=False): + logger = setup_logging() + assert logger.level == logging.INFO + + def test_setup_logging_clears_existing_handlers(self): + """Test that setup_logging clears existing handlers.""" + # Add a dummy handler + root = logging.getLogger() + dummy_handler = logging.StreamHandler() + root.addHandler(dummy_handler) + + # Setup logging should clear it + setup_logging() + + # Verify only one handler exists + assert len(root.handlers) == 1 + + +class TestGetLogger: + """Tests for get_logger function.""" + + def test_get_logger_returns_logger(self): + """Test that get_logger returns a logger instance.""" + logger = get_logger("test_module") + assert isinstance(logger, logging.Logger) + assert logger.name == "test_module" + + def test_get_logger_different_names(self): + """Test that get_logger returns different loggers for different names.""" + logger1 = get_logger("module1") + logger2 = get_logger("module2") + assert logger1.name != logger2.name + + def test_get_logger_same_name_returns_same_logger(self): + """Test that get_logger returns same logger for same name.""" + logger1 = get_logger("same_module") + logger2 = get_logger("same_module") + assert logger1 is logger2 diff --git a/app/tests/test_security.py b/app/tests/test_security.py new file mode 100644 index 0000000..5598ce3 --- /dev/null +++ b/app/tests/test_security.py @@ -0,0 +1,140 @@ +"""Tests for security module.""" + +import os +from unittest.mock import patch + +import pytest + +from app.security import ( + configure_production_logging, + generate_secure_keys, + security_health_check, + validate_production_config, +) + + +class TestConfigureProductionLogging: + """Tests for configure_production_logging function.""" + + def test_configure_logging_default_level(self): + """Test logging configuration with default INFO level.""" + with patch.dict(os.environ, {}, clear=False): + configure_production_logging() + + def test_configure_logging_debug_level(self): + """Test logging configuration with DEBUG level.""" + with patch.dict(os.environ, {"LOG_LEVEL": "DEBUG"}, clear=False): + configure_production_logging() + + def test_configure_logging_production_environment(self): + """Test logging configuration in production environment.""" + with patch.dict(os.environ, {"ENVIRONMENT": "production"}, clear=False): + configure_production_logging() + + +class TestGenerateSecureKeys: + """Tests for generate_secure_keys function.""" + + def test_generate_keys_returns_dict(self): + """Test that generate_secure_keys returns a dictionary.""" + keys = generate_secure_keys() + assert isinstance(keys, dict) + assert "api_key" in keys + assert "secret_key" in keys + + def test_generate_keys_are_unique(self): + """Test that each call generates unique keys.""" + keys1 = generate_secure_keys() + keys2 = generate_secure_keys() + assert keys1["api_key"] != keys2["api_key"] + assert keys1["secret_key"] != keys2["secret_key"] + + def test_generate_keys_sufficient_length(self): + """Test that generated keys have sufficient length.""" + keys = generate_secure_keys() + assert len(keys["api_key"]) >= 32 + assert len(keys["secret_key"]) >= 32 + + +class TestValidateProductionConfig: + """Tests for validate_production_config function.""" + + def test_valid_config(self): + """Test validation with valid configuration.""" + env = { + "API_KEY": "test-api-key-12345678901234567890", + "SECRET_KEY": "test-secret-key-12345678901234567890", + "CORS_ORIGINS": "https://example.com", + "ENVIRONMENT": "production", + } + with patch.dict(os.environ, env, clear=False): + result = validate_production_config() + assert result["valid"] is True + assert len(result["errors"]) == 0 + + def test_missing_api_key(self): + """Test validation with missing API_KEY.""" + env = {"SECRET_KEY": "test-secret", "API_KEY": ""} + with patch.dict(os.environ, env, clear=False): + os.environ.pop("API_KEY", None) + result = validate_production_config() + assert any("API_KEY" in e for e in result["errors"]) + + def test_short_api_key_warning(self): + """Test warning for short API key.""" + env = { + "API_KEY": "short", + "SECRET_KEY": "test-secret", + "CORS_ORIGINS": "https://example.com", + } + with patch.dict(os.environ, env, clear=False): + result = validate_production_config() + assert any("16 characters" in w for w in result["warnings"]) + + def test_wildcard_cors_error(self): + """Test error for wildcard CORS origins.""" + env = { + "API_KEY": "test-api-key-12345678901234567890", + "SECRET_KEY": "test-secret", + "CORS_ORIGINS": "*", + } + with patch.dict(os.environ, env, clear=False): + result = validate_production_config() + assert any("wildcard" in e for e in result["errors"]) + + def test_non_production_environment_warning(self): + """Test warning for non-production environment.""" + env = { + "API_KEY": "test-api-key-12345678901234567890", + "SECRET_KEY": "test-secret", + "CORS_ORIGINS": "https://example.com", + "ENVIRONMENT": "development", + } + with patch.dict(os.environ, env, clear=False): + result = validate_production_config() + assert any("production-ready" in w for w in result["warnings"]) + + +class TestSecurityHealthCheck: + """Tests for security_health_check function.""" + + def test_healthy_status(self): + """Test health check with valid configuration.""" + env = { + "API_KEY": "test-api-key-12345678901234567890", + "SECRET_KEY": "test-secret-key-12345678901234567890", + "CORS_ORIGINS": "https://example.com", + "ENVIRONMENT": "production", + } + with patch.dict(os.environ, env, clear=False): + result = security_health_check() + assert result["security_status"] == "healthy" + + def test_unhealthy_status(self): + """Test health check with invalid configuration.""" + env = {"CORS_ORIGINS": "*", "API_KEY": "", "SECRET_KEY": ""} + with patch.dict(os.environ, env, clear=False): + os.environ.pop("API_KEY", None) + os.environ.pop("SECRET_KEY", None) + result = security_health_check() + assert result["security_status"] == "unhealthy" diff --git a/app/tests/test_telemetry.py b/app/tests/test_telemetry.py new file mode 100644 index 0000000..f6d6e0d --- /dev/null +++ b/app/tests/test_telemetry.py @@ -0,0 +1,69 @@ +"""Tests for telemetry module.""" + +import logging +from unittest.mock import MagicMock, patch + +import pytest +from fastapi import FastAPI + +from app.telemetry import log_request_metrics, setup_telemetry + + +class TestSetupTelemetry: + """Tests for setup_telemetry function.""" + + def test_setup_telemetry_adds_events(self): + """Test that setup_telemetry configures app events.""" + app = FastAPI(title="Test App", version="1.0.0") + setup_telemetry(app) + # Verify startup and shutdown events were added + assert len(app.router.on_startup) > 0 + assert len(app.router.on_shutdown) > 0 + + def test_setup_telemetry_logs_info(self, caplog): + """Test that setup_telemetry logs setup messages.""" + app = FastAPI(title="Test App", version="1.0.0") + with caplog.at_level(logging.INFO): + setup_telemetry(app) + assert "Setting up telemetry" in caplog.text + assert "Telemetry setup complete" in caplog.text + + +class TestLogRequestMetrics: + """Tests for log_request_metrics function.""" + + def test_log_request_metrics_basic(self, caplog): + """Test basic request metrics logging.""" + with caplog.at_level(logging.INFO): + log_request_metrics( + endpoint="/api/test", + method="GET", + status_code=200, + duration_ms=15.5, + ) + assert "GET" in caplog.text + assert "/api/test" in caplog.text + + def test_log_request_metrics_with_request_id(self, caplog): + """Test request metrics logging with request ID.""" + with caplog.at_level(logging.INFO): + log_request_metrics( + endpoint="/api/users", + method="POST", + status_code=201, + duration_ms=25.3, + request_id="req-123-abc", + ) + assert "POST" in caplog.text + assert "/api/users" in caplog.text + + def test_log_request_metrics_error_status(self, caplog): + """Test request metrics logging with error status code.""" + with caplog.at_level(logging.INFO): + log_request_metrics( + endpoint="/api/error", + method="GET", + status_code=500, + duration_ms=100.0, + ) + assert "500" in caplog.text or "GET" in caplog.text