""" Tests for audit logger module. Following TDD approach - these tests are written before implementation. Tests cover all logging requirements from todo.md section 1.2.3. """ import pytest import json from pathlib import Path import tempfile import threading import time from datetime import datetime def test_log_entries_written_to_file(tmp_path): """Test that log entries are written to the log file.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log( event_type=AuditEventType.CONTAINER_CREATE, severity=AuditSeverity.INFO, message="Test container created", details={"image": "test-image", "container_id": "abc123"} ) assert log_file.exists() content = log_file.read_text() assert len(content) > 0 def test_log_entries_are_valid_json(tmp_path): """Test that log entries are valid JSON.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log( event_type=AuditEventType.CONTAINER_CREATE, severity=AuditSeverity.INFO, message="Test entry", details={"key": "value"} ) # Each line should be valid JSON lines = log_file.read_text().strip().split("\n") for line in lines: data = json.loads(line) # Should not raise assert isinstance(data, dict) def test_log_entries_contain_required_fields(tmp_path): """Test that log entries contain all required fields.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log( event_type=AuditEventType.EXECUTION_REQUEST, severity=AuditSeverity.INFO, message="Code execution requested", session_id="test-session-123" ) lines = log_file.read_text().strip().split("\n") entry = json.loads(lines[0]) # Required fields assert "timestamp" in entry assert "event_type" in entry assert "severity" in entry assert "message" in entry assert "session_id" in entry def test_timestamp_format_is_iso_8601(tmp_path): """Test that timestamp is in ISO 8601 format.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log( event_type=AuditEventType.SESSION_CREATE, severity=AuditSeverity.INFO, message="Session created" ) lines = log_file.read_text().strip().split("\n") entry = json.loads(lines[0]) # Should be parseable as ISO 8601 timestamp = entry["timestamp"] dt = datetime.fromisoformat(timestamp) assert isinstance(dt, datetime) def test_concurrent_logging_is_thread_safe(tmp_path): """Test that concurrent logging from multiple threads is thread-safe.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) def log_entries(thread_id, count): for i in range(count): logger.log( event_type=AuditEventType.CONTAINER_CREATE, severity=AuditSeverity.INFO, message=f"Thread {thread_id} entry {i}" ) # Create multiple threads threads = [] entries_per_thread = 10 num_threads = 5 for i in range(num_threads): t = threading.Thread(target=log_entries, args=(i, entries_per_thread)) threads.append(t) t.start() # Wait for all threads for t in threads: t.join() # Verify all entries written lines = log_file.read_text().strip().split("\n") assert len(lines) == num_threads * entries_per_thread # Verify all entries are valid JSON for line in lines: json.loads(line) def test_security_violations_logged_with_correct_severity(tmp_path): """Test that security violations are logged at correct severity.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log_security_violation( operation="container_create", reason="Privileged mode attempted", session_id="test-session" ) lines = log_file.read_text().strip().split("\n") entry = json.loads(lines[0]) assert entry["severity"] == "critical" assert entry["event_type"] == "security.violation" def test_pii_is_not_logged(tmp_path): """Test that PII (code content, tokens, files) is not logged.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) # Log execution request - should NOT include actual code logger.log( event_type=AuditEventType.EXECUTION_REQUEST, severity=AuditSeverity.INFO, message="Code execution requested", details={ "code_hash": "abc123def456", # Hash is OK # "code": "print('hello')" # Should NOT be logged } ) content = log_file.read_text() # Should not contain actual code assert "print" not in content assert "hello" not in content # Should contain hash assert "abc123def456" in content def test_log_container_operation(tmp_path): """Test log_container_operation convenience method.""" from mcp_forge.security.audit import AuditLogger, AuditEventType log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log_container_operation( operation="create", container_id="container-123", image="mcp-forge/python:3.11", session_id="session-456" ) lines = log_file.read_text().strip().split("\n") entry = json.loads(lines[0]) assert entry["event_type"] == "container.create" assert entry["container_id"] == "container-123" assert entry["image"] == "mcp-forge/python:3.11" assert entry["session_id"] == "session-456" def test_log_container_operation_with_error(tmp_path): """Test logging container operation with error.""" from mcp_forge.security.audit import AuditLogger log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log_container_operation( operation="start", container_id="container-123", image="mcp-forge/python:3.11", session_id="session-456", error="Container not found" ) lines = log_file.read_text().strip().split("\n") entry = json.loads(lines[0]) assert "error" in entry assert entry["error"] == "Container not found" assert entry["severity"] == "error" def test_log_creates_directory_if_not_exists(tmp_path): """Test that logger creates log directory if it doesn't exist.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_dir = tmp_path / "nested" / "log" / "dir" log_file = log_dir / "audit.log" logger = AuditLogger(log_file) logger.log( event_type=AuditEventType.SESSION_CREATE, severity=AuditSeverity.INFO, message="Test" ) assert log_file.exists() assert log_file.parent.exists() def test_audit_event_types(): """Test that all required audit event types are defined.""" from mcp_forge.security.audit import AuditEventType # Required event types from todo.md assert hasattr(AuditEventType, "CONTAINER_CREATE") assert hasattr(AuditEventType, "CONTAINER_START") assert hasattr(AuditEventType, "CONTAINER_STOP") assert hasattr(AuditEventType, "CONTAINER_REMOVE") assert hasattr(AuditEventType, "EXECUTION_REQUEST") assert hasattr(AuditEventType, "SECURITY_VIOLATION") assert hasattr(AuditEventType, "BUILD_REQUEST") assert hasattr(AuditEventType, "BUILD_COMPLETE") assert hasattr(AuditEventType, "SESSION_CREATE") assert hasattr(AuditEventType, "SESSION_DESTROY") def test_audit_severity_levels(): """Test that all required severity levels are defined.""" from mcp_forge.security.audit import AuditSeverity assert hasattr(AuditSeverity, "INFO") assert hasattr(AuditSeverity, "WARNING") assert hasattr(AuditSeverity, "ERROR") assert hasattr(AuditSeverity, "CRITICAL") def test_log_with_all_optional_parameters(tmp_path): """Test logging with all optional parameters provided.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log( event_type=AuditEventType.BUILD_COMPLETE, severity=AuditSeverity.INFO, message="Build completed successfully", session_id="session-123", user_id="user-456", details={"image": "custom-env", "duration": 120}, error=None ) lines = log_file.read_text().strip().split("\n") entry = json.loads(lines[0]) assert entry["session_id"] == "session-123" assert entry["user_id"] == "user-456" assert entry["details"]["image"] == "custom-env" assert entry["details"]["duration"] == 120 def test_multiple_log_entries_on_separate_lines(tmp_path): """Test that multiple log entries are written on separate lines.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) for i in range(5): logger.log( event_type=AuditEventType.CONTAINER_CREATE, severity=AuditSeverity.INFO, message=f"Entry {i}" ) lines = log_file.read_text().strip().split("\n") assert len(lines) == 5 # Each line should be parseable for line in lines: json.loads(line) def test_log_security_violation_parameters(tmp_path): """Test log_security_violation includes all necessary information.""" from mcp_forge.security.audit import AuditLogger log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log_security_violation( operation="volume_mount", reason="Attempted to mount /etc", session_id="session-789" ) lines = log_file.read_text().strip().split("\n") entry = json.loads(lines[0]) assert entry["event_type"] == "security.violation" assert entry["severity"] == "critical" assert entry["operation"] == "volume_mount" assert entry["reason"] == "Attempted to mount /etc" assert entry["session_id"] == "session-789" def test_details_can_be_none(tmp_path): """Test that details parameter can be None.""" from mcp_forge.security.audit import AuditLogger, AuditEventType, AuditSeverity log_file = tmp_path / "audit.log" logger = AuditLogger(log_file) logger.log( event_type=AuditEventType.SESSION_CREATE, severity=AuditSeverity.INFO, message="Session created", details=None ) lines = log_file.read_text().strip().split("\n") entry = json.loads(lines[0]) # Should work without error assert "message" in entry