|
c588255…
|
ragelink
|
1 |
"""Tests for Unified Audit Log: view, permissions, filtering.""" |
|
c588255…
|
ragelink
|
2 |
|
|
c588255…
|
ragelink
|
3 |
import pytest |
|
c588255…
|
ragelink
|
4 |
from django.contrib.auth.models import Group, Permission |
|
c588255…
|
ragelink
|
5 |
|
|
c588255…
|
ragelink
|
6 |
from organization.models import Team |
|
c588255…
|
ragelink
|
7 |
from projects.models import Project |
|
c588255…
|
ragelink
|
8 |
|
|
c588255…
|
ragelink
|
9 |
|
|
c588255…
|
ragelink
|
10 |
@pytest.fixture |
|
c588255…
|
ragelink
|
11 |
def org_admin_user(db): |
|
c588255…
|
ragelink
|
12 |
"""User with ORGANIZATION_CHANGE permission but not superuser.""" |
|
c588255…
|
ragelink
|
13 |
user = __import__("django.contrib.auth.models", fromlist=["User"]).User.objects.create_user( |
|
c588255…
|
ragelink
|
14 |
username="orgadmin", email="[email protected]", password="testpass123" |
|
c588255…
|
ragelink
|
15 |
) |
|
c588255…
|
ragelink
|
16 |
group, _ = Group.objects.get_or_create(name="OrgAdmins") |
|
c588255…
|
ragelink
|
17 |
perms = Permission.objects.filter( |
|
c588255…
|
ragelink
|
18 |
content_type__app_label="organization", |
|
c588255…
|
ragelink
|
19 |
) |
|
c588255…
|
ragelink
|
20 |
group.permissions.set(perms) |
|
c588255…
|
ragelink
|
21 |
user.groups.add(group) |
|
c588255…
|
ragelink
|
22 |
return user |
|
c588255…
|
ragelink
|
23 |
|
|
c588255…
|
ragelink
|
24 |
|
|
c588255…
|
ragelink
|
25 |
@pytest.fixture |
|
c588255…
|
ragelink
|
26 |
def org_admin_client(client, org_admin_user): |
|
c588255…
|
ragelink
|
27 |
client.login(username="orgadmin", password="testpass123") |
|
c588255…
|
ragelink
|
28 |
return client |
|
c588255…
|
ragelink
|
29 |
|
|
c588255…
|
ragelink
|
30 |
|
|
c588255…
|
ragelink
|
31 |
# --- Access Control --- |
|
c588255…
|
ragelink
|
32 |
|
|
c588255…
|
ragelink
|
33 |
|
|
c588255…
|
ragelink
|
34 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
35 |
class TestAuditLogAccess: |
|
c588255…
|
ragelink
|
36 |
def test_audit_log_accessible_to_superuser(self, admin_client): |
|
c588255…
|
ragelink
|
37 |
response = admin_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
38 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
39 |
assert "Audit Log" in response.content.decode() |
|
c588255…
|
ragelink
|
40 |
|
|
c588255…
|
ragelink
|
41 |
def test_audit_log_accessible_to_org_admin(self, org_admin_client): |
|
c588255…
|
ragelink
|
42 |
response = org_admin_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
43 |
assert response.status_code == 200 |
|
c588255…
|
ragelink
|
44 |
|
|
c588255…
|
ragelink
|
45 |
def test_audit_log_denied_for_viewer(self, viewer_client): |
|
c588255…
|
ragelink
|
46 |
response = viewer_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
47 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
48 |
|
|
c588255…
|
ragelink
|
49 |
def test_audit_log_denied_for_no_perm(self, no_perm_client): |
|
c588255…
|
ragelink
|
50 |
response = no_perm_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
51 |
assert response.status_code == 403 |
|
c588255…
|
ragelink
|
52 |
|
|
c588255…
|
ragelink
|
53 |
def test_audit_log_denied_for_anon(self, client): |
|
c588255…
|
ragelink
|
54 |
response = client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
55 |
assert response.status_code == 302 # Redirect to login |
|
c588255…
|
ragelink
|
56 |
|
|
c588255…
|
ragelink
|
57 |
|
|
c588255…
|
ragelink
|
58 |
# --- Content --- |
|
c588255…
|
ragelink
|
59 |
|
|
c588255…
|
ragelink
|
60 |
|
|
c588255…
|
ragelink
|
61 |
@pytest.mark.django_db |
|
c588255…
|
ragelink
|
62 |
class TestAuditLogContent: |
|
c588255…
|
ragelink
|
63 |
def test_shows_project_history(self, admin_client, admin_user, org): |
|
c588255…
|
ragelink
|
64 |
Project.objects.create(name="Audit Test Project", organization=org, created_by=admin_user) |
|
c588255…
|
ragelink
|
65 |
response = admin_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
66 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
67 |
assert "Audit Test Project" in content |
|
c588255…
|
ragelink
|
68 |
assert "Created" in content |
|
c588255…
|
ragelink
|
69 |
|
|
c588255…
|
ragelink
|
70 |
def test_shows_organization_history(self, admin_client, org): |
|
c588255…
|
ragelink
|
71 |
response = admin_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
72 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
73 |
assert "Organization" in content |
|
c588255…
|
ragelink
|
74 |
|
|
c588255…
|
ragelink
|
75 |
def test_shows_team_history(self, admin_client, admin_user, org): |
|
c588255…
|
ragelink
|
76 |
Team.objects.create(name="Audit Test Team", organization=org, created_by=admin_user) |
|
c588255…
|
ragelink
|
77 |
response = admin_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
78 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
79 |
assert "Audit Test Team" in content |
|
c588255…
|
ragelink
|
80 |
|
|
c588255…
|
ragelink
|
81 |
def test_filter_by_model_type(self, admin_client, admin_user, org): |
|
c588255…
|
ragelink
|
82 |
Project.objects.create(name="Filter Test", organization=org, created_by=admin_user) |
|
c588255…
|
ragelink
|
83 |
Team.objects.create(name="Should Not Show", organization=org, created_by=admin_user) |
|
c588255…
|
ragelink
|
84 |
response = admin_client.get("/settings/audit/?model=Project") |
|
c588255…
|
ragelink
|
85 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
86 |
assert "Filter Test" in content |
|
c588255…
|
ragelink
|
87 |
assert "Should Not Show" not in content |
|
c588255…
|
ragelink
|
88 |
|
|
c588255…
|
ragelink
|
89 |
def test_filter_shows_all_when_no_filter(self, admin_client, admin_user, org): |
|
c588255…
|
ragelink
|
90 |
Project.objects.create(name="Project Entry", organization=org, created_by=admin_user) |
|
c588255…
|
ragelink
|
91 |
Team.objects.create(name="Team Entry", organization=org, created_by=admin_user) |
|
c588255…
|
ragelink
|
92 |
response = admin_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
93 |
content = response.content.decode() |
|
c588255…
|
ragelink
|
94 |
assert "Project Entry" in content |
|
c588255…
|
ragelink
|
95 |
assert "Team Entry" in content |
|
c588255…
|
ragelink
|
96 |
|
|
c588255…
|
ragelink
|
97 |
def test_audit_log_entries_sorted_by_date(self, admin_client, admin_user, org): |
|
c588255…
|
ragelink
|
98 |
Project.objects.create(name="First Project", organization=org, created_by=admin_user) |
|
c588255…
|
ragelink
|
99 |
Project.objects.create(name="Second Project", organization=org, created_by=admin_user) |
|
c588255…
|
ragelink
|
100 |
response = admin_client.get("/settings/audit/?model=Project") |
|
c588255…
|
ragelink
|
101 |
entries = response.context["entries"] |
|
c588255…
|
ragelink
|
102 |
# Most recent first |
|
c588255…
|
ragelink
|
103 |
project_entries = [e for e in entries if e["model"] == "Project"] |
|
c588255…
|
ragelink
|
104 |
dates = [e["date"] for e in project_entries] |
|
c588255…
|
ragelink
|
105 |
assert dates == sorted(dates, reverse=True) |
|
c588255…
|
ragelink
|
106 |
|
|
c588255…
|
ragelink
|
107 |
def test_available_models_in_context(self, admin_client): |
|
c588255…
|
ragelink
|
108 |
response = admin_client.get("/settings/audit/") |
|
c588255…
|
ragelink
|
109 |
assert "available_models" in response.context |
|
c588255…
|
ragelink
|
110 |
assert "Project" in response.context["available_models"] |
|
c588255…
|
ragelink
|
111 |
assert "Organization" in response.context["available_models"] |
|
c588255…
|
ragelink
|
112 |
assert "Team" in response.context["available_models"] |
|
c588255…
|
ragelink
|
113 |
assert "FossilRepository" in response.context["available_models"] |
|
c588255…
|
ragelink
|
114 |
|
|
c588255…
|
ragelink
|
115 |
def test_audit_log_sidebar_link_for_superuser(self, admin_client): |
|
c588255…
|
ragelink
|
116 |
response = admin_client.get("/dashboard/") |
|
c588255…
|
ragelink
|
117 |
assert "/settings/audit/" in response.content.decode() |
|
c588255…
|
ragelink
|
118 |
|
|
c588255…
|
ragelink
|
119 |
def test_audit_log_sidebar_link_hidden_for_viewer(self, viewer_client): |
|
c588255…
|
ragelink
|
120 |
response = viewer_client.get("/dashboard/") |
|
c588255…
|
ragelink
|
121 |
assert "/settings/audit/" not in response.content.decode() |