Skip to content

Commit 24c3eb7

Browse files
authored
test: añadir tests para security, telemetry y logging (#112)
- test_security.py: 14 tests para security.py (0% -> 100%) - test_telemetry.py: 5 tests para telemetry.py (0% -> 81%) - test_logging.py: 8 tests para logging.py (32% -> 100%) - Cobertura total: 72% -> 96% - 34 tests passing
1 parent afecacc commit 24c3eb7

File tree

3 files changed

+287
-0
lines changed

3 files changed

+287
-0
lines changed

app/tests/test_logging.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Tests for logging module."""
2+
3+
import logging
4+
import os
5+
from unittest.mock import patch
6+
7+
import pytest
8+
9+
from app.utils.logging import get_logger, setup_logging
10+
11+
12+
class TestSetupLogging:
13+
"""Tests for setup_logging function."""
14+
15+
def test_setup_logging_returns_logger(self):
16+
"""Test that setup_logging returns a logger instance."""
17+
logger = setup_logging()
18+
assert isinstance(logger, logging.Logger)
19+
20+
def test_setup_logging_default_level(self):
21+
"""Test setup_logging with default INFO level."""
22+
with patch.dict(os.environ, {}, clear=False):
23+
os.environ.pop("LOG_LEVEL", None)
24+
logger = setup_logging()
25+
assert logger.level == logging.INFO
26+
27+
def test_setup_logging_debug_level(self):
28+
"""Test setup_logging with DEBUG level."""
29+
with patch.dict(os.environ, {"LOG_LEVEL": "DEBUG"}, clear=False):
30+
logger = setup_logging()
31+
assert logger.level == logging.DEBUG
32+
33+
def test_setup_logging_warning_level(self):
34+
"""Test setup_logging with WARNING level."""
35+
with patch.dict(os.environ, {"LOG_LEVEL": "WARNING"}, clear=False):
36+
logger = setup_logging()
37+
assert logger.level == logging.WARNING
38+
39+
def test_setup_logging_invalid_level_defaults_to_info(self):
40+
"""Test setup_logging with invalid level defaults to INFO."""
41+
with patch.dict(os.environ, {"LOG_LEVEL": "INVALID"}, clear=False):
42+
logger = setup_logging()
43+
assert logger.level == logging.INFO
44+
45+
def test_setup_logging_clears_existing_handlers(self):
46+
"""Test that setup_logging clears existing handlers."""
47+
# Add a dummy handler
48+
root = logging.getLogger()
49+
dummy_handler = logging.StreamHandler()
50+
root.addHandler(dummy_handler)
51+
52+
# Setup logging should clear it
53+
setup_logging()
54+
55+
# Verify only one handler exists
56+
assert len(root.handlers) == 1
57+
58+
59+
class TestGetLogger:
60+
"""Tests for get_logger function."""
61+
62+
def test_get_logger_returns_logger(self):
63+
"""Test that get_logger returns a logger instance."""
64+
logger = get_logger("test_module")
65+
assert isinstance(logger, logging.Logger)
66+
assert logger.name == "test_module"
67+
68+
def test_get_logger_different_names(self):
69+
"""Test that get_logger returns different loggers for different names."""
70+
logger1 = get_logger("module1")
71+
logger2 = get_logger("module2")
72+
assert logger1.name != logger2.name
73+
74+
def test_get_logger_same_name_returns_same_logger(self):
75+
"""Test that get_logger returns same logger for same name."""
76+
logger1 = get_logger("same_module")
77+
logger2 = get_logger("same_module")
78+
assert logger1 is logger2

app/tests/test_security.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Tests for security module."""
2+
3+
import os
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
from app.security import (
9+
configure_production_logging,
10+
generate_secure_keys,
11+
security_health_check,
12+
validate_production_config,
13+
)
14+
15+
16+
class TestConfigureProductionLogging:
17+
"""Tests for configure_production_logging function."""
18+
19+
def test_configure_logging_default_level(self):
20+
"""Test logging configuration with default INFO level."""
21+
with patch.dict(os.environ, {}, clear=False):
22+
configure_production_logging()
23+
24+
def test_configure_logging_debug_level(self):
25+
"""Test logging configuration with DEBUG level."""
26+
with patch.dict(os.environ, {"LOG_LEVEL": "DEBUG"}, clear=False):
27+
configure_production_logging()
28+
29+
def test_configure_logging_production_environment(self):
30+
"""Test logging configuration in production environment."""
31+
with patch.dict(os.environ, {"ENVIRONMENT": "production"}, clear=False):
32+
configure_production_logging()
33+
34+
35+
class TestGenerateSecureKeys:
36+
"""Tests for generate_secure_keys function."""
37+
38+
def test_generate_keys_returns_dict(self):
39+
"""Test that generate_secure_keys returns a dictionary."""
40+
keys = generate_secure_keys()
41+
assert isinstance(keys, dict)
42+
assert "api_key" in keys
43+
assert "secret_key" in keys
44+
45+
def test_generate_keys_are_unique(self):
46+
"""Test that each call generates unique keys."""
47+
keys1 = generate_secure_keys()
48+
keys2 = generate_secure_keys()
49+
assert keys1["api_key"] != keys2["api_key"]
50+
assert keys1["secret_key"] != keys2["secret_key"]
51+
52+
def test_generate_keys_sufficient_length(self):
53+
"""Test that generated keys have sufficient length."""
54+
keys = generate_secure_keys()
55+
assert len(keys["api_key"]) >= 32
56+
assert len(keys["secret_key"]) >= 32
57+
58+
59+
class TestValidateProductionConfig:
60+
"""Tests for validate_production_config function."""
61+
62+
def test_valid_config(self):
63+
"""Test validation with valid configuration."""
64+
env = {
65+
"API_KEY": "test-api-key-12345678901234567890",
66+
"SECRET_KEY": "test-secret-key-12345678901234567890",
67+
"CORS_ORIGINS": "https://example.com",
68+
"ENVIRONMENT": "production",
69+
}
70+
with patch.dict(os.environ, env, clear=False):
71+
result = validate_production_config()
72+
assert result["valid"] is True
73+
assert len(result["errors"]) == 0
74+
75+
def test_missing_api_key(self):
76+
"""Test validation with missing API_KEY."""
77+
env = {"SECRET_KEY": "test-secret", "API_KEY": ""}
78+
with patch.dict(os.environ, env, clear=False):
79+
os.environ.pop("API_KEY", None)
80+
result = validate_production_config()
81+
assert any("API_KEY" in e for e in result["errors"])
82+
83+
def test_short_api_key_warning(self):
84+
"""Test warning for short API key."""
85+
env = {
86+
"API_KEY": "short",
87+
"SECRET_KEY": "test-secret",
88+
"CORS_ORIGINS": "https://example.com",
89+
}
90+
with patch.dict(os.environ, env, clear=False):
91+
result = validate_production_config()
92+
assert any("16 characters" in w for w in result["warnings"])
93+
94+
def test_wildcard_cors_error(self):
95+
"""Test error for wildcard CORS origins."""
96+
env = {
97+
"API_KEY": "test-api-key-12345678901234567890",
98+
"SECRET_KEY": "test-secret",
99+
"CORS_ORIGINS": "*",
100+
}
101+
with patch.dict(os.environ, env, clear=False):
102+
result = validate_production_config()
103+
assert any("wildcard" in e for e in result["errors"])
104+
105+
def test_non_production_environment_warning(self):
106+
"""Test warning for non-production environment."""
107+
env = {
108+
"API_KEY": "test-api-key-12345678901234567890",
109+
"SECRET_KEY": "test-secret",
110+
"CORS_ORIGINS": "https://example.com",
111+
"ENVIRONMENT": "development",
112+
}
113+
with patch.dict(os.environ, env, clear=False):
114+
result = validate_production_config()
115+
assert any("production-ready" in w for w in result["warnings"])
116+
117+
118+
class TestSecurityHealthCheck:
119+
"""Tests for security_health_check function."""
120+
121+
def test_healthy_status(self):
122+
"""Test health check with valid configuration."""
123+
env = {
124+
"API_KEY": "test-api-key-12345678901234567890",
125+
"SECRET_KEY": "test-secret-key-12345678901234567890",
126+
"CORS_ORIGINS": "https://example.com",
127+
"ENVIRONMENT": "production",
128+
}
129+
with patch.dict(os.environ, env, clear=False):
130+
result = security_health_check()
131+
assert result["security_status"] == "healthy"
132+
133+
def test_unhealthy_status(self):
134+
"""Test health check with invalid configuration."""
135+
env = {"CORS_ORIGINS": "*", "API_KEY": "", "SECRET_KEY": ""}
136+
with patch.dict(os.environ, env, clear=False):
137+
os.environ.pop("API_KEY", None)
138+
os.environ.pop("SECRET_KEY", None)
139+
result = security_health_check()
140+
assert result["security_status"] == "unhealthy"

app/tests/test_telemetry.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Tests for telemetry module."""
2+
3+
import logging
4+
from unittest.mock import MagicMock, patch
5+
6+
import pytest
7+
from fastapi import FastAPI
8+
9+
from app.telemetry import log_request_metrics, setup_telemetry
10+
11+
12+
class TestSetupTelemetry:
13+
"""Tests for setup_telemetry function."""
14+
15+
def test_setup_telemetry_adds_events(self):
16+
"""Test that setup_telemetry configures app events."""
17+
app = FastAPI(title="Test App", version="1.0.0")
18+
setup_telemetry(app)
19+
# Verify startup and shutdown events were added
20+
assert len(app.router.on_startup) > 0
21+
assert len(app.router.on_shutdown) > 0
22+
23+
def test_setup_telemetry_logs_info(self, caplog):
24+
"""Test that setup_telemetry logs setup messages."""
25+
app = FastAPI(title="Test App", version="1.0.0")
26+
with caplog.at_level(logging.INFO):
27+
setup_telemetry(app)
28+
assert "Setting up telemetry" in caplog.text
29+
assert "Telemetry setup complete" in caplog.text
30+
31+
32+
class TestLogRequestMetrics:
33+
"""Tests for log_request_metrics function."""
34+
35+
def test_log_request_metrics_basic(self, caplog):
36+
"""Test basic request metrics logging."""
37+
with caplog.at_level(logging.INFO):
38+
log_request_metrics(
39+
endpoint="/api/test",
40+
method="GET",
41+
status_code=200,
42+
duration_ms=15.5,
43+
)
44+
assert "GET" in caplog.text
45+
assert "/api/test" in caplog.text
46+
47+
def test_log_request_metrics_with_request_id(self, caplog):
48+
"""Test request metrics logging with request ID."""
49+
with caplog.at_level(logging.INFO):
50+
log_request_metrics(
51+
endpoint="/api/users",
52+
method="POST",
53+
status_code=201,
54+
duration_ms=25.3,
55+
request_id="req-123-abc",
56+
)
57+
assert "POST" in caplog.text
58+
assert "/api/users" in caplog.text
59+
60+
def test_log_request_metrics_error_status(self, caplog):
61+
"""Test request metrics logging with error status code."""
62+
with caplog.at_level(logging.INFO):
63+
log_request_metrics(
64+
endpoint="/api/error",
65+
method="GET",
66+
status_code=500,
67+
duration_ms=100.0,
68+
)
69+
assert "500" in caplog.text or "GET" in caplog.text

0 commit comments

Comments
 (0)