arthexis 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl
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 arthexis might be problematic. Click here for more details.
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/METADATA +87 -6
- arthexis-0.1.10.dist-info/RECORD +95 -0
- arthexis-0.1.10.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +352 -37
- config/urls.py +71 -6
- core/admin.py +1601 -200
- core/admin_history.py +50 -0
- core/admindocs.py +108 -1
- core/apps.py +161 -3
- core/auto_upgrade.py +57 -0
- core/backends.py +123 -7
- core/entity.py +62 -48
- core/fields.py +98 -0
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1279 -267
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/reference_utils.py +97 -0
- core/release.py +27 -20
- core/sigil_builder.py +144 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +162 -29
- core/tasks.py +269 -27
- core/test_system_info.py +59 -1
- core/tests.py +644 -73
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +425 -168
- core/views.py +627 -59
- core/widgets.py +51 -0
- core/workgroup_urls.py +7 -3
- core/workgroup_views.py +43 -6
- nodes/actions.py +0 -2
- nodes/admin.py +168 -285
- nodes/apps.py +9 -15
- nodes/backends.py +145 -0
- nodes/lcd.py +24 -10
- nodes/models.py +579 -179
- nodes/tasks.py +1 -5
- nodes/tests.py +894 -130
- nodes/utils.py +13 -2
- nodes/views.py +204 -28
- ocpp/admin.py +212 -63
- ocpp/apps.py +1 -1
- ocpp/consumers.py +642 -68
- ocpp/evcs.py +30 -10
- ocpp/models.py +452 -70
- ocpp/simulator.py +75 -11
- ocpp/store.py +288 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1576 -137
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +701 -123
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +39 -6
- pages/forms.py +131 -0
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +1182 -42
- pages/urls.py +4 -0
- pages/utils.py +0 -1
- pages/views.py +844 -51
- arthexis-0.1.8.dist-info/RECORD +0 -80
- arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
core/tests.py
CHANGED
|
@@ -2,42 +2,59 @@ import os
|
|
|
2
2
|
|
|
3
3
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
4
4
|
import django
|
|
5
|
+
|
|
5
6
|
django.setup()
|
|
6
7
|
|
|
7
|
-
from django.test import Client, TestCase, RequestFactory
|
|
8
|
+
from django.test import Client, TestCase, RequestFactory, override_settings
|
|
8
9
|
from django.urls import reverse
|
|
9
10
|
from django.http import HttpRequest
|
|
10
11
|
import json
|
|
12
|
+
from decimal import Decimal
|
|
11
13
|
from unittest import mock
|
|
14
|
+
from unittest.mock import patch
|
|
12
15
|
from pathlib import Path
|
|
13
16
|
import subprocess
|
|
17
|
+
from glob import glob
|
|
18
|
+
from datetime import timedelta
|
|
19
|
+
import tempfile
|
|
20
|
+
from urllib.parse import quote
|
|
14
21
|
|
|
15
22
|
from django.utils import timezone
|
|
23
|
+
from django.contrib.auth.models import Permission
|
|
24
|
+
from django.contrib.messages import get_messages
|
|
16
25
|
from .models import (
|
|
17
26
|
User,
|
|
27
|
+
UserPhoneNumber,
|
|
18
28
|
EnergyAccount,
|
|
19
29
|
ElectricVehicle,
|
|
20
30
|
EnergyCredit,
|
|
21
|
-
Address,
|
|
22
31
|
Product,
|
|
23
|
-
Subscription,
|
|
24
32
|
Brand,
|
|
25
33
|
EVModel,
|
|
26
34
|
RFID,
|
|
27
|
-
FediverseProfile,
|
|
28
35
|
SecurityGroup,
|
|
29
36
|
Package,
|
|
30
37
|
PackageRelease,
|
|
38
|
+
ReleaseManager,
|
|
39
|
+
Todo,
|
|
40
|
+
PublicWifiAccess,
|
|
31
41
|
)
|
|
32
42
|
from django.contrib.admin.sites import AdminSite
|
|
33
|
-
from core.admin import
|
|
43
|
+
from core.admin import (
|
|
44
|
+
PackageReleaseAdmin,
|
|
45
|
+
PackageAdmin,
|
|
46
|
+
UserAdmin,
|
|
47
|
+
USER_PROFILE_INLINES,
|
|
48
|
+
)
|
|
34
49
|
from ocpp.models import Transaction, Charger
|
|
35
50
|
|
|
36
51
|
from django.core.exceptions import ValidationError
|
|
37
52
|
from django.core.management import call_command
|
|
38
53
|
from django.db import IntegrityError
|
|
39
54
|
from .backends import LocalhostAdminBackend
|
|
40
|
-
from core.views import
|
|
55
|
+
from core.views import _step_check_version, _step_promote_build, _step_publish
|
|
56
|
+
from core import views as core_views
|
|
57
|
+
from core import public_wifi
|
|
41
58
|
|
|
42
59
|
|
|
43
60
|
class DefaultAdminTests(TestCase):
|
|
@@ -78,6 +95,205 @@ class DefaultAdminTests(TestCase):
|
|
|
78
95
|
)
|
|
79
96
|
|
|
80
97
|
|
|
98
|
+
class UserOperateAsTests(TestCase):
|
|
99
|
+
@classmethod
|
|
100
|
+
def setUpTestData(cls):
|
|
101
|
+
cls.permission = Permission.objects.get(codename="view_todo")
|
|
102
|
+
|
|
103
|
+
def test_staff_user_delegates_permissions(self):
|
|
104
|
+
delegate = User.objects.create_user(username="delegate", password="secret")
|
|
105
|
+
delegate.user_permissions.add(self.permission)
|
|
106
|
+
operator = User.objects.create_user(
|
|
107
|
+
username="operator", password="secret", is_staff=True
|
|
108
|
+
)
|
|
109
|
+
self.assertFalse(operator.has_perm("core.view_todo"))
|
|
110
|
+
operator.operate_as = delegate
|
|
111
|
+
operator.full_clean()
|
|
112
|
+
operator.save()
|
|
113
|
+
operator.refresh_from_db()
|
|
114
|
+
self.assertTrue(operator.has_perm("core.view_todo"))
|
|
115
|
+
|
|
116
|
+
def test_only_staff_may_operate_as(self):
|
|
117
|
+
delegate = User.objects.create_user(username="delegate", password="secret")
|
|
118
|
+
operator = User.objects.create_user(username="operator", password="secret")
|
|
119
|
+
operator.operate_as = delegate
|
|
120
|
+
with self.assertRaises(ValidationError):
|
|
121
|
+
operator.full_clean()
|
|
122
|
+
|
|
123
|
+
def test_non_superuser_cannot_operate_as_staff(self):
|
|
124
|
+
staff_delegate = User.objects.create_user(
|
|
125
|
+
username="delegate", password="secret", is_staff=True
|
|
126
|
+
)
|
|
127
|
+
operator = User.objects.create_user(
|
|
128
|
+
username="operator", password="secret", is_staff=True
|
|
129
|
+
)
|
|
130
|
+
operator.operate_as = staff_delegate
|
|
131
|
+
with self.assertRaises(ValidationError):
|
|
132
|
+
operator.full_clean()
|
|
133
|
+
|
|
134
|
+
def test_recursive_chain_and_cycle_detection(self):
|
|
135
|
+
base = User.objects.create_user(username="base", password="secret")
|
|
136
|
+
base.user_permissions.add(self.permission)
|
|
137
|
+
middle = User.objects.create_user(
|
|
138
|
+
username="middle", password="secret", is_staff=True
|
|
139
|
+
)
|
|
140
|
+
middle.operate_as = base
|
|
141
|
+
middle.full_clean()
|
|
142
|
+
middle.save()
|
|
143
|
+
top = User.objects.create_superuser(
|
|
144
|
+
username="top", email="top@example.com", password="secret"
|
|
145
|
+
)
|
|
146
|
+
top.operate_as = middle
|
|
147
|
+
top.full_clean()
|
|
148
|
+
top.save()
|
|
149
|
+
top.refresh_from_db()
|
|
150
|
+
self.assertTrue(top.has_perm("core.view_todo"))
|
|
151
|
+
|
|
152
|
+
first = User.objects.create_superuser(
|
|
153
|
+
username="first", email="first@example.com", password="secret"
|
|
154
|
+
)
|
|
155
|
+
second = User.objects.create_superuser(
|
|
156
|
+
username="second", email="second@example.com", password="secret"
|
|
157
|
+
)
|
|
158
|
+
first.operate_as = second
|
|
159
|
+
first.full_clean()
|
|
160
|
+
first.save()
|
|
161
|
+
second.operate_as = first
|
|
162
|
+
second.full_clean()
|
|
163
|
+
second.save()
|
|
164
|
+
self.assertFalse(first._check_operate_as_chain(lambda user: False))
|
|
165
|
+
|
|
166
|
+
def test_module_permissions_fall_back(self):
|
|
167
|
+
delegate = User.objects.create_user(username="helper", password="secret")
|
|
168
|
+
delegate.user_permissions.add(self.permission)
|
|
169
|
+
operator = User.objects.create_user(
|
|
170
|
+
username="mod", password="secret", is_staff=True
|
|
171
|
+
)
|
|
172
|
+
operator.operate_as = delegate
|
|
173
|
+
operator.full_clean()
|
|
174
|
+
operator.save()
|
|
175
|
+
self.assertTrue(operator.has_module_perms("core"))
|
|
176
|
+
|
|
177
|
+
def test_has_profile_via_delegate(self):
|
|
178
|
+
delegate = User.objects.create_user(
|
|
179
|
+
username="delegate", password="secret", is_staff=True
|
|
180
|
+
)
|
|
181
|
+
ReleaseManager.objects.create(user=delegate)
|
|
182
|
+
operator = User.objects.create_superuser(
|
|
183
|
+
username="operator",
|
|
184
|
+
email="operator@example.com",
|
|
185
|
+
password="secret",
|
|
186
|
+
)
|
|
187
|
+
operator.operate_as = delegate
|
|
188
|
+
operator.full_clean()
|
|
189
|
+
operator.save()
|
|
190
|
+
profile = operator.get_profile(ReleaseManager)
|
|
191
|
+
self.assertIsNotNone(profile)
|
|
192
|
+
self.assertEqual(profile.user, delegate)
|
|
193
|
+
self.assertTrue(operator.has_profile(ReleaseManager))
|
|
194
|
+
|
|
195
|
+
def test_has_profile_via_group_membership(self):
|
|
196
|
+
member = User.objects.create_user(username="member", password="secret")
|
|
197
|
+
group = SecurityGroup.objects.create(name="Managers")
|
|
198
|
+
group.user_set.add(member)
|
|
199
|
+
profile = ReleaseManager.objects.create(group=group)
|
|
200
|
+
self.assertEqual(member.get_profile(ReleaseManager), profile)
|
|
201
|
+
self.assertTrue(member.has_profile(ReleaseManager))
|
|
202
|
+
|
|
203
|
+
def test_release_manager_property_uses_delegate_profile(self):
|
|
204
|
+
delegate = User.objects.create_user(
|
|
205
|
+
username="delegate-property", password="secret", is_staff=True
|
|
206
|
+
)
|
|
207
|
+
profile = ReleaseManager.objects.create(user=delegate)
|
|
208
|
+
operator = User.objects.create_superuser(
|
|
209
|
+
username="operator-property",
|
|
210
|
+
email="operator-property@example.com",
|
|
211
|
+
password="secret",
|
|
212
|
+
)
|
|
213
|
+
operator.operate_as = delegate
|
|
214
|
+
operator.full_clean()
|
|
215
|
+
operator.save()
|
|
216
|
+
self.assertEqual(operator.release_manager, profile)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class UserPhoneNumberTests(TestCase):
|
|
220
|
+
def test_get_phone_numbers_by_priority(self):
|
|
221
|
+
user = User.objects.create_user(username="phone-user", password="secret")
|
|
222
|
+
later = UserPhoneNumber.objects.create(
|
|
223
|
+
user=user, number="+15555550101", priority=10
|
|
224
|
+
)
|
|
225
|
+
earlier = UserPhoneNumber.objects.create(
|
|
226
|
+
user=user, number="+15555550100", priority=1
|
|
227
|
+
)
|
|
228
|
+
immediate = UserPhoneNumber.objects.create(
|
|
229
|
+
user=user, number="+15555550099", priority=0
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
phones = user.get_phones_by_priority()
|
|
233
|
+
self.assertEqual(phones, [immediate, earlier, later])
|
|
234
|
+
|
|
235
|
+
def test_get_phone_numbers_by_priority_orders_by_id_when_equal(self):
|
|
236
|
+
user = User.objects.create_user(username="phone-order", password="secret")
|
|
237
|
+
first = UserPhoneNumber.objects.create(
|
|
238
|
+
user=user, number="+19995550000", priority=0
|
|
239
|
+
)
|
|
240
|
+
second = UserPhoneNumber.objects.create(
|
|
241
|
+
user=user, number="+19995550001", priority=0
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
phones = user.get_phones_by_priority()
|
|
245
|
+
self.assertEqual(phones, [first, second])
|
|
246
|
+
|
|
247
|
+
def test_get_phone_numbers_by_priority_alias(self):
|
|
248
|
+
user = User.objects.create_user(username="phone-alias", password="secret")
|
|
249
|
+
phone = UserPhoneNumber.objects.create(
|
|
250
|
+
user=user, number="+14445550000", priority=3
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
self.assertEqual(user.get_phone_numbers_by_priority(), [phone])
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class ProfileValidationTests(TestCase):
|
|
257
|
+
def test_system_user_cannot_receive_profiles(self):
|
|
258
|
+
system_user = User.objects.get(username=User.SYSTEM_USERNAME)
|
|
259
|
+
profile = ReleaseManager(user=system_user)
|
|
260
|
+
with self.assertRaises(ValidationError) as exc:
|
|
261
|
+
profile.full_clean()
|
|
262
|
+
self.assertIn("user", exc.exception.error_dict)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class UserAdminInlineTests(TestCase):
|
|
266
|
+
def setUp(self):
|
|
267
|
+
self.site = AdminSite()
|
|
268
|
+
self.factory = RequestFactory()
|
|
269
|
+
self.admin = UserAdmin(User, self.site)
|
|
270
|
+
self.system_user = User.objects.get(username=User.SYSTEM_USERNAME)
|
|
271
|
+
self.superuser = User.objects.create_superuser(
|
|
272
|
+
username="inline-super",
|
|
273
|
+
email="inline-super@example.com",
|
|
274
|
+
password="secret",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def test_profile_inlines_hidden_for_system_user(self):
|
|
278
|
+
request = self.factory.get("/")
|
|
279
|
+
request.user = self.superuser
|
|
280
|
+
system_inlines = self.admin.get_inline_instances(request, self.system_user)
|
|
281
|
+
system_profiles = [
|
|
282
|
+
inline
|
|
283
|
+
for inline in system_inlines
|
|
284
|
+
if inline.__class__ in USER_PROFILE_INLINES
|
|
285
|
+
]
|
|
286
|
+
self.assertFalse(system_profiles)
|
|
287
|
+
|
|
288
|
+
other_inlines = self.admin.get_inline_instances(request, self.superuser)
|
|
289
|
+
other_profiles = [
|
|
290
|
+
inline
|
|
291
|
+
for inline in other_inlines
|
|
292
|
+
if inline.__class__ in USER_PROFILE_INLINES
|
|
293
|
+
]
|
|
294
|
+
self.assertEqual(len(other_profiles), len(USER_PROFILE_INLINES))
|
|
295
|
+
|
|
296
|
+
|
|
81
297
|
class RFIDLoginTests(TestCase):
|
|
82
298
|
def setUp(self):
|
|
83
299
|
self.client = Client()
|
|
@@ -112,7 +328,7 @@ class RFIDBatchApiTests(TestCase):
|
|
|
112
328
|
self.client.force_login(self.user)
|
|
113
329
|
|
|
114
330
|
def test_export_rfids(self):
|
|
115
|
-
tag_black = RFID.objects.create(rfid="CARD999")
|
|
331
|
+
tag_black = RFID.objects.create(rfid="CARD999", custom_label="Main Tag")
|
|
116
332
|
tag_white = RFID.objects.create(rfid="CARD998", color=RFID.WHITE)
|
|
117
333
|
self.account.rfids.add(tag_black, tag_white)
|
|
118
334
|
response = self.client.get(reverse("rfid-batch"))
|
|
@@ -123,6 +339,7 @@ class RFIDBatchApiTests(TestCase):
|
|
|
123
339
|
"rfids": [
|
|
124
340
|
{
|
|
125
341
|
"rfid": "CARD999",
|
|
342
|
+
"custom_label": "Main Tag",
|
|
126
343
|
"energy_accounts": [self.account.id],
|
|
127
344
|
"allowed": True,
|
|
128
345
|
"color": "B",
|
|
@@ -141,6 +358,7 @@ class RFIDBatchApiTests(TestCase):
|
|
|
141
358
|
"rfids": [
|
|
142
359
|
{
|
|
143
360
|
"rfid": "CARD111",
|
|
361
|
+
"custom_label": "",
|
|
144
362
|
"energy_accounts": [],
|
|
145
363
|
"allowed": True,
|
|
146
364
|
"color": "W",
|
|
@@ -160,6 +378,7 @@ class RFIDBatchApiTests(TestCase):
|
|
|
160
378
|
"rfids": [
|
|
161
379
|
{
|
|
162
380
|
"rfid": "CARD112",
|
|
381
|
+
"custom_label": "",
|
|
163
382
|
"energy_accounts": [],
|
|
164
383
|
"allowed": True,
|
|
165
384
|
"color": "B",
|
|
@@ -174,6 +393,7 @@ class RFIDBatchApiTests(TestCase):
|
|
|
174
393
|
"rfids": [
|
|
175
394
|
{
|
|
176
395
|
"rfid": "A1B2C3D4",
|
|
396
|
+
"custom_label": "Imported Tag",
|
|
177
397
|
"energy_accounts": [self.account.id],
|
|
178
398
|
"allowed": True,
|
|
179
399
|
"color": "W",
|
|
@@ -191,6 +411,7 @@ class RFIDBatchApiTests(TestCase):
|
|
|
191
411
|
self.assertTrue(
|
|
192
412
|
RFID.objects.filter(
|
|
193
413
|
rfid="A1B2C3D4",
|
|
414
|
+
custom_label="Imported Tag",
|
|
194
415
|
energy_accounts=self.account,
|
|
195
416
|
color=RFID.WHITE,
|
|
196
417
|
released=True,
|
|
@@ -237,6 +458,11 @@ class RFIDValidationTests(TestCase):
|
|
|
237
458
|
found = RFID.get_account_by_rfid("abcd1234")
|
|
238
459
|
self.assertEqual(found, acc)
|
|
239
460
|
|
|
461
|
+
def test_custom_label_length(self):
|
|
462
|
+
tag = RFID(rfid="FACE1234", custom_label="x" * 33)
|
|
463
|
+
with self.assertRaises(ValidationError):
|
|
464
|
+
tag.full_clean()
|
|
465
|
+
|
|
240
466
|
|
|
241
467
|
class RFIDAssignmentTests(TestCase):
|
|
242
468
|
def setUp(self):
|
|
@@ -279,7 +505,9 @@ class EnergyAccountTests(TestCase):
|
|
|
279
505
|
|
|
280
506
|
def test_service_account_ignores_balance(self):
|
|
281
507
|
user = User.objects.create_user(username="service", password="x")
|
|
282
|
-
acc = EnergyAccount.objects.create(
|
|
508
|
+
acc = EnergyAccount.objects.create(
|
|
509
|
+
user=user, service_account=True, name="SERVICE"
|
|
510
|
+
)
|
|
283
511
|
self.assertTrue(acc.can_authorize())
|
|
284
512
|
|
|
285
513
|
def test_account_without_user(self):
|
|
@@ -331,7 +559,43 @@ class AddressTests(TestCase):
|
|
|
331
559
|
self.assertEqual(user.address, addr)
|
|
332
560
|
|
|
333
561
|
|
|
334
|
-
class
|
|
562
|
+
class PublicWifiUtilitiesTests(TestCase):
|
|
563
|
+
def setUp(self):
|
|
564
|
+
self.user = User.objects.create_user(username="wifi", password="pwd")
|
|
565
|
+
|
|
566
|
+
def test_grant_public_access_records_allowlist(self):
|
|
567
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
568
|
+
base = Path(tmp)
|
|
569
|
+
allow_file = base / "locks" / "public_wifi_allow.list"
|
|
570
|
+
with override_settings(BASE_DIR=base):
|
|
571
|
+
with patch("core.public_wifi._iptables_available", return_value=False):
|
|
572
|
+
public_wifi.grant_public_access(self.user, "AA:BB:CC:DD:EE:FF")
|
|
573
|
+
self.assertTrue(allow_file.exists())
|
|
574
|
+
content = allow_file.read_text()
|
|
575
|
+
self.assertIn("aa:bb:cc:dd:ee:ff", content)
|
|
576
|
+
self.assertTrue(
|
|
577
|
+
PublicWifiAccess.objects.filter(
|
|
578
|
+
user=self.user, mac_address="aa:bb:cc:dd:ee:ff"
|
|
579
|
+
).exists()
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
def test_revoke_public_access_for_user_updates_allowlist(self):
|
|
583
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
584
|
+
base = Path(tmp)
|
|
585
|
+
allow_file = base / "locks" / "public_wifi_allow.list"
|
|
586
|
+
with override_settings(BASE_DIR=base):
|
|
587
|
+
with patch("core.public_wifi._iptables_available", return_value=False):
|
|
588
|
+
access = public_wifi.grant_public_access(
|
|
589
|
+
self.user, "AA:BB:CC:DD:EE:FF"
|
|
590
|
+
)
|
|
591
|
+
public_wifi.revoke_public_access_for_user(self.user)
|
|
592
|
+
access.refresh_from_db()
|
|
593
|
+
self.assertIsNotNone(access.revoked_on)
|
|
594
|
+
if allow_file.exists():
|
|
595
|
+
self.assertNotIn("aa:bb:cc:dd:ee:ff", allow_file.read_text())
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
class LiveSubscriptionTests(TestCase):
|
|
335
599
|
def setUp(self):
|
|
336
600
|
self.client = Client()
|
|
337
601
|
self.user = User.objects.create_user(username="bob", password="pwd")
|
|
@@ -339,22 +603,41 @@ class SubscriptionTests(TestCase):
|
|
|
339
603
|
self.product = Product.objects.create(name="Gold", renewal_period=30)
|
|
340
604
|
self.client.force_login(self.user)
|
|
341
605
|
|
|
342
|
-
def
|
|
606
|
+
def test_create_and_list_live_subscription(self):
|
|
343
607
|
response = self.client.post(
|
|
344
|
-
reverse("add-subscription"),
|
|
608
|
+
reverse("add-live-subscription"),
|
|
345
609
|
data={"account_id": self.account.id, "product_id": self.product.id},
|
|
346
610
|
content_type="application/json",
|
|
347
611
|
)
|
|
348
612
|
self.assertEqual(response.status_code, 200)
|
|
349
|
-
self.
|
|
613
|
+
self.account.refresh_from_db()
|
|
614
|
+
self.assertEqual(
|
|
615
|
+
self.account.live_subscription_product,
|
|
616
|
+
self.product,
|
|
617
|
+
)
|
|
618
|
+
self.assertIsNotNone(self.account.live_subscription_start_date)
|
|
619
|
+
self.assertEqual(
|
|
620
|
+
self.account.live_subscription_start_date,
|
|
621
|
+
timezone.localdate(),
|
|
622
|
+
)
|
|
623
|
+
self.assertEqual(
|
|
624
|
+
self.account.live_subscription_next_renewal,
|
|
625
|
+
self.account.live_subscription_start_date
|
|
626
|
+
+ timedelta(days=self.product.renewal_period),
|
|
627
|
+
)
|
|
350
628
|
|
|
351
629
|
list_resp = self.client.get(
|
|
352
|
-
reverse("subscription-list"), {"account_id": self.account.id}
|
|
630
|
+
reverse("live-subscription-list"), {"account_id": self.account.id}
|
|
353
631
|
)
|
|
354
632
|
self.assertEqual(list_resp.status_code, 200)
|
|
355
633
|
data = list_resp.json()
|
|
356
|
-
self.assertEqual(len(data["
|
|
357
|
-
self.assertEqual(data["
|
|
634
|
+
self.assertEqual(len(data["live_subscriptions"]), 1)
|
|
635
|
+
self.assertEqual(data["live_subscriptions"][0]["product__name"], "Gold")
|
|
636
|
+
self.assertEqual(data["live_subscriptions"][0]["id"], self.account.id)
|
|
637
|
+
self.assertEqual(
|
|
638
|
+
data["live_subscriptions"][0]["next_renewal"],
|
|
639
|
+
str(self.account.live_subscription_next_renewal),
|
|
640
|
+
)
|
|
358
641
|
|
|
359
642
|
def test_product_list(self):
|
|
360
643
|
response = self.client.get(reverse("product-list"))
|
|
@@ -394,8 +677,8 @@ class EVBrandFixtureTests(TestCase):
|
|
|
394
677
|
def test_ev_brand_fixture_loads(self):
|
|
395
678
|
call_command(
|
|
396
679
|
"loaddata",
|
|
397
|
-
"core/fixtures/
|
|
398
|
-
"core/fixtures/
|
|
680
|
+
*sorted(glob("core/fixtures/ev_brands__*.json")),
|
|
681
|
+
*sorted(glob("core/fixtures/ev_models__*.json")),
|
|
399
682
|
verbosity=0,
|
|
400
683
|
)
|
|
401
684
|
porsche = Brand.objects.get(name="Porsche")
|
|
@@ -408,11 +691,14 @@ class EVBrandFixtureTests(TestCase):
|
|
|
408
691
|
)
|
|
409
692
|
self.assertTrue(EVModel.objects.filter(brand=porsche, name="Taycan").exists())
|
|
410
693
|
self.assertTrue(EVModel.objects.filter(brand=audi, name="e-tron GT").exists())
|
|
694
|
+
self.assertTrue(EVModel.objects.filter(brand=porsche, name="Macan").exists())
|
|
695
|
+
model3 = EVModel.objects.get(brand__name="Tesla", name="Model 3 RWD")
|
|
696
|
+
self.assertEqual(model3.est_battery_kwh, Decimal("57.50"))
|
|
411
697
|
|
|
412
698
|
def test_brand_from_vin(self):
|
|
413
699
|
call_command(
|
|
414
700
|
"loaddata",
|
|
415
|
-
"core/fixtures/
|
|
701
|
+
*sorted(glob("core/fixtures/ev_brands__*.json")),
|
|
416
702
|
verbosity=0,
|
|
417
703
|
)
|
|
418
704
|
self.assertEqual(Brand.from_vin("WP0ZZZ12345678901").name, "Porsche")
|
|
@@ -424,9 +710,9 @@ class RFIDFixtureTests(TestCase):
|
|
|
424
710
|
def test_fixture_assigns_gelectriic_rfid(self):
|
|
425
711
|
call_command(
|
|
426
712
|
"loaddata",
|
|
427
|
-
"core/fixtures/
|
|
428
|
-
"core/fixtures/
|
|
429
|
-
"core/fixtures/
|
|
713
|
+
"core/fixtures/users__arthexis.json",
|
|
714
|
+
"core/fixtures/energy_accounts__gelectriic.json",
|
|
715
|
+
"core/fixtures/rfids__ffffffff.json",
|
|
430
716
|
verbosity=0,
|
|
431
717
|
)
|
|
432
718
|
account = EnergyAccount.objects.get(name="GELECTRIIC")
|
|
@@ -458,37 +744,6 @@ class SecurityGroupTests(TestCase):
|
|
|
458
744
|
self.assertIn(user, child.user_set.all())
|
|
459
745
|
|
|
460
746
|
|
|
461
|
-
class FediverseProfileTests(TestCase):
|
|
462
|
-
def setUp(self):
|
|
463
|
-
self.user = User.objects.create_user(username="fed", password="secret")
|
|
464
|
-
|
|
465
|
-
@mock.patch("requests.get")
|
|
466
|
-
def test_connection_success_sets_verified(self, mock_get):
|
|
467
|
-
mock_get.return_value.ok = True
|
|
468
|
-
mock_get.return_value.raise_for_status.return_value = None
|
|
469
|
-
profile = FediverseProfile.objects.create(
|
|
470
|
-
user=self.user,
|
|
471
|
-
service=FediverseProfile.MASTODON,
|
|
472
|
-
host="example.com",
|
|
473
|
-
handle="fed",
|
|
474
|
-
access_token="tok",
|
|
475
|
-
)
|
|
476
|
-
self.assertTrue(profile.test_connection())
|
|
477
|
-
self.assertIsNotNone(profile.verified_on)
|
|
478
|
-
|
|
479
|
-
@mock.patch("requests.get", side_effect=Exception("boom"))
|
|
480
|
-
def test_connection_failure_raises(self, mock_get):
|
|
481
|
-
profile = FediverseProfile.objects.create(
|
|
482
|
-
user=self.user,
|
|
483
|
-
service=FediverseProfile.MASTODON,
|
|
484
|
-
host="example.com",
|
|
485
|
-
handle="fed",
|
|
486
|
-
)
|
|
487
|
-
with self.assertRaises(ValidationError):
|
|
488
|
-
profile.test_connection()
|
|
489
|
-
self.assertIsNone(profile.verified_on)
|
|
490
|
-
|
|
491
|
-
|
|
492
747
|
class ReleaseProcessTests(TestCase):
|
|
493
748
|
def setUp(self):
|
|
494
749
|
self.package = Package.objects.create(name="pkg")
|
|
@@ -499,14 +754,14 @@ class ReleaseProcessTests(TestCase):
|
|
|
499
754
|
@mock.patch("core.views.release_utils._git_clean", return_value=False)
|
|
500
755
|
def test_step_check_requires_clean_repo(self, git_clean):
|
|
501
756
|
with self.assertRaises(Exception):
|
|
502
|
-
|
|
757
|
+
_step_check_version(self.release, {}, Path("rel.log"))
|
|
503
758
|
|
|
504
759
|
@mock.patch("core.views.release_utils._git_clean", return_value=True)
|
|
505
760
|
@mock.patch("core.views.release_utils.network_available", return_value=False)
|
|
506
761
|
def test_step_check_keeps_repo_clean(self, network_available, git_clean):
|
|
507
762
|
version_path = Path("VERSION")
|
|
508
763
|
original = version_path.read_text(encoding="utf-8")
|
|
509
|
-
|
|
764
|
+
_step_check_version(self.release, {}, Path("rel.log"))
|
|
510
765
|
proc = subprocess.run(
|
|
511
766
|
["git", "status", "--porcelain", str(version_path)],
|
|
512
767
|
capture_output=True,
|
|
@@ -524,9 +779,7 @@ class ReleaseProcessTests(TestCase):
|
|
|
524
779
|
@mock.patch("core.views.subprocess.run")
|
|
525
780
|
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
526
781
|
@mock.patch("core.views.release_utils.promote", side_effect=Exception("boom"))
|
|
527
|
-
def test_promote_cleans_repo_on_failure(
|
|
528
|
-
self, promote, dump_fixture, run
|
|
529
|
-
):
|
|
782
|
+
def test_promote_cleans_repo_on_failure(self, promote, dump_fixture, run):
|
|
530
783
|
with self.assertRaises(Exception):
|
|
531
784
|
_step_promote_build(self.release, {}, Path("rel.log"))
|
|
532
785
|
dump_fixture.assert_not_called()
|
|
@@ -651,6 +904,139 @@ class ReleaseProcessTests(TestCase):
|
|
|
651
904
|
self.assertEqual(count_file.read_text(), "1")
|
|
652
905
|
|
|
653
906
|
|
|
907
|
+
class ReleaseProgressSyncTests(TestCase):
|
|
908
|
+
def setUp(self):
|
|
909
|
+
self.client = Client()
|
|
910
|
+
self.user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
911
|
+
self.client.force_login(self.user)
|
|
912
|
+
self.package = Package.objects.get(name="arthexis")
|
|
913
|
+
self.version_path = Path("VERSION")
|
|
914
|
+
self.original_version = self.version_path.read_text(encoding="utf-8")
|
|
915
|
+
self.version_path.write_text("1.2.3", encoding="utf-8")
|
|
916
|
+
|
|
917
|
+
def tearDown(self):
|
|
918
|
+
self.version_path.write_text(self.original_version, encoding="utf-8")
|
|
919
|
+
|
|
920
|
+
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
921
|
+
@mock.patch("core.views.revision.get_revision", return_value="abc123")
|
|
922
|
+
def test_unpublished_release_syncs_version_and_revision(
|
|
923
|
+
self, get_revision, dump_fixture
|
|
924
|
+
):
|
|
925
|
+
release = PackageRelease.objects.create(
|
|
926
|
+
package=self.package,
|
|
927
|
+
version="1.0.0",
|
|
928
|
+
)
|
|
929
|
+
release.revision = "oldrev"
|
|
930
|
+
release.save(update_fields=["revision"])
|
|
931
|
+
|
|
932
|
+
url = reverse("release-progress", args=[release.pk, "publish"])
|
|
933
|
+
response = self.client.get(url)
|
|
934
|
+
|
|
935
|
+
self.assertEqual(response.status_code, 200)
|
|
936
|
+
release.refresh_from_db()
|
|
937
|
+
self.assertEqual(release.version, "1.2.4")
|
|
938
|
+
self.assertEqual(release.revision, "abc123")
|
|
939
|
+
dump_fixture.assert_called_once()
|
|
940
|
+
|
|
941
|
+
def test_published_release_not_current_returns_404(self):
|
|
942
|
+
release = PackageRelease.objects.create(
|
|
943
|
+
package=self.package,
|
|
944
|
+
version="1.2.4",
|
|
945
|
+
pypi_url="https://example.com",
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
url = reverse("release-progress", args=[release.pk, "publish"])
|
|
949
|
+
response = self.client.get(url)
|
|
950
|
+
|
|
951
|
+
self.assertEqual(response.status_code, 404)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
class ReleaseProgressFixtureVisibilityTests(TestCase):
|
|
955
|
+
def setUp(self):
|
|
956
|
+
self.client = Client()
|
|
957
|
+
self.user = User.objects.create_superuser(
|
|
958
|
+
"fixture-check", "fixture@example.com", "pw"
|
|
959
|
+
)
|
|
960
|
+
self.client.force_login(self.user)
|
|
961
|
+
current_version = Path("VERSION").read_text(encoding="utf-8").strip()
|
|
962
|
+
package = Package.objects.filter(is_active=True).first()
|
|
963
|
+
if package is None:
|
|
964
|
+
package = Package.objects.create(name="fixturepkg", is_active=True)
|
|
965
|
+
try:
|
|
966
|
+
self.release = PackageRelease.objects.get(
|
|
967
|
+
package=package, version=current_version
|
|
968
|
+
)
|
|
969
|
+
except PackageRelease.DoesNotExist:
|
|
970
|
+
self.release = PackageRelease.objects.create(
|
|
971
|
+
package=package, version=current_version
|
|
972
|
+
)
|
|
973
|
+
self.session_key = f"release_publish_{self.release.pk}"
|
|
974
|
+
self.log_name = f"{self.release.package.name}-{self.release.version}.log"
|
|
975
|
+
self.lock_path = Path("locks") / f"{self.session_key}.json"
|
|
976
|
+
self.restart_path = Path("locks") / f"{self.session_key}.restarts"
|
|
977
|
+
self.log_path = Path("logs") / self.log_name
|
|
978
|
+
for path in (self.lock_path, self.restart_path, self.log_path):
|
|
979
|
+
if path.exists():
|
|
980
|
+
path.unlink()
|
|
981
|
+
try:
|
|
982
|
+
self.fixture_step_index = next(
|
|
983
|
+
idx
|
|
984
|
+
for idx, (name, _) in enumerate(core_views.PUBLISH_STEPS)
|
|
985
|
+
if name == core_views.FIXTURE_REVIEW_STEP_NAME
|
|
986
|
+
)
|
|
987
|
+
except StopIteration: # pragma: no cover - defensive guard
|
|
988
|
+
self.fail("Fixture review step not configured in publish steps")
|
|
989
|
+
self.url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
990
|
+
|
|
991
|
+
def tearDown(self):
|
|
992
|
+
session = self.client.session
|
|
993
|
+
if self.session_key in session:
|
|
994
|
+
session.pop(self.session_key)
|
|
995
|
+
session.save()
|
|
996
|
+
for path in (self.lock_path, self.restart_path, self.log_path):
|
|
997
|
+
if path.exists():
|
|
998
|
+
path.unlink()
|
|
999
|
+
super().tearDown()
|
|
1000
|
+
|
|
1001
|
+
def _set_session(self, step: int, fixtures: list[dict]):
|
|
1002
|
+
session = self.client.session
|
|
1003
|
+
session[self.session_key] = {
|
|
1004
|
+
"step": step,
|
|
1005
|
+
"fixtures": fixtures,
|
|
1006
|
+
"log": self.log_name,
|
|
1007
|
+
"started": True,
|
|
1008
|
+
}
|
|
1009
|
+
session.save()
|
|
1010
|
+
|
|
1011
|
+
def test_fixture_summary_visible_until_migration_step(self):
|
|
1012
|
+
fixtures = [
|
|
1013
|
+
{
|
|
1014
|
+
"path": "core/fixtures/example.json",
|
|
1015
|
+
"count": 2,
|
|
1016
|
+
"models": ["core.Model"],
|
|
1017
|
+
}
|
|
1018
|
+
]
|
|
1019
|
+
self._set_session(self.fixture_step_index, fixtures)
|
|
1020
|
+
response = self.client.get(self.url)
|
|
1021
|
+
self.assertEqual(response.status_code, 200)
|
|
1022
|
+
self.assertEqual(response.context["fixtures"], fixtures)
|
|
1023
|
+
self.assertContains(response, "Fixture changes")
|
|
1024
|
+
|
|
1025
|
+
def test_fixture_summary_hidden_after_migration_step(self):
|
|
1026
|
+
fixtures = [
|
|
1027
|
+
{
|
|
1028
|
+
"path": "core/fixtures/example.json",
|
|
1029
|
+
"count": 2,
|
|
1030
|
+
"models": ["core.Model"],
|
|
1031
|
+
}
|
|
1032
|
+
]
|
|
1033
|
+
self._set_session(self.fixture_step_index + 1, fixtures)
|
|
1034
|
+
response = self.client.get(self.url)
|
|
1035
|
+
self.assertEqual(response.status_code, 200)
|
|
1036
|
+
self.assertIsNone(response.context["fixtures"])
|
|
1037
|
+
self.assertNotContains(response, "Fixture changes")
|
|
1038
|
+
|
|
1039
|
+
|
|
654
1040
|
class PackageReleaseAdminActionTests(TestCase):
|
|
655
1041
|
def setUp(self):
|
|
656
1042
|
self.factory = RequestFactory()
|
|
@@ -688,12 +1074,9 @@ class PackageReleaseAdminActionTests(TestCase):
|
|
|
688
1074
|
mock_get.return_value.json.return_value = {
|
|
689
1075
|
"releases": {"1.0.0": [], "1.1.0": []}
|
|
690
1076
|
}
|
|
691
|
-
self.admin.refresh_from_pypi(
|
|
692
|
-
|
|
693
|
-
)
|
|
694
|
-
self.assertTrue(
|
|
695
|
-
PackageRelease.objects.filter(version="1.1.0").exists()
|
|
696
|
-
)
|
|
1077
|
+
self.admin.refresh_from_pypi(self.request, PackageRelease.objects.none())
|
|
1078
|
+
new_release = PackageRelease.objects.get(version="1.1.0")
|
|
1079
|
+
self.assertEqual(new_release.revision, "")
|
|
697
1080
|
dump.assert_called_once()
|
|
698
1081
|
|
|
699
1082
|
|
|
@@ -735,6 +1118,40 @@ class PackageReleaseCurrentTests(TestCase):
|
|
|
735
1118
|
self.assertFalse(self.release.is_current)
|
|
736
1119
|
|
|
737
1120
|
|
|
1121
|
+
class PackageReleaseChangelistTests(TestCase):
|
|
1122
|
+
def setUp(self):
|
|
1123
|
+
self.client = Client()
|
|
1124
|
+
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1125
|
+
self.client.force_login(User.objects.get(username="admin"))
|
|
1126
|
+
|
|
1127
|
+
def test_prepare_next_release_button_present(self):
|
|
1128
|
+
response = self.client.get(reverse("admin:core_packagerelease_changelist"))
|
|
1129
|
+
prepare_url = reverse(
|
|
1130
|
+
"admin:core_packagerelease_actions", args=["prepare_next_release"]
|
|
1131
|
+
)
|
|
1132
|
+
self.assertContains(response, prepare_url, html=False)
|
|
1133
|
+
|
|
1134
|
+
def test_refresh_from_pypi_button_present(self):
|
|
1135
|
+
response = self.client.get(reverse("admin:core_packagerelease_changelist"))
|
|
1136
|
+
refresh_url = reverse(
|
|
1137
|
+
"admin:core_packagerelease_actions", args=["refresh_from_pypi"]
|
|
1138
|
+
)
|
|
1139
|
+
self.assertContains(response, refresh_url, html=False)
|
|
1140
|
+
|
|
1141
|
+
def test_prepare_next_release_action_creates_release(self):
|
|
1142
|
+
package = Package.objects.get(name="arthexis")
|
|
1143
|
+
PackageRelease.all_objects.filter(package=package).delete()
|
|
1144
|
+
response = self.client.post(
|
|
1145
|
+
reverse(
|
|
1146
|
+
"admin:core_packagerelease_actions", args=["prepare_next_release"]
|
|
1147
|
+
)
|
|
1148
|
+
)
|
|
1149
|
+
self.assertEqual(response.status_code, 302)
|
|
1150
|
+
self.assertTrue(
|
|
1151
|
+
PackageRelease.all_objects.filter(package=package).exists()
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
|
|
738
1155
|
class PackageAdminPrepareNextReleaseTests(TestCase):
|
|
739
1156
|
def setUp(self):
|
|
740
1157
|
self.factory = RequestFactory()
|
|
@@ -753,22 +1170,176 @@ class PackageAdminPrepareNextReleaseTests(TestCase):
|
|
|
753
1170
|
)
|
|
754
1171
|
|
|
755
1172
|
|
|
756
|
-
class
|
|
1173
|
+
class PackageAdminChangeViewTests(TestCase):
|
|
757
1174
|
def setUp(self):
|
|
758
1175
|
self.client = Client()
|
|
759
1176
|
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
760
1177
|
self.client.force_login(User.objects.get(username="admin"))
|
|
1178
|
+
self.package = Package.objects.get(name="arthexis")
|
|
761
1179
|
|
|
762
|
-
def
|
|
763
|
-
response = self.client.get(
|
|
764
|
-
|
|
765
|
-
response, reverse("admin:core_package_prepare_next_release"), html=False
|
|
1180
|
+
def test_prepare_next_release_button_visible_on_change_view(self):
|
|
1181
|
+
response = self.client.get(
|
|
1182
|
+
reverse("admin:core_package_change", args=[self.package.pk])
|
|
766
1183
|
)
|
|
1184
|
+
self.assertContains(response, "Prepare next Release")
|
|
767
1185
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1186
|
+
|
|
1187
|
+
class TodoDoneTests(TestCase):
|
|
1188
|
+
def setUp(self):
|
|
1189
|
+
self.client = Client()
|
|
1190
|
+
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1191
|
+
self.client.force_login(User.objects.get(username="admin"))
|
|
1192
|
+
|
|
1193
|
+
def test_mark_done_sets_timestamp(self):
|
|
1194
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
1195
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
1196
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1197
|
+
todo.refresh_from_db()
|
|
1198
|
+
self.assertIsNotNone(todo.done_on)
|
|
1199
|
+
self.assertFalse(todo.is_deleted)
|
|
1200
|
+
|
|
1201
|
+
def test_mark_done_condition_failure_shows_message(self):
|
|
1202
|
+
todo = Todo.objects.create(
|
|
1203
|
+
request="Task",
|
|
1204
|
+
on_done_condition="1 = 0",
|
|
772
1205
|
)
|
|
773
|
-
self.
|
|
1206
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
1207
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1208
|
+
messages = [m.message for m in get_messages(resp.wsgi_request)]
|
|
1209
|
+
self.assertTrue(messages)
|
|
1210
|
+
self.assertIn("1 = 0", messages[0])
|
|
1211
|
+
todo.refresh_from_db()
|
|
1212
|
+
self.assertIsNone(todo.done_on)
|
|
1213
|
+
|
|
1214
|
+
def test_mark_done_condition_invalid_expression(self):
|
|
1215
|
+
todo = Todo.objects.create(
|
|
1216
|
+
request="Task",
|
|
1217
|
+
on_done_condition="1; SELECT 1",
|
|
1218
|
+
)
|
|
1219
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
1220
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1221
|
+
messages = [m.message for m in get_messages(resp.wsgi_request)]
|
|
1222
|
+
self.assertTrue(messages)
|
|
1223
|
+
self.assertIn("Semicolons", messages[0])
|
|
1224
|
+
todo.refresh_from_db()
|
|
1225
|
+
self.assertIsNone(todo.done_on)
|
|
1226
|
+
|
|
1227
|
+
def test_mark_done_condition_resolves_sigils(self):
|
|
1228
|
+
todo = Todo.objects.create(
|
|
1229
|
+
request="Task",
|
|
1230
|
+
on_done_condition="[TEST]",
|
|
1231
|
+
)
|
|
1232
|
+
with mock.patch.object(Todo, "resolve_sigils", return_value="1 = 1") as resolver:
|
|
1233
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
1234
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1235
|
+
resolver.assert_called_once_with("on_done_condition")
|
|
1236
|
+
todo.refresh_from_db()
|
|
1237
|
+
self.assertIsNotNone(todo.done_on)
|
|
1238
|
+
|
|
1239
|
+
def test_mark_done_respects_next_parameter(self):
|
|
1240
|
+
todo = Todo.objects.create(request="Task")
|
|
1241
|
+
next_url = reverse("admin:index") + "?section=todos"
|
|
1242
|
+
resp = self.client.post(
|
|
1243
|
+
reverse("todo-done", args=[todo.pk]),
|
|
1244
|
+
{"next": next_url},
|
|
1245
|
+
)
|
|
1246
|
+
self.assertRedirects(resp, next_url, target_status_code=200)
|
|
1247
|
+
todo.refresh_from_db()
|
|
1248
|
+
self.assertIsNotNone(todo.done_on)
|
|
1249
|
+
|
|
1250
|
+
def test_mark_done_rejects_external_next(self):
|
|
1251
|
+
todo = Todo.objects.create(request="Task")
|
|
1252
|
+
resp = self.client.post(
|
|
1253
|
+
reverse("todo-done", args=[todo.pk]),
|
|
1254
|
+
{"next": "https://example.com/"},
|
|
1255
|
+
)
|
|
1256
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1257
|
+
todo.refresh_from_db()
|
|
1258
|
+
self.assertIsNotNone(todo.done_on)
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
class TodoFocusViewTests(TestCase):
|
|
1262
|
+
def setUp(self):
|
|
1263
|
+
self.client = Client()
|
|
1264
|
+
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1265
|
+
self.client.force_login(User.objects.get(username="admin"))
|
|
1266
|
+
|
|
1267
|
+
def test_focus_view_renders_requested_page(self):
|
|
1268
|
+
todo = Todo.objects.create(request="Task", url="/docs/")
|
|
1269
|
+
next_url = reverse("admin:index")
|
|
1270
|
+
resp = self.client.get(
|
|
1271
|
+
f"{reverse('todo-focus', args=[todo.pk])}?next={quote(next_url)}"
|
|
1272
|
+
)
|
|
1273
|
+
self.assertEqual(resp.status_code, 200)
|
|
1274
|
+
self.assertContains(resp, todo.request)
|
|
1275
|
+
self.assertEqual(resp["X-Frame-Options"], "SAMEORIGIN")
|
|
1276
|
+
self.assertContains(resp, f'src="{todo.url}"')
|
|
1277
|
+
self.assertContains(resp, "Done")
|
|
1278
|
+
self.assertContains(resp, "Back")
|
|
1279
|
+
|
|
1280
|
+
def test_focus_view_uses_admin_change_when_no_url(self):
|
|
1281
|
+
todo = Todo.objects.create(request="Task")
|
|
1282
|
+
resp = self.client.get(reverse("todo-focus", args=[todo.pk]))
|
|
1283
|
+
change_url = reverse("admin:core_todo_change", args=[todo.pk])
|
|
1284
|
+
self.assertContains(resp, f'src="{change_url}"')
|
|
1285
|
+
|
|
1286
|
+
def test_focus_view_sanitizes_loopback_absolute_url(self):
|
|
1287
|
+
todo = Todo.objects.create(
|
|
1288
|
+
request="Task",
|
|
1289
|
+
url="http://127.0.0.1:8000/docs/?section=chart",
|
|
1290
|
+
)
|
|
1291
|
+
resp = self.client.get(reverse("todo-focus", args=[todo.pk]))
|
|
1292
|
+
self.assertContains(resp, 'src="/docs/?section=chart"')
|
|
1293
|
+
|
|
1294
|
+
def test_focus_view_rejects_external_absolute_url(self):
|
|
1295
|
+
todo = Todo.objects.create(
|
|
1296
|
+
request="Task",
|
|
1297
|
+
url="https://outside.invalid/external/",
|
|
1298
|
+
)
|
|
1299
|
+
resp = self.client.get(reverse("todo-focus", args=[todo.pk]))
|
|
1300
|
+
change_url = reverse("admin:core_todo_change", args=[todo.pk])
|
|
1301
|
+
self.assertContains(resp, f'src="{change_url}"')
|
|
1302
|
+
|
|
1303
|
+
def test_focus_view_redirects_if_todo_completed(self):
|
|
1304
|
+
todo = Todo.objects.create(request="Task")
|
|
1305
|
+
todo.done_on = timezone.now()
|
|
1306
|
+
todo.save(update_fields=["done_on"])
|
|
1307
|
+
next_url = reverse("admin:index")
|
|
1308
|
+
resp = self.client.get(
|
|
1309
|
+
f"{reverse('todo-focus', args=[todo.pk])}?next={quote(next_url)}"
|
|
1310
|
+
)
|
|
1311
|
+
self.assertRedirects(resp, next_url, target_status_code=200)
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
class TodoUrlValidationTests(TestCase):
|
|
1315
|
+
def test_relative_url_valid(self):
|
|
1316
|
+
todo = Todo(request="Task", url="/path")
|
|
1317
|
+
todo.full_clean() # should not raise
|
|
1318
|
+
|
|
1319
|
+
def test_absolute_url_invalid(self):
|
|
1320
|
+
todo = Todo(request="Task", url="https://example.com/path")
|
|
1321
|
+
with self.assertRaises(ValidationError):
|
|
1322
|
+
todo.full_clean()
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
class TodoUniqueTests(TestCase):
|
|
1326
|
+
def test_request_unique_case_insensitive(self):
|
|
1327
|
+
Todo.objects.create(request="Task")
|
|
1328
|
+
with self.assertRaises(IntegrityError):
|
|
1329
|
+
Todo.objects.create(request="task")
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
class TodoAdminPermissionTests(TestCase):
|
|
1333
|
+
def setUp(self):
|
|
1334
|
+
self.client = Client()
|
|
1335
|
+
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1336
|
+
self.client.force_login(User.objects.get(username="admin"))
|
|
1337
|
+
|
|
1338
|
+
def test_add_view_disallowed(self):
|
|
1339
|
+
resp = self.client.get(reverse("admin:core_todo_add"))
|
|
1340
|
+
self.assertEqual(resp.status_code, 403)
|
|
774
1341
|
|
|
1342
|
+
def test_change_form_loads(self):
|
|
1343
|
+
todo = Todo.objects.create(request="Task")
|
|
1344
|
+
resp = self.client.get(reverse("admin:core_todo_change", args=[todo.pk]))
|
|
1345
|
+
self.assertEqual(resp.status_code, 200)
|