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)
Comment on lines +22 to +23
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.

This test may not reliably verify the default INFO level behavior. The conftest.py file sets LOG_LEVEL to "INFO" by default, so even after popping it from os.environ within the patch.dict context, the test may still be affected. Additionally, patch.dict with clear=False and an empty dict doesn't guarantee LOG_LEVEL is unset. Use clear=True or explicitly verify that LOG_LEVEL is not in os.environ after the pop.

Suggested change
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("LOG_LEVEL", None)
with patch.dict(os.environ, {}, clear=True):

Copilot uses AI. Check for mistakes.
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 +56
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.

This test may not reliably verify handler cleanup behavior. After calling setup_logging(), the test asserts exactly 1 handler exists, but this doesn't necessarily prove that the dummy_handler was removed - it could be that setup_logging() removes all handlers and adds exactly 1 new one, or it could keep the dummy handler and not add a new one. The test should verify the handler identity or type to ensure proper cleanup occurred.

Suggested change
# Verify only one handler exists
assert len(root.handlers) == 1
# Verify only one handler exists and it's not the dummy handler
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 may not properly validate missing API_KEY behavior due to conftest.py setting default test environment variables. Even after popping API_KEY from os.environ, the patch.dict context only patches with the provided env dict and doesn't guarantee that API_KEY remains unset. The test should use clear=True in patch.dict to ensure complete isolation from conftest.py's default environment variables.

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=True):

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

This test has potential issues with test isolation. The test first sets "API_KEY" to an empty string in the patched environment, then immediately pops it. This two-step approach is confusing and could lead to flaky tests. The test should either use clear=True in patch.dict or only use pop without setting the empty string first.

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=True):

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"])

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 API_KEY test, this test has confusing environment manipulation. Setting environment variables to empty strings and then immediately popping them creates unnecessary complexity. Use a cleaner approach with clear=True in patch.dict or only use the pop operation.

Copilot uses AI. Check for mistakes.
Comment on lines +136 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 test_missing_api_key, this test may not work correctly due to conftest.py setting default environment variables. The patch.dict with clear=False means the test environment from conftest.py may still interfere. Consider using clear=True to ensure the test properly validates the unhealthy status when API_KEY and SECRET_KEY are truly missing.

Suggested change
with patch.dict(os.environ, env, clear=False):
os.environ.pop("API_KEY", None)
os.environ.pop("SECRET_KEY", None)
with patch.dict(os.environ, env, clear=True):

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.

Import of 'MagicMock' is not used.
Import of 'patch' is not used.

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

Comment on lines +35 to +46
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 assertions for this test are too weak. The test should verify that the status_code (200) and duration_ms (15.5) are also present in the logged output to ensure all parameters are being logged correctly. Currently, only the method and endpoint are checked.

Copilot uses AI. Check for mistakes.
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
Comment on lines +47 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.

This test doesn't actually verify that the request_id is included in the logged output. The test should assert that "req-123-abc" appears in the captured log text to properly validate that the request_id parameter is being logged correctly.

Copilot uses AI. Check for mistakes.

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 here is too weak and doesn't verify the actual logged message. The test should verify that both the status code "500" AND the expected log information are present in the log output. The current assertion with "or" will pass even if only "GET" is found, which doesn't properly validate error logging behavior.

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

Copilot uses AI. Check for mistakes.