Skip to content
Merged
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
78 changes: 78 additions & 0 deletions app/tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Tests for logging module."""

import logging
import os
from unittest.mock import patch

import pytest
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'pytest' is not used.

Suggested change
import pytest

Copilot uses AI. Check for mistakes.

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


Comment on lines +55 to +58
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test_setup_logging_clears_existing_handlers test has a flaw: it adds a dummy handler but then only checks that exactly one handler exists after setup_logging is called. This doesn't actually verify that the dummy handler was cleared - it only verifies the final count. The test would pass even if setup_logging never cleared handlers as long as it only added one handler. A more robust test would verify the handler identity or check that the dummy handler is no longer present.

Suggested change
# Verify only one handler exists
assert len(root.handlers) == 1
# Verify only one handler exists and dummy_handler is not present
assert len(root.handlers) == 1
assert dummy_handler not in root.handlers

Copilot uses AI. Check for mistakes.
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
140 changes: 140 additions & 0 deletions app/tests/test_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Tests for security module."""

import os
from unittest.mock import patch

import pytest
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'pytest' is not used.

Suggested change
import pytest

Copilot uses AI. Check for mistakes.

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)
Comment on lines +77 to +79
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test patches the environment with a SECRET_KEY and API_KEY but then immediately removes API_KEY with os.environ.pop(). This creates confusion about the test's intent. The environment should either be set up correctly from the start (without API_KEY) or the patch.dict should not include it. The current approach unnecessarily patches then removes the value.

Suggested change
env = {"SECRET_KEY": "test-secret", "API_KEY": ""}
with patch.dict(os.environ, env, clear=False):
os.environ.pop("API_KEY", None)
env = {"SECRET_KEY": "test-secret"}
with patch.dict(os.environ, env, clear=False):

Copilot uses AI. Check for mistakes.
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"])
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion message checks for "16 characters" in the warning, but according to the security.py source code (line 47), the actual warning message says "at least 16 characters long". The test should check for the complete or more specific phrase to ensure it matches the actual warning message exactly.

Suggested change
assert any("16 characters" in w for w in result["warnings"])
assert any("at least 16 characters long" in w for w in result["warnings"])

Copilot uses AI. Check for mistakes.

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)
Comment on lines +135 to +138
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the test_missing_api_key test, this test patches environment variables but then immediately removes them with os.environ.pop(). This is unnecessarily complex. The environment variables should simply not be included in the patch.dict in the first place if the goal is to test their absence.

Copilot uses AI. Check for mistakes.
result = security_health_check()
assert result["security_status"] == "unhealthy"
69 changes: 69 additions & 0 deletions app/tests/test_telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for telemetry module."""

import logging
from unittest.mock import MagicMock, patch
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MagicMock and patch imports from unittest.mock are not used anywhere in this test file. These unused imports should be removed to keep the code clean.

Suggested change
from unittest.mock import MagicMock, patch

Copilot uses AI. Check for mistakes.

import pytest
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'pytest' is not used.

Suggested change
import pytest

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion on line 69 uses an OR condition that will almost always pass. Since the test is checking for an error status code (500), the assertion should verify that "500" appears in the log output, not fall back to checking for "GET" which would be present in all GET requests. This makes the test less effective at catching issues with error status code logging.

Suggested change
assert "500" in caplog.text or "GET" in caplog.text
assert "500" in caplog.text

Copilot uses AI. Check for mistakes.