""" 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 '