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