"""Tests for audit: write operations produce audit log entries.""" import uuid import pytest import respx from httpx import Response from app.core.config import settings @pytest.mark.asyncio async def test_login_produces_audit_log(client, db_session): """Login action creates an audit log entry.""" from sqlalchemy import text from app.auth.models import AuthUserRole from app.identity.person_models import IdentityPerson user_id = uuid.uuid4() user = IdentityPerson( id=user_id, work_email="audituser@company.com", display_name="Audit User", employment_status="active", source_system="successfactors", hris_person_id="sf-audit-001", ) db_session.add(user) await db_session.flush() role_result = await db_session.execute( text("SELECT id FROM auth_roles WHERE name = 'employee'") ) role_id = role_result.scalar_one() if isinstance(role_id, str): role_id = uuid.UUID(role_id) db_session.add(AuthUserRole(user_id=user_id, role_id=role_id)) await db_session.flush() with respx.mock: respx.get("https://oauth2.googleapis.com/tokeninfo").mock( return_value=Response( 200, json={ "sub": "google-sub-audit", "email": "audituser@company.com", "email_verified": "true", "aud": settings.GOOGLE_CLIENT_ID, "iss": "accounts.google.com", }, ) ) await client.post( "/api/v1/auth/google/verify", json={"id_token": "fake-token"}, ) # Check audit log from sqlalchemy import select from app.audit.models import AuditLog result = await db_session.execute( select(AuditLog).where( AuditLog.action == "auth.login", AuditLog.actor_user_id == user_id, ) ) log = result.scalar_one_or_none() assert log is not None assert log.resource_type == "auth_user" assert log.status == "success" @pytest.mark.asyncio async def test_user_update_produces_audit_log(client, admin_user, employee_user, db_session): """Updating a user creates an audit log entry.""" admin, admin_token = admin_user emp, _ = employee_user resp = await client.patch( f"/api/v1/users/{emp.id}", headers={"Authorization": f"Bearer {admin_token}"}, json={"display_name": "Updated Name"}, ) assert resp.status_code == 200 from sqlalchemy import select from app.audit.models import AuditLog result = await db_session.execute( select(AuditLog).where( AuditLog.action == "user.update", AuditLog.resource_id == str(emp.id), ) ) log = result.scalar_one_or_none() assert log is not None assert log.actor_user_id == admin.id @pytest.mark.asyncio async def test_admin_can_query_audit_logs(client, admin_user): """IT Admin and Security Auditor can query audit logs.""" _, token = admin_user resp = await client.get( "/api/v1/audit/logs", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 200 assert resp.json()["success"] is True @pytest.mark.asyncio async def test_audit_log_has_trace_id(client, admin_user, employee_user, db_session): """Audit logs have trace_id for traceability.""" admin, admin_token = admin_user emp, _ = employee_user resp = await client.patch( f"/api/v1/users/{emp.id}", headers={"Authorization": f"Bearer {admin_token}"}, json={"position": "Senior Engineer"}, ) trace_id = resp.json()["meta"]["trace_id"] assert trace_id is not None from sqlalchemy import select from app.audit.models import AuditLog result = await db_session.execute( select(AuditLog).where(AuditLog.action == "user.update") ) logs = list(result.scalars().all()) trace_ids = [str(log.trace_id) for log in logs] assert trace_id in trace_ids