Skip to content

Commit febe580

Browse files
authored
chore(backend): increase coverage (inventree#10121)
* chore(backend): increase coverage * add a full api based install / uninstall test * fix asserted code * delete unreachable code * clean up unused code * add more notification tests * fix test * order currencies
1 parent 5574e7c commit febe580

9 files changed

Lines changed: 185 additions & 130 deletions

File tree

src/backend/InvenTree/common/apps.py

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
import structlog
66

77
import InvenTree.ready
8-
from common.settings import (
9-
get_global_setting,
10-
global_setting_overrides,
11-
set_global_setting,
12-
)
8+
from common.settings import get_global_setting, set_global_setting
139

1410
logger = structlog.get_logger('inventree')
1511

@@ -24,11 +20,10 @@ class CommonConfig(AppConfig):
2420

2521
def ready(self):
2622
"""Initialize restart flag clearance on startup."""
27-
if InvenTree.ready.isRunningMigrations():
23+
if InvenTree.ready.isRunningMigrations(): # pragma: no cover
2824
return
2925

3026
self.clear_restart_flag()
31-
self.override_global_settings()
3227

3328
def clear_restart_flag(self):
3429
"""Clear the SERVER_RESTART_REQUIRED setting."""
@@ -40,28 +35,5 @@ def clear_restart_flag(self):
4035

4136
if not InvenTree.ready.isImportingData():
4237
set_global_setting('SERVER_RESTART_REQUIRED', False, None)
43-
except Exception:
38+
except Exception: # pragma: no cover
4439
pass
45-
46-
def override_global_settings(self):
47-
"""Update global settings based on environment variables."""
48-
overrides = global_setting_overrides()
49-
50-
if not overrides:
51-
return
52-
53-
for key, value in overrides.items():
54-
try:
55-
current_value = get_global_setting(key, create=False)
56-
57-
if current_value != value:
58-
logger.info(
59-
'INVE-I1: Overriding global setting: %s = %s',
60-
value,
61-
current_value,
62-
)
63-
set_global_setting(key, value, None, create=True)
64-
65-
except Exception:
66-
logger.warning('Failed to override global setting %s -> %s', key, value)
67-
continue

src/backend/InvenTree/common/migrations/0001_initial.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ def database_forwards(self, app_label, schema_editor, from_state, to_state) -> N
1818

1919
try:
2020
super().database_forwards(app_label, schema_editor, from_state, to_state)
21-
except Exception:
21+
except Exception: # pragma: no cover
2222
pass
2323

2424
def state_forwards(self, app_label, state) -> None:
2525
try:
2626
super().state_forwards(app_label, state)
27-
except Exception:
27+
except Exception: # pragma: no cover
2828
pass
2929

3030

src/backend/InvenTree/common/migrations/0023_auto_20240602_1332.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,18 @@ def set_currencies(apps, schema_editor):
4949

5050
value = ','.join(valid_codes)
5151

52-
if not settings.TESTING:
52+
if not settings.TESTING: # pragma: no cover
5353
print(f"Found existing currency codes:", value)
5454

5555
setting = InvenTreeSetting.objects.filter(key=key).first()
5656

5757
if setting:
58-
if not settings.TESTING:
58+
if not settings.TESTING: # pragma: no cover
5959
print(f"- Updating existing setting for currency codes")
6060
setting.value = value
6161
setting.save()
6262
else:
63-
if not settings.TESTING:
63+
if not settings.TESTING: # pragma: no cover
6464
print(f"- Creating new setting for currency codes")
6565
setting = InvenTreeSetting(key=key, value=value)
6666
setting.save()

src/backend/InvenTree/common/models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def save_to_cache(self):
288288

289289
try:
290290
cache.set(key, self, timeout=3600)
291-
except Exception:
291+
except Exception: # pragma: no cover
292292
pass
293293

294294
@classmethod
@@ -558,7 +558,7 @@ def get_setting_object(cls, key, **kwargs):
558558
or InvenTree.ready.isRunningMigrations()
559559
or InvenTree.ready.isRebuildingData()
560560
or InvenTree.ready.isRunningBackup()
561-
):
561+
): # pragma: no cover
562562
create = False
563563
access_global_cache = False
564564

@@ -706,7 +706,7 @@ def set_setting(cls, key, value, change_user=None, create=True, **kwargs):
706706
or InvenTree.ready.isRunningMigrations()
707707
or InvenTree.ready.isRebuildingData()
708708
or InvenTree.ready.isRunningBackup()
709-
):
709+
): # pragma: no cover
710710
return
711711

712712
attempts = int(kwargs.get('attempts', 3))
@@ -731,7 +731,7 @@ def set_setting(cls, key, value, change_user=None, create=True, **kwargs):
731731
logger.warning("Database is locked, cannot set setting '%s'", key)
732732
# Likely the DB is locked - not much we can do here
733733
return
734-
except Exception as exc:
734+
except Exception as exc: # pragma: no cover
735735
logger.exception(
736736
"Error setting setting '%s' for %s: %s", key, str(cls), str(type(exc))
737737
)
@@ -766,7 +766,7 @@ def set_setting(cls, key, value, change_user=None, create=True, **kwargs):
766766
except (OperationalError, ProgrammingError):
767767
logger.warning("Database is locked, cannot set setting '%s'", key)
768768
# Likely the DB is locked - not much we can do here
769-
except Exception as exc:
769+
except Exception as exc: # pragma: no cover
770770
# Some other error
771771
logger.exception(
772772
"Error setting setting '%s' for %s: %s", key, str(cls), str(type(exc))

src/backend/InvenTree/common/notifications.py

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -22,92 +22,6 @@
2222

2323

2424
# region methods
25-
class NotificationMethod:
26-
"""Base class for notification methods."""
27-
28-
METHOD_NAME = ''
29-
METHOD_ICON = None
30-
CONTEXT_BUILTIN = ['name', 'message']
31-
CONTEXT_EXTRA = []
32-
GLOBAL_SETTING = None
33-
USER_SETTING = None
34-
35-
def __init__(self, obj: Model, category: str, targets: list, context) -> None:
36-
"""Check that the method is read.
37-
38-
This checks that:
39-
- All needed functions are implemented
40-
- The method is not disabled via plugin
41-
- All needed context values were provided
42-
"""
43-
# Check if a sending fnc is defined
44-
if (not hasattr(self, 'send')) and (not hasattr(self, 'send_bulk')):
45-
raise NotImplementedError(
46-
'A NotificationMethod must either define a `send` or a `send_bulk` method'
47-
)
48-
49-
# No method name is no good
50-
if self.METHOD_NAME in ('', None):
51-
raise NotImplementedError(
52-
f'The NotificationMethod {self.__class__} did not provide a METHOD_NAME'
53-
)
54-
55-
# Check if plugin is disabled - if so do not gather targets etc.
56-
if self.global_setting_disable():
57-
self.targets = None
58-
return
59-
60-
# Define arguments
61-
self.obj = obj
62-
self.category = category
63-
self.targets = targets
64-
self.context = self.check_context(context)
65-
66-
# Gather targets
67-
self.targets = self.get_targets()
68-
69-
def check_context(self, context):
70-
"""Check that all values defined in the methods CONTEXT were provided in the current context."""
71-
72-
def check(ref, obj):
73-
# the obj is not accessible so we are on the end
74-
if not isinstance(obj, (list, dict, tuple)):
75-
return ref
76-
77-
# check if the ref exists
78-
if isinstance(ref, str):
79-
if not obj.get(ref):
80-
return ref
81-
return False
82-
83-
# nested
84-
elif isinstance(ref, (tuple, list)):
85-
if len(ref) == 1:
86-
return check(ref[0], obj)
87-
ret = check(ref[0], obj)
88-
if ret:
89-
return ret
90-
return check(ref[1:], obj[ref[0]])
91-
92-
# other cases -> raise
93-
raise NotImplementedError(
94-
'This type can not be used as a context reference'
95-
)
96-
97-
missing = []
98-
for item in (*self.CONTEXT_BUILTIN, *self.CONTEXT_EXTRA):
99-
ret = check(item, context)
100-
if ret:
101-
missing.append(ret)
102-
103-
if missing:
104-
raise NotImplementedError(
105-
f'The `context` is missing the following items:\n{missing}'
106-
)
107-
108-
return context
109-
110-
11125
@dataclass()
11226
class NotificationBody:
11327
"""Information needed to create a notification.
@@ -182,7 +96,7 @@ def trigger_notification(
18296
kwargs: Additional arguments to pass to the notification method
18397
"""
18498
# Check if data is importing currently
185-
if isImportingData() or isRebuildingData():
99+
if isImportingData() or isRebuildingData(): # pragma: no cover
186100
return
187101

188102
targets = kwargs.get('targets')

src/backend/InvenTree/common/test_migrations.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Data migration unit tests for the 'common' app."""
22

33
import io
4+
import os
45

56
from django.core.files.base import ContentFile
67

@@ -208,3 +209,49 @@ def test_items_exist(self):
208209
'stockitem',
209210
]:
210211
self.assertEqual(Attachment.objects.filter(model_type=model).count(), 2)
212+
213+
214+
def prep_currency_migration(self, vals: str):
215+
"""Prepare the environment for the currency migration tests."""
216+
# Set keys
217+
os.environ['INVENTREE_CURRENCIES'] = vals
218+
219+
# And setting
220+
InvenTreeSetting = self.old_state.apps.get_model('common', 'InvenTreeSetting')
221+
222+
setting = InvenTreeSetting(key='CURRENCY_CODES', value='123')
223+
setting.save()
224+
225+
226+
class TestCurrencyMigration(MigratorTestCase):
227+
"""Test currency migration."""
228+
229+
migrate_from = ('common', '0022_projectcode_responsible')
230+
migrate_to = ('common', '0023_auto_20240602_1332')
231+
232+
def prepare(self):
233+
"""Prepare the environment for the migration test."""
234+
prep_currency_migration(self, 'USD,EUR,GBP')
235+
236+
def test_currency_migration(self):
237+
"""Test that the currency migration works."""
238+
InvenTreeSetting = self.old_state.apps.get_model('common', 'InvenTreeSetting')
239+
setting = InvenTreeSetting.objects.filter(key='CURRENCY_CODES').first()
240+
self.assertEqual(setting.value, 'EUR,GBP,USD')
241+
242+
243+
class TestCurrencyMigrationNo(MigratorTestCase):
244+
"""Test currency migration."""
245+
246+
migrate_from = ('common', '0022_projectcode_responsible')
247+
migrate_to = ('common', '0023_auto_20240602_1332')
248+
249+
def prepare(self):
250+
"""Prepare the environment for the migration test."""
251+
prep_currency_migration(self, 'YYY,ZZZ')
252+
253+
def test_currency_migration(self):
254+
"""Test that no currency migration occurs if wrong currencies are set."""
255+
InvenTreeSetting = self.old_state.apps.get_model('common', 'InvenTreeSetting')
256+
setting = InvenTreeSetting.objects.filter(key='CURRENCY_CODES').first()
257+
self.assertEqual(setting.value, '123')

src/backend/InvenTree/common/tests.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from unittest import mock
99

1010
from django.contrib.auth import get_user_model
11+
from django.contrib.auth.models import Group
1112
from django.contrib.contenttypes.models import ContentType
1213
from django.core.cache import cache
1314
from django.core.exceptions import ValidationError
@@ -21,6 +22,7 @@
2122
import PIL
2223

2324
import common.validators
25+
from common.notifications import trigger_notification
2426
from common.settings import get_global_setting, set_global_setting
2527
from InvenTree.helpers import str2bool
2628
from InvenTree.unit_test import (
@@ -1073,6 +1075,7 @@ class NotificationTest(InvenTreeAPITestCase):
10731075
"""Tests for NotificationEntry."""
10741076

10751077
fixtures = ['users']
1078+
roles = ['admin.view']
10761079

10771080
def test_check_notification_entries(self):
10781081
"""Test that notification entries can be created."""
@@ -1162,6 +1165,72 @@ def test_bulk_delete(self):
11621165
self.assertEqual(NotificationMessage.objects.count(), 13)
11631166
self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3)
11641167

1168+
def test_simple(self):
1169+
"""Test that a simple notification can be created."""
1170+
trigger_notification(
1171+
Group.objects.get(name='Sales'),
1172+
user=self.user,
1173+
data={'message': 'This is a test notification'},
1174+
)
1175+
1176+
def test_with_group(self):
1177+
"""Test that a notification can be created with a group."""
1178+
grp = Group.objects.get(name='Sales')
1179+
trigger_notification(
1180+
grp,
1181+
user=self.user,
1182+
data={'message': 'This is a test notification with group'},
1183+
targets=[grp],
1184+
)
1185+
1186+
def test_wrong_target(self):
1187+
"""Test that a notification with an invalid target raises an error."""
1188+
with self.assertLogs() as cm:
1189+
trigger_notification(
1190+
Group.objects.get(name='Sales'),
1191+
user=self.user,
1192+
data={'message': 'This is a test notification'},
1193+
targets=['invalid_target'],
1194+
)
1195+
self.assertIn('Unknown target passed to t', str(cm[1]))
1196+
1197+
def test_wrong_obj(self):
1198+
"""Test that a object without a reference is raising an issue."""
1199+
1200+
class SampleObj:
1201+
pass
1202+
1203+
with self.assertRaises(KeyError) as cm:
1204+
trigger_notification(
1205+
SampleObj(),
1206+
user=self.user,
1207+
data={'message': 'This is a test notification'},
1208+
)
1209+
self.assertIn('Could not resolve an object reference for', str(cm.exception))
1210+
1211+
# Without reference, it should not raise an error
1212+
trigger_notification(
1213+
Group.objects.get(name='Sales'),
1214+
user=self.user,
1215+
data={'message': 'This is a test notification'},
1216+
)
1217+
1218+
def test_recent(self):
1219+
"""Test that a notification is not created if it was already sent recently."""
1220+
grp = Group.objects.get(name='Sales')
1221+
trigger_notification( #
1222+
grp, category='core', context={'name': 'test'}, targets=[self.user]
1223+
)
1224+
self.assertEqual(NotificationMessage.objects.count(), 1)
1225+
1226+
# Should not create a new notification
1227+
with self.assertLogs(logger='inventree') as cm:
1228+
trigger_notification(
1229+
grp, category='core', context={'name': 'test'}, targets=[self.user]
1230+
)
1231+
self.assertEqual(NotificationMessage.objects.count(), 1)
1232+
self.assertIn('as recently been sent for', str(cm[1]))
1233+
11651234

11661235
class CommonTest(InvenTreeAPITestCase):
11671236
"""Tests for the common config."""

0 commit comments

Comments
 (0)