"""
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()
from factories import TimeEntryFactory
other_entry = TimeEntryFactory(
user_id=other_user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=datetime.utcnow(),
source="manual",
)
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 "