"""Audit log coverage for every v49 admin endpoint (Task 6.7.3). Maps directly to Section 9.1 of the unified-stack design — every row in that table needs at least one passing assertion here. Failure of this file is the canary signal that an admin mutation forgot its audit write. """ from __future__ import annotations import json import uuid import pytest from src.db import get_system_db from src.repositories.knowledge import KnowledgeRepository def _auth(token): return {"Authorization": f"Bearer {token}"} def _audit_actions() -> list[dict]: conn = get_system_db() rows = conn.execute( "SELECT action, resource, params FROM audit_log " "ORDER BY timestamp DESC LIMIT 500" ).fetchall() conn.close() return [ { "action": a, "resource": r, "params": json.loads(p) if p else None, } for a, r, p in rows ] def _create_group_with_analyst(name: str) -> str: from src.repositories.user_groups import UserGroupsRepository from src.repositories.user_group_members import UserGroupMembersRepository conn = get_system_db() g = UserGroupsRepository(conn).create(name=name, description="", created_by="test") gid = g["id"] if isinstance(g, dict) else g UserGroupMembersRepository(conn).add_member("analyst1", gid, source="test") conn.close() return gid def _create_knowledge_item() -> str: conn = get_system_db() item_id = "ki_" + uuid.uuid4().hex[:8] KnowledgeRepository(conn).create( id=item_id, title="T", content="x", category="engineering", status="approved", ) conn.close() return item_id # -------- Data Package admin actions ------------------------------------- def test_data_package_create_update_delete_audited(seeded_app): c = seeded_app["client"] h = _auth(seeded_app["admin_token"]) pid = c.post( "/api/admin/data-packages", json={"name": "AuditPkg", "slug": "audit-pkg"}, headers=h, ).json()["id"] c.put(f"/api/admin/data-packages/{pid}", json={"name": "AuditPkg2"}, headers=h) c.delete(f"/api/admin/data-packages/{pid}", headers=h) rows = _audit_actions() actions = {r["action"] for r in rows if r["resource"] == f"data_package:{pid}"} assert {"data_package.create", "data_package.update", "data_package.delete"} <= actions def test_data_package_add_remove_table_audited(seeded_app): from src.repositories.table_registry import TableRegistryRepository conn = get_system_db() tbl_id = "tbl_audit_xx" TableRegistryRepository(conn).register( id=tbl_id, name="t_audit_xx", source_type="keboola", source_table="in.c-test.t_audit_xx", bucket="in.c-test", query_mode="local", ) conn.close() c = seeded_app["client"] h = _auth(seeded_app["admin_token"]) pid = c.post( "/api/admin/data-packages", json={"name": "P", "slug": "audit-junction"}, headers=h, ).json()["id"] c.post(f"/api/admin/data-packages/{pid}/tables", json={"table_id": tbl_id}, headers=h) c.delete(f"/api/admin/data-packages/{pid}/tables/{tbl_id}", headers=h) actions = {r["action"] for r in _audit_actions() if r["resource"] == f"data_package:{pid}"} assert "data_package.add_table" in actions assert "data_package.remove_table" in actions # -------- Memory Domain admin actions ------------------------------------ def test_memory_domain_create_update_delete_audited(seeded_app): c = seeded_app["client"] h = _auth(seeded_app["admin_token"]) did = c.post( "/api/admin/memory-domains", json={"name": "AuditDom", "slug": "audit-dom"}, headers=h, ).json()["id"] c.put(f"/api/admin/memory-domains/{did}", json={"name": "AuditDom2"}, headers=h) c.delete(f"/api/admin/memory-domains/{did}", headers=h) actions = {r["action"] for r in _audit_actions() if r["resource"] == f"memory_domain:{did}"} assert {"memory_domain.create", "memory_domain.update", "memory_domain.delete"} <= actions def test_memory_domain_add_remove_item_audited(seeded_app): item_id = _create_knowledge_item() c = seeded_app["client"] h = _auth(seeded_app["admin_token"]) did = c.post( "/api/admin/memory-domains", json={"name": "DJ", "slug": "audit-dom-junction"}, headers=h, ).json()["id"] c.post(f"/api/admin/memory-domains/{did}/items", json={"item_id": item_id}, headers=h) c.delete(f"/api/admin/memory-domains/{did}/items/{item_id}", headers=h) actions = {r["action"] for r in _audit_actions() if r["resource"] == f"memory_domain:{did}"} assert "memory_domain.add_item" in actions assert "memory_domain.remove_item" in actions # -------- Grant admin actions -------------------------------------------- def test_grant_create_update_delete_audited(seeded_app): from src.repositories.data_packages import DataPackagesRepository conn = get_system_db() pkg_id = DataPackagesRepository(conn).create( name="GrantP", slug="grant-pkg", description=None, icon=None, color=None, created_by="test", ) conn.close() gid = _create_group_with_analyst("GrantG") c = seeded_app["client"] h = _auth(seeded_app["admin_token"]) # create grant = c.post( "/api/admin/grants", json={ "group_id": gid, "resource_type": "data_package", "resource_id": pkg_id, "requirement": "available", }, headers=h, ) if grant.status_code != 201: pytest.skip(f"grant create returned {grant.status_code}: {grant.json()}") grant_id = grant.json()["id"] # update (requirement → required → triggers soft downgrade audit on revert) c.put( f"/api/admin/grants/{grant_id}", json={"requirement": "required"}, headers=h, ) c.delete(f"/api/admin/grants/{grant_id}", headers=h) actions = [r for r in _audit_actions() if r["resource"] == f"grant:{grant_id}"] a_set = {r["action"] for r in actions} # The existing /api/admin/grants endpoints write # resource_grant.created/requirement_updated/deleted. Per Section 9.1 # those are the canonical actions for the grant lifecycle — verify each # link in the chain emitted at least one row. assert "resource_grant.created" in a_set assert "resource_grant.requirement_updated" in a_set assert "resource_grant.deleted" in a_set # -------- Memory item set_required --------------------------------------- def test_memory_item_set_required_audited(seeded_app): item_id = _create_knowledge_item() c = seeded_app["client"] h = _auth(seeded_app["admin_token"]) c.post(f"/api/memory/items/{item_id}/mark-mandatory", headers=h) c.post(f"/api/memory/items/{item_id}/mark-unmandatory", headers=h) rows = [ r for r in _audit_actions() if r["resource"] == f"knowledge_item:{item_id}" and r["action"] == "memory_item.set_required" ] values = {r["params"]["new_value"] for r in rows} assert values == {True, False}