"""
Security testing suite.
Tests authentication, authorization, and security vulnerabilities.
"""
import pytest
from flask import session
from app import db
from app.models import User, Project, TimeEntry
# ============================================================================
# Authentication Tests
# ============================================================================
@pytest.mark.security
@pytest.mark.smoke
def test_unauthenticated_cannot_access_dashboard(client):
"""Test that unauthenticated users cannot access protected pages."""
response = client.get('/dashboard', follow_redirects=False)
assert response.status_code == 302 # Redirect to login
@pytest.mark.security
@pytest.mark.smoke
def test_unauthenticated_cannot_access_api(client):
"""Test that unauthenticated users cannot access API endpoints."""
response = client.get('/api/timer/active')
assert response.status_code in [302, 401, 403, 404] # 404 is also acceptable if endpoint doesn't exist without auth
@pytest.mark.security
def test_session_cookie_httponly(client, user):
"""Test that session cookies are HTTPOnly."""
with client:
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
response = client.get('/dashboard')
# Check Set-Cookie header for HTTPOnly flag
set_cookie_headers = response.headers.getlist('Set-Cookie')
for header in set_cookie_headers:
if 'session' in header.lower():
assert 'HttpOnly' in header
# ============================================================================
# Authorization Tests
# ============================================================================
@pytest.mark.security
@pytest.mark.integration
def test_regular_user_cannot_access_admin_pages(authenticated_client):
"""Test that regular users cannot access admin pages."""
response = authenticated_client.get('/admin', follow_redirects=False)
assert response.status_code in [302, 403]
@pytest.mark.security
@pytest.mark.integration
def test_admin_can_access_admin_pages(admin_authenticated_client):
"""Test that admin users can access admin pages."""
response = admin_authenticated_client.get('/admin')
assert response.status_code == 200
@pytest.mark.security
@pytest.mark.integration
def test_user_cannot_access_other_users_data(app, user, multiple_users, authenticated_client):
"""Test that users cannot access other users' data."""
with app.app_context():
other_user = multiple_users[0]
# Try to access another user's profile/data
response = authenticated_client.get(f'/api/user/{other_user.id}')
# Should return 403 Forbidden or 404 Not Found
assert response.status_code in [403, 404, 302]
@pytest.mark.security
@pytest.mark.integration
def test_user_cannot_edit_other_users_time_entries(app, authenticated_client, user, test_client):
"""Test that users cannot edit other users' time entries."""
from datetime import datetime
with app.app_context():
# Create another user with a time entry
other_user = User(username='otheruser', role='user', email='otheruser@example.com')
other_user.is_active = True
db.session.add(other_user)
db.session.commit()
project = Project.query.first()
if not project:
project = Project(name='Test', client_id=test_client.id, billable=True)
project.status = 'active'
db.session.add(project)
db.session.commit()
other_entry = TimeEntry(
user_id=other_user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=datetime.utcnow(),
source='manual'
)
db.session.add(other_entry)
db.session.commit()
# Try to edit the other user's entry
response = authenticated_client.post(f'/api/timer/edit/{other_entry.id}', json={
'notes': 'Trying to hack'
})
# Should be forbidden
assert response.status_code in [403, 404, 302]
# ============================================================================
# CSRF Protection Tests
# ============================================================================
@pytest.mark.security
def test_csrf_token_required_for_forms(client, user):
"""Test that CSRF token is required for form submissions."""
with client:
with client.session_transaction() as sess:
sess['_user_id'] = str(user.id)
# Try to submit a form without CSRF token
response = client.post('/projects/new', data={
'name': 'Test Project',
'billable': True
}, follow_redirects=False)
# Should fail with 400 or redirect
# Note: This test assumes CSRF is enabled in production
# In test config, CSRF might be disabled
pass # Adjust based on your CSRF configuration
# ============================================================================
# SQL Injection Tests
# ============================================================================
@pytest.mark.security
def test_sql_injection_in_search(authenticated_client):
"""Test SQL injection protection in search."""
# Try SQL injection in search
malicious_query = "'; DROP TABLE users; --"
response = authenticated_client.get('/api/search', query_string={
'q': malicious_query
})
# Should handle gracefully, not execute SQL
assert response.status_code in [200, 400, 404]
@pytest.mark.security
def test_sql_injection_in_filter(authenticated_client):
"""Test SQL injection protection in filters."""
malicious_input = "1' OR '1'='1"
response = authenticated_client.get('/api/projects', query_string={
'client_id': malicious_input
})
# Should handle gracefully
assert response.status_code in [200, 400, 404]
# ============================================================================
# XSS Protection Tests
# ============================================================================
@pytest.mark.security
def test_xss_in_project_name(app, authenticated_client, test_client):
"""Test XSS protection in project names."""
with app.app_context():
xss_payload = ''
response = authenticated_client.post('/api/projects', json={
'name': xss_payload,
'client_id': test_client.id,
'billable': True
})
# Should either sanitize or reject
if response.status_code in [200, 201]:
data = response.get_json()
# Script tags should be escaped or removed
assert '