|
1 | 1 | from django.test import TestCase |
2 | 2 |
|
3 | 3 | from nav.models.arnold import Justification |
| 4 | +from nav.models.profiles import Account |
4 | 5 |
|
5 | 6 | from nav.auditlog import find_modelname |
6 | 7 | from nav.auditlog.models import LogEntry |
@@ -86,6 +87,51 @@ def test_find_name(self): |
86 | 87 | name = find_modelname(self.justification) |
87 | 88 | self.assertEqual(name, 'blocked_reason') |
88 | 89 |
|
| 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 | + |
89 | 135 |
|
90 | 136 | class AuditlogUtilsTestCase(TestCase): |
91 | 137 | def setUp(self): |
@@ -118,3 +164,112 @@ def test_get_auditlog_entries(self): |
118 | 164 | self.assertEqual(entries.count(), 1) |
119 | 165 | entries = get_auditlog_entries(modelname=modelname, pks=[justification_1.pk]) |
120 | 166 | 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