feat(logs): add workspace activity log api

This commit is contained in:
2026-04-28 16:42:37 +03:30
parent c8a118788b
commit 71924ce6fb
32 changed files with 1118 additions and 122 deletions

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,181 @@
from __future__ import annotations
from datetime import timedelta
import pytest
from auditlog.models import LogEntry
from rest_framework_simplejwt.tokens import AccessToken
from rest_framework.test import APIClient
from apps.reports.models import ReportExportJob
from apps.users.models import User
from apps.workspaces.models import Workspace, WorkspaceMembership
@pytest.fixture()
def api_client():
return APIClient()
def _user(index: int) -> User:
return User.objects.create_user(
mobile=f"093355500{index:02d}",
password="secret123",
first_name=f"Log{index}",
last_name="User",
)
@pytest.fixture()
def owner(db):
return _user(1)
@pytest.fixture()
def admin(db):
return _user(2)
@pytest.fixture()
def member(db):
return _user(3)
@pytest.fixture()
def outsider(db):
return _user(4)
@pytest.fixture()
def workspace(owner, admin, member):
workspace = Workspace.objects.create(name="Logs WS", description="", owner=owner)
WorkspaceMembership.objects.create(
workspace=workspace,
user=admin,
role=WorkspaceMembership.Role.ADMIN,
is_active=True,
)
WorkspaceMembership.objects.create(
workspace=workspace,
user=member,
role=WorkspaceMembership.Role.MEMBER,
is_active=True,
)
return workspace
def _auth_headers(user: User) -> dict:
token = str(AccessToken.for_user(user))
return {"HTTP_AUTHORIZATION": f"Bearer {token}"}
def _create_tag(client: APIClient, user: User, workspace: Workspace, *, name="Audit Tag"):
return client.post(
"/api/tags/",
{"workspace_id": str(workspace.id), "name": name, "color": "#123456"},
format="json",
**_auth_headers(user),
)
@pytest.mark.django_db
def test_owner_and_admin_can_list_workspace_logs(api_client, owner, admin, workspace):
create_response = _create_tag(api_client, owner, workspace)
assert create_response.status_code == 201
owner_response = api_client.get(
f"/api/logs/?workspace={workspace.id}",
**_auth_headers(owner),
)
admin_response = api_client.get(
f"/api/logs/?workspace={workspace.id}",
**_auth_headers(admin),
)
assert owner_response.status_code == 200
assert admin_response.status_code == 200
assert owner_response.data["items"][0]["section"] == "tags"
@pytest.mark.django_db
def test_member_and_non_member_cannot_list_workspace_logs(api_client, owner, member, outsider, workspace):
_create_tag(api_client, owner, workspace)
member_response = api_client.get(
f"/api/logs/?workspace={workspace.id}",
**_auth_headers(member),
)
outsider_response = api_client.get(
f"/api/logs/?workspace={workspace.id}",
**_auth_headers(outsider),
)
assert member_response.status_code == 403
assert outsider_response.status_code == 403
@pytest.mark.django_db
def test_jwt_authenticated_writes_capture_actor_and_workspace_metadata(api_client, owner, workspace):
response = _create_tag(api_client, owner, workspace, name="JWT Tag")
assert response.status_code == 201
log_entry = LogEntry.objects.filter(content_type__app_label="tags").latest("timestamp")
assert log_entry.actor_id == owner.id
assert log_entry.additional_data["workspace_id"] == str(workspace.id)
assert log_entry.additional_data["section"] == "tags"
@pytest.mark.django_db
def test_logs_support_section_filter_and_detail(api_client, owner, workspace):
tag_response = _create_tag(api_client, owner, workspace, name="Filtered Tag")
assert tag_response.status_code == 201
list_response = api_client.get(
f"/api/logs/?workspace={workspace.id}&section=tags",
**_auth_headers(owner),
)
assert list_response.status_code == 200
assert list_response.data["items"]
log_id = list_response.data["items"][0]["id"]
detail_response = api_client.get(
f"/api/logs/{log_id}/",
**_auth_headers(owner),
)
assert detail_response.status_code == 200
assert detail_response.data["target"]["name"] == "Filtered Tag"
assert detail_response.data["changes"]
@pytest.mark.django_db
def test_soft_delete_and_actorless_background_logs_are_filtered(api_client, owner, workspace):
create_response = _create_tag(api_client, owner, workspace, name="Delete Me")
assert create_response.status_code == 201
tag_id = create_response.data["id"]
delete_response = api_client.delete(
f"/api/tags/{tag_id}/",
**_auth_headers(owner),
)
assert delete_response.status_code == 204
ReportExportJob.objects.create(
requesting_user=owner,
workspace=workspace,
export_type=ReportExportJob.ExportType.PDF,
filters={"workspace": str(workspace.id)},
status=ReportExportJob.Status.PENDING,
)
response = api_client.get(
f"/api/logs/?workspace={workspace.id}&event=delete",
**_auth_headers(owner),
)
assert response.status_code == 200
assert any(item["event"] == "delete" and item["section"] == "tags" for item in response.data["items"])
assert all(item["section"] != "report_exports" for item in response.data["items"])