Files
TimeTracker/tests/test_audit_trail_smoke.py
T
Dries Peeters 350d7105a2 feat: Add comprehensive audit trail/history tracking system
Implement a complete audit logging system to track all changes made to
tracked entities, providing full compliance and accountability capabilities.

Features:
- Automatic tracking of create, update, and delete operations on 25+ models
- Detailed field-level change tracking with old/new value comparison
- User attribution with IP address, user agent, and request path logging
- Web UI for viewing and filtering audit logs with pagination
- REST API endpoints for programmatic access
- Entity-specific history views
- Comprehensive test coverage (unit, model, route, and smoke tests)

Core Components:
- AuditLog model with JSON-encoded value storage and decoding helpers
- SQLAlchemy event listeners for automatic change detection
- Audit utility module with defensive programming for table existence checks
- Blueprint routes for audit log viewing and API access
- Jinja2 templates for audit log list, detail, and entity history views
- Database migration (044) creating audit_logs table with proper indexes

Technical Implementation:
- Uses SQLAlchemy 'after_flush' event listener to capture changes
- Tracks 25+ models including Projects, Tasks, TimeEntries, Invoices, Clients, Users, etc.
- Excludes sensitive fields (passwords) and system fields (id, timestamps)
- Implements lazy import pattern to avoid circular dependencies
- Graceful error handling to prevent audit logging from breaking core functionality
- Transaction-safe logging that integrates with main application transactions

Fixes:
- Resolved login errors caused by premature transaction commits
- Fixed circular import issues with lazy model loading
- Added table existence checks to prevent errors before migrations
- Improved error handling with debug-level logging for non-critical failures

UI/UX:
- Added "Audit Logs" link to admin dropdown menu
- Organized admin menu into logical sections for better usability
- Filterable audit log views by entity type, user, action, and date range
- Color-coded action badges and side-by-side old/new value display
- Pagination support for large audit log datasets

Documentation:
- Added comprehensive feature documentation
- Included troubleshooting guide and data examples
- Created diagnostic scripts for verifying audit log setup

Testing:
- Unit tests for AuditLog model and value encoding/decoding
- Route tests for all audit log endpoints
- Integration tests for audit logging functionality
- Smoke tests for end-to-end audit trail verification

This implementation provides a robust foundation for compliance tracking
and change accountability without impacting application performance or
requiring code changes in existing routes/models.
2025-11-13 08:08:48 +01:00

189 lines
7.1 KiB
Python

"""Smoke tests for audit trail feature"""
import pytest
from datetime import datetime
from app.models import AuditLog, Project, User, Task
from app import db
@pytest.mark.smoke
class TestAuditTrailSmoke:
"""Smoke tests to verify audit trail feature works end-to-end"""
def test_audit_log_creation_smoke(self, app, test_user, test_project):
"""Smoke test: Create an audit log entry"""
with app.app_context():
audit_log = AuditLog.log_change(
user_id=test_user.id,
action='created',
entity_type='Project',
entity_id=test_project.id,
entity_name=test_project.name,
change_description='Smoke test audit log'
)
# Verify log was created
logs = AuditLog.query.filter_by(
entity_type='Project',
entity_id=test_project.id
).all()
assert len(logs) > 0
assert logs[0].action == 'created'
assert logs[0].user_id == test_user.id
def test_audit_log_field_change_tracking_smoke(self, app, test_user, test_project):
"""Smoke test: Track field-level changes"""
with app.app_context():
# Log a field change
AuditLog.log_change(
user_id=test_user.id,
action='updated',
entity_type='Project',
entity_id=test_project.id,
field_name='name',
old_value='Old Project Name',
new_value='New Project Name',
entity_name=test_project.name
)
# Verify field change was logged
logs = AuditLog.query.filter_by(
entity_type='Project',
entity_id=test_project.id,
field_name='name'
).all()
assert len(logs) > 0
log = logs[0]
assert log.field_name == 'name'
assert log.get_old_value() == 'Old Project Name'
assert log.get_new_value() == 'New Project Name'
def test_audit_log_entity_history_smoke(self, app, test_user, test_project):
"""Smoke test: Retrieve entity history"""
with app.app_context():
# Create multiple audit logs for the same entity
for i in range(3):
AuditLog.log_change(
user_id=test_user.id,
action='updated',
entity_type='Project',
entity_id=test_project.id,
field_name=f'field_{i}',
old_value=f'old_{i}',
new_value=f'new_{i}',
entity_name=test_project.name
)
# Retrieve entity history
history = AuditLog.get_for_entity('Project', test_project.id, limit=10)
assert len(history) == 3
assert all(log.entity_type == 'Project' for log in history)
assert all(log.entity_id == test_project.id for log in history)
def test_audit_log_user_activity_smoke(self, app, test_user, test_project):
"""Smoke test: Retrieve user activity history"""
with app.app_context():
# Create multiple audit logs by the same user
for i in range(3):
AuditLog.log_change(
user_id=test_user.id,
action='updated',
entity_type='Project',
entity_id=test_project.id,
field_name=f'field_{i}',
old_value=f'old_{i}',
new_value=f'new_{i}',
entity_name=test_project.name
)
# Retrieve user activity
user_logs = AuditLog.get_for_user(test_user.id, limit=10)
assert len(user_logs) >= 3
assert all(log.user_id == test_user.id for log in user_logs)
def test_audit_log_filtering_smoke(self, app, test_user, test_project):
"""Smoke test: Filter audit logs by various criteria"""
with app.app_context():
# Create audit logs with different actions
AuditLog.log_change(
user_id=test_user.id,
action='created',
entity_type='Project',
entity_id=test_project.id,
entity_name=test_project.name
)
AuditLog.log_change(
user_id=test_user.id,
action='updated',
entity_type='Project',
entity_id=test_project.id,
field_name='name',
old_value='Old',
new_value='New',
entity_name=test_project.name
)
AuditLog.log_change(
user_id=test_user.id,
action='deleted',
entity_type='Project',
entity_id=test_project.id,
entity_name=test_project.name
)
# Filter by action
created_logs = AuditLog.get_recent(action='created', limit=10)
assert len(created_logs) == 1
assert created_logs[0].action == 'created'
# Filter by entity type
project_logs = AuditLog.get_recent(entity_type='Project', limit=10)
assert len(project_logs) >= 3
# Filter by user
user_logs = AuditLog.get_recent(user_id=test_user.id, limit=10)
assert len(user_logs) >= 3
def test_audit_log_value_serialization_smoke(self, app, test_user, test_project):
"""Smoke test: Verify value serialization works correctly"""
with app.app_context():
# Test with various value types
test_cases = [
('string', 'Old Value', 'New Value'),
('number', 123, 456),
('boolean', True, False),
('datetime', datetime(2024, 1, 1), datetime(2024, 1, 2)),
]
for field_type, old_val, new_val in test_cases:
AuditLog.log_change(
user_id=test_user.id,
action='updated',
entity_type='Project',
entity_id=test_project.id,
field_name=f'test_{field_type}',
old_value=old_val,
new_value=new_val,
entity_name=test_project.name
)
# Verify all logs were created
logs = AuditLog.query.filter_by(
entity_type='Project',
entity_id=test_project.id
).all()
assert len(logs) >= len(test_cases)
# Verify values can be retrieved
for log in logs:
if log.field_name and log.field_name.startswith('test_'):
old_val = log.get_old_value()
new_val = log.get_new_value()
assert old_val is not None or log.old_value is None
assert new_val is not None or log.new_value is None