feat: Add security hardening, agentic coding infrastructure, and test framework
- Add SECRET_KEY and ENCRYPTION_KEY validation on startup - Implement security headers middleware (X-Frame-Options, CSP, HSTS) - Add CSRF protection middleware - Create comprehensive GitHub issue templates and PR template - Add Makefile with common development tasks - Configure pre-commit hooks (black, ruff, mypy, bandit, detect-secrets) - Create docs/CODING_PATTERNS.md with best practices - Create docs/ERRORS.md documenting all error codes - Add Architecture Decision Records (ADR) for Celery and Fernet encryption - Create CHANGELOG.md for version tracking - Set up pytest test infrastructure with fixtures and factories - Add sample unit tests for security and config validation - Create CI/CD workflows (test, lint, security) - Add comprehensive TODO.md with milestones and progress tracking Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Unit tests for configuration module.
|
||||
"""
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
from app.core.config import Settings
|
||||
|
||||
|
||||
class TestConfigValidation:
|
||||
"""Test configuration validation"""
|
||||
|
||||
def test_default_secret_key_rejected(self):
|
||||
"""Test that default SECRET_KEY is rejected"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Settings(
|
||||
SECRET_KEY="change-this-to-a-secure-random-secret-key-in-production",
|
||||
ENCRYPTION_KEY="this-is-a-secure-32-character-key-for-testing",
|
||||
)
|
||||
|
||||
assert "SECRET_KEY must be changed from default" in str(exc_info.value)
|
||||
|
||||
def test_short_secret_key_rejected(self):
|
||||
"""Test that short SECRET_KEY is rejected"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Settings(
|
||||
SECRET_KEY="short",
|
||||
ENCRYPTION_KEY="this-is-a-secure-32-character-key-for-testing",
|
||||
)
|
||||
|
||||
assert "at least 32 characters" in str(exc_info.value)
|
||||
|
||||
def test_default_encryption_key_rejected(self):
|
||||
"""Test that default ENCRYPTION_KEY is rejected"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Settings(
|
||||
SECRET_KEY="this-is-a-secure-32-character-key-for-testing",
|
||||
ENCRYPTION_KEY="change-this-to-a-secure-encryption-key",
|
||||
)
|
||||
|
||||
assert "ENCRYPTION_KEY must be changed from default" in str(exc_info.value)
|
||||
|
||||
def test_short_encryption_key_rejected(self):
|
||||
"""Test that short ENCRYPTION_KEY is rejected"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Settings(
|
||||
SECRET_KEY="this-is-a-secure-32-character-key-for-testing",
|
||||
ENCRYPTION_KEY="short",
|
||||
)
|
||||
|
||||
assert "at least 32 characters" in str(exc_info.value)
|
||||
|
||||
def test_valid_keys_accepted(self):
|
||||
"""Test that valid keys are accepted"""
|
||||
settings = Settings(
|
||||
SECRET_KEY="this-is-a-secure-32-character-key-for-testing-secret",
|
||||
ENCRYPTION_KEY="this-is-a-secure-32-character-key-for-encryption",
|
||||
)
|
||||
|
||||
assert settings.SECRET_KEY == "this-is-a-secure-32-character-key-for-testing-secret"
|
||||
assert settings.ENCRYPTION_KEY == "this-is-a-secure-32-character-key-for-encryption"
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Unit tests for security module.
|
||||
"""
|
||||
import pytest
|
||||
from app.core.security import (
|
||||
get_password_hash,
|
||||
verify_password,
|
||||
create_access_token,
|
||||
encrypt_password,
|
||||
decrypt_password,
|
||||
)
|
||||
|
||||
|
||||
class TestPasswordHashing:
|
||||
"""Test password hashing and verification"""
|
||||
|
||||
def test_hash_password(self):
|
||||
"""Test password hashing"""
|
||||
password = "securepassword123"
|
||||
hashed = get_password_hash(password)
|
||||
|
||||
assert hashed != password
|
||||
assert len(hashed) > 50
|
||||
assert hashed.startswith("$2b$")
|
||||
|
||||
def test_verify_password_success(self):
|
||||
"""Test password verification with correct password"""
|
||||
password = "securepassword123"
|
||||
hashed = get_password_hash(password)
|
||||
|
||||
assert verify_password(password, hashed) is True
|
||||
|
||||
def test_verify_password_failure(self):
|
||||
"""Test password verification with wrong password"""
|
||||
password = "securepassword123"
|
||||
wrong_password = "wrongpassword"
|
||||
hashed = get_password_hash(password)
|
||||
|
||||
assert verify_password(wrong_password, hashed) is False
|
||||
|
||||
|
||||
class TestJWT:
|
||||
"""Test JWT token creation and validation"""
|
||||
|
||||
def test_create_access_token(self):
|
||||
"""Test access token creation"""
|
||||
data = {"sub": "test@example.com"}
|
||||
token = create_access_token(data)
|
||||
|
||||
assert isinstance(token, str)
|
||||
assert len(token) > 50
|
||||
assert token.count('.') == 2 # JWT has 3 parts
|
||||
|
||||
|
||||
class TestEncryption:
|
||||
"""Test credential encryption/decryption"""
|
||||
|
||||
def test_encrypt_password(self):
|
||||
"""Test password encryption"""
|
||||
password = "mailpassword123"
|
||||
user_id = 1
|
||||
|
||||
encrypted = encrypt_password(password, user_id)
|
||||
|
||||
assert encrypted != password
|
||||
assert len(encrypted) > 50
|
||||
|
||||
def test_decrypt_password(self):
|
||||
"""Test password decryption"""
|
||||
password = "mailpassword123"
|
||||
user_id = 1
|
||||
|
||||
encrypted = encrypt_password(password, user_id)
|
||||
decrypted = decrypt_password(encrypted, user_id)
|
||||
|
||||
assert decrypted == password
|
||||
|
||||
def test_encryption_with_different_user_ids(self):
|
||||
"""Test that encryption produces different results for different users"""
|
||||
password = "mailpassword123"
|
||||
user_id_1 = 1
|
||||
user_id_2 = 2
|
||||
|
||||
encrypted_1 = encrypt_password(password, user_id_1)
|
||||
encrypted_2 = encrypt_password(password, user_id_2)
|
||||
|
||||
# Different users should produce different encrypted values
|
||||
assert encrypted_1 != encrypted_2
|
||||
|
||||
# But decryption should work correctly for each
|
||||
assert decrypt_password(encrypted_1, user_id_1) == password
|
||||
assert decrypt_password(encrypted_2, user_id_2) == password
|
||||
|
||||
def test_decrypt_with_wrong_user_id_fails(self):
|
||||
"""Test that decryption fails with wrong user ID"""
|
||||
password = "mailpassword123"
|
||||
user_id = 1
|
||||
wrong_user_id = 2
|
||||
|
||||
encrypted = encrypt_password(password, user_id)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
decrypt_password(encrypted, wrong_user_id)
|
||||
Reference in New Issue
Block a user