Skip to content

Commit 878e419

Browse files
committed
Add tests and changelog for auditlog sortkey columns
1 parent ed5b3f2 commit 878e419

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

changelog.d/3757.changed.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Auditlog entries now store sortable display names for actor, object, and target, enabling sorting and searching on these columns. Links to detail pages are shown when the referenced object still exists.
2+
3+
**Note:** The database migration includes backfill operations to populate sortkey columns for existing log entries. On deployments with millions of log entries, this migration may take some time to complete.

tests/integration/auditlog_test.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.test import TestCase
22

33
from nav.models.arnold import Justification
4+
from nav.models.profiles import Account
45

56
from nav.auditlog import find_modelname
67
from nav.auditlog.models import LogEntry
@@ -86,6 +87,51 @@ def test_find_name(self):
8687
name = find_modelname(self.justification)
8788
self.assertEqual(name, 'blocked_reason')
8889

90+
def test_when_log_entry_created_then_actor_sortkey_is_set(self):
91+
LogEntry.add_log_entry(
92+
self.justification, 'sortkey test', '{actor} did something'
93+
)
94+
log_entry = LogEntry.objects.filter(verb='sortkey test').get()
95+
self.assertEqual(log_entry.actor_sortkey, str(self.justification))
96+
97+
def test_when_log_entry_has_object_then_object_sortkey_is_set(self):
98+
other = Justification.objects.create(name='object_test')
99+
LogEntry.add_log_entry(
100+
self.justification,
101+
'object sortkey test',
102+
'{actor} edited {object}',
103+
object=other,
104+
)
105+
log_entry = LogEntry.objects.filter(verb='object sortkey test').get()
106+
self.assertEqual(log_entry.object_sortkey, str(other))
107+
108+
def test_when_log_entry_has_no_object_then_object_sortkey_is_null(self):
109+
LogEntry.add_log_entry(
110+
self.justification, 'no object test', '{actor} did something'
111+
)
112+
log_entry = LogEntry.objects.filter(verb='no object test').get()
113+
self.assertIsNone(log_entry.object_sortkey)
114+
115+
def test_when_log_entry_has_target_then_target_sortkey_is_set(self):
116+
obj = Justification.objects.create(name='target_obj')
117+
target = Justification.objects.create(name='target_test')
118+
LogEntry.add_log_entry(
119+
self.justification,
120+
'target sortkey test',
121+
'{actor} sent {object} to {target}',
122+
object=obj,
123+
target=target,
124+
)
125+
log_entry = LogEntry.objects.filter(verb='target sortkey test').get()
126+
self.assertEqual(log_entry.target_sortkey, str(target))
127+
128+
def test_when_log_entry_has_no_target_then_target_sortkey_is_null(self):
129+
LogEntry.add_log_entry(
130+
self.justification, 'no target test', '{actor} did something'
131+
)
132+
log_entry = LogEntry.objects.filter(verb='no target test').get()
133+
self.assertIsNone(log_entry.target_sortkey)
134+
89135

90136
class AuditlogUtilsTestCase(TestCase):
91137
def setUp(self):
@@ -118,3 +164,112 @@ def test_get_auditlog_entries(self):
118164
self.assertEqual(entries.count(), 1)
119165
entries = get_auditlog_entries(modelname=modelname, pks=[justification_1.pk])
120166
self.assertEqual(entries.count(), 2)
167+
168+
169+
def test_v1_api_returns_plain_strings_for_backward_compatibility(db, token, api_client):
170+
"""Test that v1 API returns plain strings (backward compatibility)"""
171+
# Configure token to allow access to v1 auditlog endpoint
172+
token.endpoints = {'auditlog': '/auditlog/'}
173+
token.save()
174+
175+
# Create an account that will be the actor
176+
account = Account.objects.create(
177+
login='testuser', name='Test User', password='unused'
178+
)
179+
# Create a justification as the object
180+
justification = Justification.objects.create(name='test_object')
181+
182+
# Create a log entry
183+
entry = LogEntry.add_log_entry(
184+
account,
185+
'test-action',
186+
'{actor} performed action on {object}',
187+
object=justification,
188+
)
189+
190+
# Fetch the entry via v1 API
191+
response = api_client.get(f'/api/1/auditlog/{entry.id}/')
192+
193+
assert response.status_code == 200
194+
data = response.json()
195+
196+
# Verify v1 returns plain strings
197+
assert isinstance(data['actor'], str)
198+
assert data['actor'] == 'testuser'
199+
assert isinstance(data['object'], str)
200+
assert data['object'] == 'test_object'
201+
202+
203+
def test_v2_api_retrieve_returns_entity_objects_with_urls(db, token, api_client):
204+
"""Test that v2 retrieve endpoint returns objects with {name, url}"""
205+
# Configure token to allow access to v2 auditlog endpoint
206+
# Note: TokenPermission.version=1 strips /api/1, so v2 paths become /2/auditlog/
207+
token.endpoints = {'auditlog': '/2/auditlog/'}
208+
token.save()
209+
210+
# Create an account that will be the actor (accounts have get_absolute_url)
211+
account = Account.objects.create(
212+
login='testuser', name='Test User', password='unused'
213+
)
214+
# Create a justification as the object
215+
justification = Justification.objects.create(name='test_object')
216+
217+
# Create a log entry
218+
entry = LogEntry.add_log_entry(
219+
account,
220+
'test-action',
221+
'{actor} performed action on {object}',
222+
object=justification,
223+
)
224+
225+
# Fetch the entry via v2 API retrieve endpoint
226+
response = api_client.get(f'/api/2/auditlog/{entry.id}/')
227+
228+
assert response.status_code == 200
229+
data = response.json()
230+
231+
# Verify actor has both name and url
232+
assert isinstance(data['actor'], dict)
233+
assert data['actor']['name'] == 'testuser'
234+
assert data['actor']['url'] is not None
235+
assert '/useradmin/account/' in data['actor']['url']
236+
237+
# Verify object has name (url may be None if Justification lacks get_absolute_url)
238+
assert isinstance(data['object'], dict)
239+
assert data['object']['name'] == 'test_object'
240+
241+
242+
def test_v2_api_list_returns_entity_objects_with_urls(db, token, api_client):
243+
"""Test that v2 list endpoint returns objects with {name, url}"""
244+
# Configure token to allow access to v2 auditlog endpoint
245+
# Note: TokenPermission.version=1 strips /api/1, so v2 paths become /2/auditlog/
246+
token.endpoints = {'auditlog': '/2/auditlog/'}
247+
token.save()
248+
249+
# Create an account that will be the actor
250+
account = Account.objects.create(
251+
login='testuser', name='Test User', password='unused'
252+
)
253+
# Create a justification as the object
254+
justification = Justification.objects.create(name='test_object')
255+
256+
# Create a log entry
257+
LogEntry.add_log_entry(
258+
account,
259+
'test-action',
260+
'{actor} performed action on {object}',
261+
object=justification,
262+
)
263+
264+
# Fetch entries via v2 API list endpoint
265+
response = api_client.get('/api/2/auditlog/')
266+
267+
assert response.status_code == 200
268+
data = response.json()
269+
assert len(data['results']) > 0
270+
271+
# Check the first entry
272+
first_entry = data['results'][0]
273+
assert isinstance(first_entry['actor'], dict)
274+
assert 'name' in first_entry['actor']
275+
assert 'url' in first_entry['actor']

0 commit comments

Comments
 (0)