django-field-audit 1.2.7__tar.gz → 1.2.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-field-audit might be problematic. Click here for more details.
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/PKG-INFO +12 -3
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/README.md +2 -1
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/django_field_audit.egg-info/PKG-INFO +12 -3
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/django_field_audit.egg-info/SOURCES.txt +9 -1
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/__init__.py +1 -1
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/auditors.py +1 -1
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/models.py +2 -2
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/setup.cfg +3 -1
- django_field_audit-1.2.9/tests/test_apps.py +18 -0
- django_field_audit-1.2.9/tests/test_auditors.py +238 -0
- django_field_audit-1.2.9/tests/test_bootstrap_field_audit_events.py +144 -0
- django_field_audit-1.2.9/tests/test_django_compat.py +148 -0
- django_field_audit-1.2.9/tests/test_field_audit.py +178 -0
- django_field_audit-1.2.9/tests/test_middleware.py +25 -0
- django_field_audit-1.2.9/tests/test_models.py +1424 -0
- django_field_audit-1.2.9/tests/test_utils.py +83 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/LICENSE +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/django_field_audit.egg-info/dependency_links.txt +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/django_field_audit.egg-info/top_level.txt +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/apps.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/const.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/field_audit.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/management/__init__.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/management/commands/__init__.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/management/commands/bootstrap_field_audit_events.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/middleware.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/migrations/0001_initial.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/migrations/0002_add_is_bootstrap_column.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/migrations/__init__.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/field_audit/utils.py +0 -0
- {django-field-audit-1.2.7 → django_field_audit-1.2.9}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: django-field-audit
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.9
|
|
4
4
|
Summary: Audit Field Changes on Django Models
|
|
5
5
|
Home-page: https://github.com/dimagi/django-field-audit
|
|
6
6
|
Maintainer: Joel Miller
|
|
@@ -26,6 +26,14 @@ Classifier: Framework :: Django :: 4.2
|
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
License-File: LICENSE
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: maintainer
|
|
35
|
+
Dynamic: maintainer-email
|
|
36
|
+
Dynamic: summary
|
|
29
37
|
|
|
30
38
|
# Audit Field Changes on Django Models
|
|
31
39
|
|
|
@@ -274,7 +282,8 @@ python example/manage.py makemigrations field_audit
|
|
|
274
282
|
|
|
275
283
|
### Uploading to PyPI
|
|
276
284
|
|
|
277
|
-
|
|
285
|
+
First bump the package version in the `field_audit/__init__.py` file. Then create a changelog entry in the CHANGELOG.md
|
|
286
|
+
file. After these changes are merged, you should tag the main branch with the new version. Then, package and upload the generated files to PyPI.
|
|
278
287
|
|
|
279
288
|
```shell
|
|
280
289
|
pip install -r pkg-requires.txt
|
|
@@ -245,7 +245,8 @@ python example/manage.py makemigrations field_audit
|
|
|
245
245
|
|
|
246
246
|
### Uploading to PyPI
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
First bump the package version in the `field_audit/__init__.py` file. Then create a changelog entry in the CHANGELOG.md
|
|
249
|
+
file. After these changes are merged, you should tag the main branch with the new version. Then, package and upload the generated files to PyPI.
|
|
249
250
|
|
|
250
251
|
```shell
|
|
251
252
|
pip install -r pkg-requires.txt
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: django-field-audit
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.9
|
|
4
4
|
Summary: Audit Field Changes on Django Models
|
|
5
5
|
Home-page: https://github.com/dimagi/django-field-audit
|
|
6
6
|
Maintainer: Joel Miller
|
|
@@ -26,6 +26,14 @@ Classifier: Framework :: Django :: 4.2
|
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
License-File: LICENSE
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: maintainer
|
|
35
|
+
Dynamic: maintainer-email
|
|
36
|
+
Dynamic: summary
|
|
29
37
|
|
|
30
38
|
# Audit Field Changes on Django Models
|
|
31
39
|
|
|
@@ -274,7 +282,8 @@ python example/manage.py makemigrations field_audit
|
|
|
274
282
|
|
|
275
283
|
### Uploading to PyPI
|
|
276
284
|
|
|
277
|
-
|
|
285
|
+
First bump the package version in the `field_audit/__init__.py` file. Then create a changelog entry in the CHANGELOG.md
|
|
286
|
+
file. After these changes are merged, you should tag the main branch with the new version. Then, package and upload the generated files to PyPI.
|
|
278
287
|
|
|
279
288
|
```shell
|
|
280
289
|
pip install -r pkg-requires.txt
|
{django-field-audit-1.2.7 → django_field_audit-1.2.9}/django_field_audit.egg-info/SOURCES.txt
RENAMED
|
@@ -19,4 +19,12 @@ field_audit/management/commands/__init__.py
|
|
|
19
19
|
field_audit/management/commands/bootstrap_field_audit_events.py
|
|
20
20
|
field_audit/migrations/0001_initial.py
|
|
21
21
|
field_audit/migrations/0002_add_is_bootstrap_column.py
|
|
22
|
-
field_audit/migrations/__init__.py
|
|
22
|
+
field_audit/migrations/__init__.py
|
|
23
|
+
tests/test_apps.py
|
|
24
|
+
tests/test_auditors.py
|
|
25
|
+
tests/test_bootstrap_field_audit_events.py
|
|
26
|
+
tests/test_django_compat.py
|
|
27
|
+
tests/test_field_audit.py
|
|
28
|
+
tests/test_middleware.py
|
|
29
|
+
tests/test_models.py
|
|
30
|
+
tests/test_utils.py
|
|
@@ -107,7 +107,7 @@ class SystemUserAuditor(BaseAuditor):
|
|
|
107
107
|
try:
|
|
108
108
|
# get owner of STDIN file on login sessions (e.g. SSH)
|
|
109
109
|
output = check_output(["who", "-m"], stderr=DEVNULL)
|
|
110
|
-
except CalledProcessError:
|
|
110
|
+
except (FileNotFoundError, CalledProcessError):
|
|
111
111
|
self.has_who_bin = False
|
|
112
112
|
else:
|
|
113
113
|
if output:
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
1
|
from enum import Enum
|
|
3
2
|
from functools import wraps
|
|
4
3
|
from itertools import islice
|
|
@@ -6,6 +5,7 @@ from itertools import islice
|
|
|
6
5
|
from django.conf import settings
|
|
7
6
|
from django.db import models, transaction
|
|
8
7
|
from django.db.models import Expression
|
|
8
|
+
from django.utils import timezone
|
|
9
9
|
|
|
10
10
|
from .const import BOOTSTRAP_BATCH_SIZE
|
|
11
11
|
from .utils import class_import_helper
|
|
@@ -197,7 +197,7 @@ def get_date():
|
|
|
197
197
|
This is the "getter" for default values of the ``AuditEvent.event_date``
|
|
198
198
|
field.
|
|
199
199
|
"""
|
|
200
|
-
return
|
|
200
|
+
return timezone.now()
|
|
201
201
|
|
|
202
202
|
|
|
203
203
|
class AuditEvent(models.Model):
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.test import TestCase
|
|
3
|
+
|
|
4
|
+
from field_audit.auditors import audit_dispatcher
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestFieldAuditConfig(TestCase):
|
|
8
|
+
|
|
9
|
+
def test_config_ready_sets_auditors(self):
|
|
10
|
+
# ensure it's not empty already
|
|
11
|
+
self.assertNotEqual([], audit_dispatcher.auditors)
|
|
12
|
+
try:
|
|
13
|
+
audit_dispatcher.auditors = []
|
|
14
|
+
apps.get_app_config("field_audit").ready()
|
|
15
|
+
self.assertNotEqual([], audit_dispatcher.auditors)
|
|
16
|
+
finally:
|
|
17
|
+
# reset to defaults
|
|
18
|
+
audit_dispatcher.setup_auditors()
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from subprocess import CalledProcessError
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.test import TestCase
|
|
6
|
+
from django.test.utils import override_settings
|
|
7
|
+
|
|
8
|
+
from field_audit.auditors import (
|
|
9
|
+
BaseAuditor,
|
|
10
|
+
RequestAuditor,
|
|
11
|
+
SystemUserAuditor,
|
|
12
|
+
audit_dispatcher,
|
|
13
|
+
)
|
|
14
|
+
from field_audit.models import (
|
|
15
|
+
USER_TYPE_PROCESS,
|
|
16
|
+
USER_TYPE_REQUEST,
|
|
17
|
+
USER_TYPE_TTY,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestAuditDispatcher(TestCase):
|
|
22
|
+
|
|
23
|
+
def setUp(self):
|
|
24
|
+
# reset the auditor chain to default values
|
|
25
|
+
audit_dispatcher.setup_auditors()
|
|
26
|
+
|
|
27
|
+
def test_audit_dispatcher_default_auditor_chain(self):
|
|
28
|
+
with self.assertRaises(AttributeError):
|
|
29
|
+
getattr(settings, "FIELD_AUDIT_AUDITORS")
|
|
30
|
+
request_auditor, sysuser_auditor = audit_dispatcher.auditors
|
|
31
|
+
self.assertIsInstance(request_auditor, RequestAuditor)
|
|
32
|
+
self.assertIsInstance(sysuser_auditor, SystemUserAuditor)
|
|
33
|
+
|
|
34
|
+
@override_settings(
|
|
35
|
+
FIELD_AUDIT_AUDITORS=[
|
|
36
|
+
"tests.test_auditors.CustomAuditor1",
|
|
37
|
+
"tests.test_auditors.CustomAuditor2",
|
|
38
|
+
"tests.test_auditors.CustomAuditor3",
|
|
39
|
+
],
|
|
40
|
+
)
|
|
41
|
+
def test_audit_dispatcher_auditor_chain_is_configurable(self):
|
|
42
|
+
audit_dispatcher.setup_auditors()
|
|
43
|
+
one, two, three = audit_dispatcher.auditors
|
|
44
|
+
self.assertIsInstance(one, CustomAuditor1)
|
|
45
|
+
self.assertIsInstance(two, CustomAuditor2)
|
|
46
|
+
self.assertIsInstance(three, CustomAuditor3)
|
|
47
|
+
|
|
48
|
+
@override_settings(FIELD_AUDIT_AUDITORS=[])
|
|
49
|
+
def test_audit_dispatcher_setting_empty_auditor_chain_clears_auditors(self):
|
|
50
|
+
audit_dispatcher.setup_auditors()
|
|
51
|
+
self.assertEqual([], audit_dispatcher.auditors)
|
|
52
|
+
|
|
53
|
+
@override_settings(FIELD_AUDIT_AUDITORS=["tests.models.Flight"])
|
|
54
|
+
def test_audit_dispatcher_custom_auditors_must_subclass_baseauditor(self):
|
|
55
|
+
from .models import Flight
|
|
56
|
+
self.assertFalse(issubclass(Flight, BaseAuditor))
|
|
57
|
+
with self.assertRaises(ValueError):
|
|
58
|
+
audit_dispatcher.setup_auditors()
|
|
59
|
+
|
|
60
|
+
def test_audit_dispatcher_chain(self):
|
|
61
|
+
aud1 = MockAuditor(True)
|
|
62
|
+
aud2 = MockAuditor(True)
|
|
63
|
+
chain = [aud1, aud2]
|
|
64
|
+
|
|
65
|
+
# aud1 hits, aud2 never called
|
|
66
|
+
with patch.object(audit_dispatcher, "auditors", chain):
|
|
67
|
+
change_context = audit_dispatcher.dispatch(object())
|
|
68
|
+
self.assertIs(change_context, aud1)
|
|
69
|
+
self.assertEqual(aud1.dispatched, 1)
|
|
70
|
+
self.assertEqual(aud2.dispatched, 0)
|
|
71
|
+
|
|
72
|
+
# aud1 misses, aud2 hits
|
|
73
|
+
aud1.reset(False)
|
|
74
|
+
with patch.object(audit_dispatcher, "auditors", chain):
|
|
75
|
+
change_context = audit_dispatcher.dispatch(object())
|
|
76
|
+
self.assertIs(change_context, aud2)
|
|
77
|
+
self.assertEqual(aud1.dispatched, 1)
|
|
78
|
+
self.assertEqual(aud2.dispatched, 1)
|
|
79
|
+
|
|
80
|
+
# both miss
|
|
81
|
+
aud1.reset(False)
|
|
82
|
+
aud2.reset(False)
|
|
83
|
+
with patch.object(audit_dispatcher, "auditors", chain):
|
|
84
|
+
change_context = audit_dispatcher.dispatch(object())
|
|
85
|
+
self.assertIsNone(change_context)
|
|
86
|
+
self.assertEqual(aud1.dispatched, 1)
|
|
87
|
+
self.assertEqual(aud2.dispatched, 1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CustomAuditor1(BaseAuditor):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CustomAuditor2(BaseAuditor):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CustomAuditor3(BaseAuditor):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MockAuditor:
|
|
103
|
+
|
|
104
|
+
def __init__(self, enabled):
|
|
105
|
+
self.reset(enabled)
|
|
106
|
+
|
|
107
|
+
def reset(self, enabled):
|
|
108
|
+
self.dispatched = 0
|
|
109
|
+
self.enabled = enabled
|
|
110
|
+
|
|
111
|
+
def change_context(self, request):
|
|
112
|
+
self.dispatched += 1
|
|
113
|
+
return self if self.enabled else None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestBaseAuditor(TestCase):
|
|
117
|
+
|
|
118
|
+
def test_baseauditor_change_context_raises_notimplementederror(self):
|
|
119
|
+
with self.assertRaises(NotImplementedError):
|
|
120
|
+
BaseAuditor().change_context(object())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestSystemUserAuditor(TestCase):
|
|
124
|
+
|
|
125
|
+
def setUp(self):
|
|
126
|
+
super().setUp()
|
|
127
|
+
self.auditor = SystemUserAuditor()
|
|
128
|
+
|
|
129
|
+
def test_systemuserauditor_change_context_returns_sys_value_for_request(self): # noqa: E501
|
|
130
|
+
def user(*args, **kw):
|
|
131
|
+
return b"test ..."
|
|
132
|
+
with patch("field_audit.auditors.check_output", side_effect=user):
|
|
133
|
+
self.assertEqual(
|
|
134
|
+
{"user_type": USER_TYPE_TTY, "username": "test"},
|
|
135
|
+
self.auditor.change_context(object()),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _patch_system_getters_and_validate(self, fake_output, change_context):
|
|
139
|
+
kwargs = {"side_effect": fake_output}
|
|
140
|
+
user = None if change_context is None else change_context["username"]
|
|
141
|
+
with (
|
|
142
|
+
patch("field_audit.auditors.check_output", **kwargs) as chk_out,
|
|
143
|
+
patch("field_audit.auditors.getuser", return_value=user) as getuser,
|
|
144
|
+
):
|
|
145
|
+
audit_info = self.auditor.change_context(None)
|
|
146
|
+
if isinstance(audit_info, dict):
|
|
147
|
+
self.assertEqual(change_context, audit_info)
|
|
148
|
+
else:
|
|
149
|
+
self.assertIsNone(audit_info)
|
|
150
|
+
return chk_out, getuser
|
|
151
|
+
|
|
152
|
+
def test_systemuserauditor_change_context_tolerates_invalid_who_output(self): # noqa: E501
|
|
153
|
+
def bogus(*args, **kw):
|
|
154
|
+
return b"\xb4"
|
|
155
|
+
ch_by = {"user_type": USER_TYPE_PROCESS, "username": "alice"}
|
|
156
|
+
chk_out, getuser = self._patch_system_getters_and_validate(bogus, ch_by)
|
|
157
|
+
chk_out.assert_called_once()
|
|
158
|
+
getuser.assert_called_once()
|
|
159
|
+
|
|
160
|
+
def test_systemuserauditor_change_context_tolerates_empty_who_output(self):
|
|
161
|
+
def empty(*args, **kw):
|
|
162
|
+
return b""
|
|
163
|
+
ch_by = {"user_type": USER_TYPE_PROCESS, "username": "bob"}
|
|
164
|
+
chk_out, getuser = self._patch_system_getters_and_validate(empty, ch_by)
|
|
165
|
+
chk_out.assert_called_once()
|
|
166
|
+
getuser.assert_called_once()
|
|
167
|
+
|
|
168
|
+
def test_systemuserauditor_remembers_who_error(self):
|
|
169
|
+
def fail(*args, **kw):
|
|
170
|
+
raise CalledProcessError(1, [], "")
|
|
171
|
+
ch_by = {"user_type": USER_TYPE_PROCESS, "username": "carlos"}
|
|
172
|
+
# round 1
|
|
173
|
+
chk_out, getuser = self._patch_system_getters_and_validate(fail, ch_by)
|
|
174
|
+
chk_out.assert_called_once()
|
|
175
|
+
getuser.assert_called_once()
|
|
176
|
+
# round 2
|
|
177
|
+
chk_out, getuser = self._patch_system_getters_and_validate(fail, ch_by)
|
|
178
|
+
chk_out.assert_not_called()
|
|
179
|
+
getuser.assert_called_once()
|
|
180
|
+
|
|
181
|
+
def test_systemuserauditor_handles_missing_who_bin(self):
|
|
182
|
+
def fail(*args, **kw):
|
|
183
|
+
raise FileNotFoundError()
|
|
184
|
+
ch_by = {"user_type": USER_TYPE_PROCESS, "username": "carlos"}
|
|
185
|
+
chk_out, getuser = self._patch_system_getters_and_validate(fail, ch_by)
|
|
186
|
+
chk_out.assert_called_once()
|
|
187
|
+
getuser.assert_called_once()
|
|
188
|
+
|
|
189
|
+
def test_systemuserauditor_change_context_returns_tty_user_on_who_output(self): # noqa: E501
|
|
190
|
+
def mock(*args, **kw):
|
|
191
|
+
return b"eve ttys000 May 8 17:04 (localhost)"
|
|
192
|
+
ch_by = {"user_type": USER_TYPE_TTY, "username": "eve"}
|
|
193
|
+
chk_out, getuser = self._patch_system_getters_and_validate(mock, ch_by)
|
|
194
|
+
chk_out.assert_called_once()
|
|
195
|
+
getuser.assert_not_called()
|
|
196
|
+
|
|
197
|
+
def test_systemuserauditor_change_context_returns_none_if_all_else_fails(self): # noqa: E501
|
|
198
|
+
def empty(*args, **kw):
|
|
199
|
+
return b""
|
|
200
|
+
chk_out, getuser = self._patch_system_getters_and_validate(empty, None)
|
|
201
|
+
chk_out.assert_called_once()
|
|
202
|
+
getuser.assert_called_once()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TestRequestAuditor(TestCase):
|
|
206
|
+
|
|
207
|
+
def setUp(self):
|
|
208
|
+
self.request = AuthedRequest()
|
|
209
|
+
self.auditor = RequestAuditor()
|
|
210
|
+
|
|
211
|
+
def test_requestauditor_change_context(self):
|
|
212
|
+
self.assertEqual(
|
|
213
|
+
{
|
|
214
|
+
"user_type": USER_TYPE_REQUEST,
|
|
215
|
+
"username": self.request.user.username,
|
|
216
|
+
},
|
|
217
|
+
self.auditor.change_context(self.request),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def test_requestauditor_change_context_returns_none_without_request(self):
|
|
221
|
+
self.assertIsNone(self.auditor.change_context(None))
|
|
222
|
+
|
|
223
|
+
def test_requestauditor_change_context_returns_value_for_unauthorized_req(self): # noqa: E501
|
|
224
|
+
self.request.deauth()
|
|
225
|
+
self.assertEqual({}, self.auditor.change_context(self.request))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class AuthedRequest:
|
|
229
|
+
|
|
230
|
+
class User:
|
|
231
|
+
username = "test@example.com"
|
|
232
|
+
is_authenticated = True
|
|
233
|
+
|
|
234
|
+
def __init__(self):
|
|
235
|
+
self.user = self.User()
|
|
236
|
+
|
|
237
|
+
def deauth(self):
|
|
238
|
+
self.user.is_authenticated = False
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from io import StringIO
|
|
3
|
+
from unittest.mock import ANY, patch
|
|
4
|
+
|
|
5
|
+
from django.core.management import CommandError, call_command
|
|
6
|
+
from django.db import models
|
|
7
|
+
from django.test import TestCase
|
|
8
|
+
|
|
9
|
+
from field_audit.const import BOOTSTRAP_BATCH_SIZE
|
|
10
|
+
from field_audit.field_audit import get_audited_models
|
|
11
|
+
from field_audit.management.commands import (
|
|
12
|
+
bootstrap_field_audit_events as bootstrap,
|
|
13
|
+
)
|
|
14
|
+
from field_audit.models import AuditEvent
|
|
15
|
+
from tests.models import PkAuto
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@contextmanager
|
|
19
|
+
def restore_command_models(command_class):
|
|
20
|
+
backup = command_class.models
|
|
21
|
+
yield
|
|
22
|
+
command_class.models = backup
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestCommand(TestCase):
|
|
26
|
+
|
|
27
|
+
command = "bootstrap_field_audit_events"
|
|
28
|
+
|
|
29
|
+
def setUp(self):
|
|
30
|
+
super().setUp()
|
|
31
|
+
# create some records for bootstrapping
|
|
32
|
+
for _ in range(2):
|
|
33
|
+
PkAuto.objects.create()
|
|
34
|
+
# clear the "create" audit events
|
|
35
|
+
AuditEvent.objects.all().delete()
|
|
36
|
+
|
|
37
|
+
def test_setup_models_populates_models_dict(self):
|
|
38
|
+
with restore_command_models(bootstrap.Command):
|
|
39
|
+
bootstrap.Command.models = {}
|
|
40
|
+
bootstrap.Command.setup_models()
|
|
41
|
+
self.assertEqual(
|
|
42
|
+
set(get_audited_models()),
|
|
43
|
+
set(bootstrap.Command.models.values()),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def test_setup_models_uses_model_name(self):
|
|
47
|
+
with restore_command_models(bootstrap.Command):
|
|
48
|
+
bootstrap.Command.models = {}
|
|
49
|
+
bootstrap.Command.setup_models()
|
|
50
|
+
self.assertIs(PkAuto, bootstrap.Command.models["PkAuto"])
|
|
51
|
+
|
|
52
|
+
def test_setup_models_uses_app_name_to_prevent_model_name_collisions(self):
|
|
53
|
+
with restore_command_models(bootstrap.Command):
|
|
54
|
+
class ModelX(models.Model):
|
|
55
|
+
pass
|
|
56
|
+
bootstrap.Command.models = {"PkAuto": ModelX}
|
|
57
|
+
patch_kw = {"return_value": {PkAuto: "cls"}}
|
|
58
|
+
with patch.object(bootstrap, "get_audited_models", **patch_kw):
|
|
59
|
+
bootstrap.Command.setup_models()
|
|
60
|
+
self.assertEqual(
|
|
61
|
+
{"tests.ModelX": ModelX, "tests.PkAuto": PkAuto},
|
|
62
|
+
bootstrap.Command.models,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def test_setup_models_crashes_on_verbose_model_name_collision(self):
|
|
66
|
+
|
|
67
|
+
class ModelY(models.Model):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
class ModelZ(models.Model):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
ModelZ.__name__ = ModelY.__name__
|
|
74
|
+
with restore_command_models(bootstrap.Command):
|
|
75
|
+
patch_kw = {"return_value": {ModelY: "y", ModelZ: "z"}}
|
|
76
|
+
with (
|
|
77
|
+
patch.object(bootstrap, "get_audited_models", **patch_kw),
|
|
78
|
+
self.assertRaises(bootstrap.InvalidModelState),
|
|
79
|
+
):
|
|
80
|
+
bootstrap.Command.setup_models()
|
|
81
|
+
|
|
82
|
+
def test_bootstrap_uses_default_batch_size(self):
|
|
83
|
+
with patch.object(AuditEvent, "bootstrap_top_up") as mock:
|
|
84
|
+
self.quiet_command("top-up", "PkAuto")
|
|
85
|
+
mock.assert_called_once_with(
|
|
86
|
+
PkAuto, ANY, batch_size=BOOTSTRAP_BATCH_SIZE,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_bootstrap_allows_custom_batch_size(self):
|
|
90
|
+
with patch.object(AuditEvent, "bootstrap_top_up") as mock:
|
|
91
|
+
self.quiet_command("top-up", "--batch-size", "1", "PkAuto")
|
|
92
|
+
mock.assert_called_once_with(PkAuto, ANY, batch_size=1)
|
|
93
|
+
|
|
94
|
+
def test_bootstrap_disables_batching_for_batch_size_zero(self):
|
|
95
|
+
with patch.object(AuditEvent, "bootstrap_top_up") as mock:
|
|
96
|
+
self.quiet_command("top-up", "--batch-size", "0", "PkAuto")
|
|
97
|
+
mock.assert_called_once_with(PkAuto, ANY, batch_size=None)
|
|
98
|
+
|
|
99
|
+
def test_bootstrap_crashes_for_negative_batch_size(self):
|
|
100
|
+
with self.assertRaises(CommandError):
|
|
101
|
+
self.quiet_command("top-up", "--batch-size", "-1", "PkAuto")
|
|
102
|
+
|
|
103
|
+
def test_bootstrap_crashes_early_if_model_has_invalid_fields(self):
|
|
104
|
+
with (
|
|
105
|
+
patch.object(PkAuto, AuditEvent.ATTACH_FIELD_NAMES_AT, []),
|
|
106
|
+
self.assertRaises(CommandError),
|
|
107
|
+
):
|
|
108
|
+
self.quiet_command("init", "PkAuto")
|
|
109
|
+
|
|
110
|
+
def test_bootstrap_init_creates_audit_events_for_all_model_records(self):
|
|
111
|
+
self.quiet_command("init", "PkAuto")
|
|
112
|
+
self.assertEqual(
|
|
113
|
+
set(PkAuto.objects.all().values_list("pk", flat=True)),
|
|
114
|
+
set(
|
|
115
|
+
AuditEvent.objects.by_model(PkAuto)
|
|
116
|
+
.filter(is_bootstrap=True)
|
|
117
|
+
.values_list("object_pk", flat=True)
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def test_bootstrap_top_up_creates_audit_events_for_some_model_records(self):
|
|
122
|
+
need_bootstrap = set(PkAuto.objects.all().values_list("pk", flat=True))
|
|
123
|
+
# generate two more models (generates two "create" audit events)
|
|
124
|
+
for _ in range(2):
|
|
125
|
+
PkAuto.objects.create()
|
|
126
|
+
# convert one of them to a bootstrap event
|
|
127
|
+
event = AuditEvent.objects.first()
|
|
128
|
+
event.is_create = False
|
|
129
|
+
event.is_bootstrap = True
|
|
130
|
+
event.save()
|
|
131
|
+
pre_top_up_event_ids = set(
|
|
132
|
+
AuditEvent.objects.all().values_list("id", flat=True)
|
|
133
|
+
)
|
|
134
|
+
self.quiet_command("top-up", "PkAuto")
|
|
135
|
+
self.assertEqual(
|
|
136
|
+
need_bootstrap,
|
|
137
|
+
set(
|
|
138
|
+
AuditEvent.objects.exclude(id__in=pre_top_up_event_ids)
|
|
139
|
+
.values_list("object_pk", flat=True)
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def quiet_command(self, *args, **kw):
|
|
144
|
+
return call_command(self.command, *args, stdout=StringIO(), **kw)
|