• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / InvenTree / 4361124568

pending completion
4361124568

push

github

GitHub
Unit test speed improvements (#4463)

181 of 181 new or added lines in 20 files covered. (100.0%)

25546 of 29143 relevant lines covered (87.66%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.93
/InvenTree/common/tests.py
1
"""Tests for mechanisms in common."""
2

3
import json
1✔
4
import time
1✔
5
from datetime import timedelta
1✔
6
from http import HTTPStatus
1✔
7

8
from django.contrib.auth import get_user_model
1✔
9
from django.core.cache import cache
1✔
10
from django.test import Client, TestCase
1✔
11
from django.urls import reverse
1✔
12

13
from InvenTree.api_tester import InvenTreeAPITestCase, PluginMixin
1✔
14
from InvenTree.helpers import InvenTreeTestCase, str2bool
1✔
15
from plugin import registry
1✔
16
from plugin.models import NotificationUserSetting
1✔
17

18
from .api import WebhookView
1✔
19
from .models import (ColorTheme, InvenTreeSetting, InvenTreeUserSetting,
1✔
20
                     NotificationEntry, NotificationMessage, WebhookEndpoint,
21
                     WebhookMessage)
22

23
CONTENT_TYPE_JSON = 'application/json'
1✔
24

25

26
class SettingsTest(InvenTreeTestCase):
1✔
27
    """Tests for the 'settings' model."""
28

29
    fixtures = [
1✔
30
        'settings',
31
    ]
32

33
    def test_settings_objects(self):
1✔
34
        """Test fixture loading and lookup for settings."""
35
        # There should be two settings objects in the database
36
        settings = InvenTreeSetting.objects.all()
1✔
37

38
        self.assertTrue(settings.count() >= 2)
1✔
39

40
        instance_name = InvenTreeSetting.objects.get(pk=1)
1✔
41
        self.assertEqual(instance_name.key, 'INVENTREE_INSTANCE')
1✔
42
        self.assertEqual(instance_name.value, 'My very first InvenTree Instance')
1✔
43

44
        # Check object lookup (case insensitive)
45
        self.assertEqual(InvenTreeSetting.get_setting_object('iNvEnTrEE_inSTanCE').pk, 1)
1✔
46

47
    def test_settings_functions(self):
1✔
48
        """Test settings functions and properties."""
49
        # define settings to check
50
        instance_ref = 'INVENTREE_INSTANCE'
1✔
51
        instance_obj = InvenTreeSetting.get_setting_object(instance_ref, cache=False)
1✔
52

53
        stale_ref = 'STOCK_STALE_DAYS'
1✔
54
        stale_days = InvenTreeSetting.get_setting_object(stale_ref, cache=False)
1✔
55

56
        report_size_obj = InvenTreeSetting.get_setting_object('REPORT_DEFAULT_PAGE_SIZE')
1✔
57
        report_test_obj = InvenTreeSetting.get_setting_object('REPORT_ENABLE_TEST_REPORT')
1✔
58

59
        # check settings base fields
60
        self.assertEqual(instance_obj.name, 'Server Instance Name')
1✔
61
        self.assertEqual(instance_obj.get_setting_name(instance_ref), 'Server Instance Name')
1✔
62
        self.assertEqual(instance_obj.description, 'String descriptor for the server instance')
1✔
63
        self.assertEqual(instance_obj.get_setting_description(instance_ref), 'String descriptor for the server instance')
1✔
64

65
        # check units
66
        self.assertEqual(instance_obj.units, '')
1✔
67
        self.assertEqual(instance_obj.get_setting_units(instance_ref), '')
1✔
68
        self.assertEqual(instance_obj.get_setting_units(stale_ref), 'days')
1✔
69

70
        # check as_choice
71
        self.assertEqual(instance_obj.as_choice(), 'My very first InvenTree Instance')
1✔
72
        self.assertEqual(report_size_obj.as_choice(), 'A4')
1✔
73

74
        # check is_choice
75
        self.assertEqual(instance_obj.is_choice(), False)
1✔
76
        self.assertEqual(report_size_obj.is_choice(), True)
1✔
77

78
        # check setting_type
79
        self.assertEqual(instance_obj.setting_type(), 'string')
1✔
80
        self.assertEqual(report_test_obj.setting_type(), 'boolean')
1✔
81
        self.assertEqual(stale_days.setting_type(), 'integer')
1✔
82

83
        # check as_int
84
        self.assertEqual(stale_days.as_int(), 0)
1✔
85
        self.assertEqual(instance_obj.as_int(), 'InvenTree')  # not an int -> return default
1✔
86

87
        # check as_bool
88
        self.assertEqual(report_test_obj.as_bool(), True)
1✔
89

90
        # check to_native_value
91
        self.assertEqual(stale_days.to_native_value(), 0)
1✔
92

93
    def test_allValues(self):
1✔
94
        """Make sure that the allValues functions returns correctly."""
95
        # define testing settings
96

97
        # check a few keys
98
        result = InvenTreeSetting.allValues()
1✔
99
        self.assertIn('INVENTREE_INSTANCE', result)
1✔
100
        self.assertIn('PART_COPY_TESTS', result)
1✔
101
        self.assertIn('STOCK_OWNERSHIP_CONTROL', result)
1✔
102
        self.assertIn('SIGNUP_GROUP', result)
1✔
103

104
    def run_settings_check(self, key, setting):
1✔
105
        """Test that all settings are valid.
106

107
        - Ensure that a name is set and that it is translated
108
        - Ensure that a description is set
109
        - Ensure that every setting key is valid
110
        - Ensure that a validator is supplied
111
        """
112
        self.assertTrue(type(setting) is dict)
1✔
113

114
        name = setting.get('name', None)
1✔
115

116
        self.assertIsNotNone(name)
1✔
117
        self.assertIn('django.utils.functional.lazy', str(type(name)))
1✔
118

119
        description = setting.get('description', None)
1✔
120

121
        self.assertIsNotNone(description)
1✔
122
        self.assertIn('django.utils.functional.lazy', str(type(description)))
1✔
123

124
        if key != key.upper():
1✔
125
            raise ValueError(f"Setting key '{key}' is not uppercase")  # pragma: no cover
126

127
        # Check that only allowed keys are provided
128
        allowed_keys = [
1✔
129
            'name',
130
            'description',
131
            'default',
132
            'validator',
133
            'hidden',
134
            'choices',
135
            'units',
136
            'requires_restart',
137
            'after_save',
138
            'before_save',
139
        ]
140

141
        for k in setting.keys():
1✔
142
            self.assertIn(k, allowed_keys)
1✔
143

144
        # Check default value for boolean settings
145
        validator = setting.get('validator', None)
1✔
146

147
        if validator is bool:
1✔
148
            default = setting.get('default', None)
1✔
149

150
            # Default value *must* be supplied for boolean setting!
151
            self.assertIsNotNone(default)
1✔
152

153
            # Default value for boolean must itself be a boolean
154
            self.assertIn(default, [True, False])
1✔
155

156
    def test_setting_data(self):
1✔
157
        """Test for settings data.
158

159
        - Ensure that every setting has a name, which is translated
160
        - Ensure that every setting has a description, which is translated
161
        """
162
        for key, setting in InvenTreeSetting.SETTINGS.items():
1✔
163

164
            try:
1✔
165
                self.run_settings_check(key, setting)
1✔
166
            except Exception as exc:  # pragma: no cover
167
                print(f"run_settings_check failed for global setting '{key}'")
168
                raise exc
169

170
        for key, setting in InvenTreeUserSetting.SETTINGS.items():
1✔
171
            try:
1✔
172
                self.run_settings_check(key, setting)
1✔
173
            except Exception as exc:  # pragma: no cover
174
                print(f"run_settings_check failed for user setting '{key}'")
175
                raise exc
176

177
    def test_defaults(self):
1✔
178
        """Populate the settings with default values."""
179
        for key in InvenTreeSetting.SETTINGS.keys():
1✔
180

181
            value = InvenTreeSetting.get_setting_default(key)
1✔
182

183
            InvenTreeSetting.set_setting(key, value, self.user)
1✔
184

185
            self.assertEqual(value, InvenTreeSetting.get_setting(key))
1✔
186

187
            # Any fields marked as 'boolean' must have a default value specified
188
            setting = InvenTreeSetting.get_setting_object(key)
1✔
189

190
            if setting.is_bool():
1✔
191
                if setting.default_value in ['', None]:
1✔
192
                    raise ValueError(f'Default value for boolean setting {key} not provided')  # pragma: no cover
193

194
                if setting.default_value not in [True, False]:
1✔
195
                    raise ValueError(f'Non-boolean default value specified for {key}')  # pragma: no cover
196

197
    def test_global_setting_caching(self):
1✔
198
        """Test caching operations for the global settings class"""
199

200
        key = 'PART_NAME_FORMAT'
1✔
201

202
        cache_key = InvenTreeSetting.create_cache_key(key)
1✔
203
        self.assertEqual(cache_key, 'InvenTreeSetting:PART_NAME_FORMAT')
1✔
204

205
        cache.clear()
1✔
206

207
        self.assertIsNone(cache.get(cache_key))
1✔
208

209
        # First request should set cache
210
        val = InvenTreeSetting.get_setting(key)
1✔
211
        self.assertEqual(cache.get(cache_key).value, val)
1✔
212

213
        for val in ['A', '{{ part.IPN }}', 'C']:
1✔
214
            # Check that the cached value is updated whenever the setting is saved
215
            InvenTreeSetting.set_setting(key, val, None)
1✔
216
            self.assertEqual(cache.get(cache_key).value, val)
1✔
217
            self.assertEqual(InvenTreeSetting.get_setting(key), val)
1✔
218

219
    def test_user_setting_caching(self):
1✔
220
        """Test caching operation for the user settings class"""
221

222
        cache.clear()
1✔
223

224
        # Generate a number of new usesr
225
        for idx in range(5):
1✔
226
            get_user_model().objects.create(
1✔
227
                username=f"User_{idx}",
228
                password="hunter42",
229
                email="email@dot.com",
230
            )
231

232
        key = 'SEARCH_PREVIEW_RESULTS'
1✔
233

234
        # Check that the settings are correctly cached for each separate user
235
        for user in get_user_model().objects.all():
1✔
236
            setting = InvenTreeUserSetting.get_setting_object(key, user=user)
1✔
237
            cache_key = setting.cache_key
1✔
238
            self.assertEqual(cache_key, f"InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}")
1✔
239
            InvenTreeUserSetting.set_setting(key, user.pk, None, user=user)
1✔
240
            self.assertIsNotNone(cache.get(cache_key))
1✔
241

242
        # Iterate through a second time, ensure the values have been cached correctly
243
        for user in get_user_model().objects.all():
1✔
244
            value = InvenTreeUserSetting.get_setting(key, user=user)
1✔
245
            self.assertEqual(value, user.pk)
1✔
246

247

248
class GlobalSettingsApiTest(InvenTreeAPITestCase):
1✔
249
    """Tests for the global settings API."""
250

251
    def setUp(self):
1✔
252
        """Ensure cache is cleared as part of test setup"""
253
        cache.clear()
1✔
254
        return super().setUp()
1✔
255

256
    def test_global_settings_api_list(self):
1✔
257
        """Test list URL for global settings."""
258
        url = reverse('api-global-setting-list')
1✔
259

260
        # Read out each of the global settings value, to ensure they are instantiated in the database
261
        for key in InvenTreeSetting.SETTINGS:
1✔
262
            InvenTreeSetting.get_setting_object(key, cache=False)
1✔
263

264
        response = self.get(url, expected_code=200)
1✔
265

266
        # Number of results should match the number of settings
267
        self.assertEqual(len(response.data), len(InvenTreeSetting.SETTINGS.keys()))
1✔
268

269
    def test_company_name(self):
1✔
270
        """Test a settings object lifecyle e2e."""
271
        setting = InvenTreeSetting.get_setting_object('INVENTREE_COMPANY_NAME')
1✔
272

273
        # Check default value
274
        self.assertEqual(setting.value, 'My company name')
1✔
275

276
        url = reverse('api-global-setting-detail', kwargs={'key': setting.key})
1✔
277

278
        # Test getting via the API
279
        for val in ['test', '123', 'My company nam3']:
1✔
280
            setting.value = val
1✔
281
            setting.save()
1✔
282

283
            response = self.get(url, expected_code=200)
1✔
284

285
            self.assertEqual(response.data['value'], val)
1✔
286

287
        # Test setting via the API
288
        for val in ['cat', 'hat', 'bat', 'mat']:
1✔
289
            response = self.patch(
1✔
290
                url,
291
                {
292
                    'value': val,
293
                },
294
                expected_code=200
295
            )
296

297
            self.assertEqual(response.data['value'], val)
1✔
298

299
            setting.refresh_from_db()
1✔
300
            self.assertEqual(setting.value, val)
1✔
301

302
    def test_api_detail(self):
1✔
303
        """Test that we can access the detail view for a setting based on the <key>."""
304
        # These keys are invalid, and should return 404
305
        for key in ["apple", "carrot", "dog"]:
1✔
306
            response = self.get(
1✔
307
                reverse('api-global-setting-detail', kwargs={'key': key}),
308
                expected_code=404,
309
            )
310

311
        key = 'INVENTREE_INSTANCE'
1✔
312
        url = reverse('api-global-setting-detail', kwargs={'key': key})
1✔
313

314
        InvenTreeSetting.objects.filter(key=key).delete()
1✔
315

316
        # Check that we can access a setting which has not previously been created
317
        self.assertFalse(InvenTreeSetting.objects.filter(key=key).exists())
1✔
318

319
        # Access via the API, and the default value should be received
320
        response = self.get(url, expected_code=200)
1✔
321

322
        self.assertEqual(response.data['value'], 'InvenTree')
1✔
323

324
        # Now, the object should have been created in the DB
325
        self.patch(
1✔
326
            url,
327
            {
328
                'value': 'My new title',
329
            },
330
            expected_code=200,
331
        )
332

333
        setting = InvenTreeSetting.objects.get(key=key)
1✔
334

335
        self.assertEqual(setting.value, 'My new title')
1✔
336

337
        # And retrieving via the API now returns the updated value
338
        response = self.get(url, expected_code=200)
1✔
339

340
        self.assertEqual(response.data['value'], 'My new title')
1✔
341

342

343
class UserSettingsApiTest(InvenTreeAPITestCase):
1✔
344
    """Tests for the user settings API."""
345

346
    def test_user_settings_api_list(self):
1✔
347
        """Test list URL for user settings."""
348
        url = reverse('api-user-setting-list')
1✔
349

350
        self.get(url, expected_code=200)
1✔
351

352
    def test_user_setting_invalid(self):
1✔
353
        """Test a user setting with an invalid key."""
354
        url = reverse('api-user-setting-detail', kwargs={'key': 'DONKEY'})
1✔
355

356
        self.get(url, expected_code=404)
1✔
357

358
    def test_user_setting_init(self):
1✔
359
        """Test we can retrieve a setting which has not yet been initialized."""
360
        key = 'HOMEPAGE_PART_LATEST'
1✔
361

362
        # Ensure it does not actually exist in the database
363
        self.assertFalse(InvenTreeUserSetting.objects.filter(key=key).exists())
1✔
364

365
        url = reverse('api-user-setting-detail', kwargs={'key': key})
1✔
366

367
        response = self.get(url, expected_code=200)
1✔
368

369
        self.assertEqual(response.data['value'], 'True')
1✔
370

371
        self.patch(url, {'value': 'False'}, expected_code=200)
1✔
372

373
        setting = InvenTreeUserSetting.objects.get(key=key, user=self.user)
1✔
374

375
        self.assertEqual(setting.value, 'False')
1✔
376
        self.assertEqual(setting.to_native_value(), False)
1✔
377

378
    def test_user_setting_boolean(self):
1✔
379
        """Test a boolean user setting value."""
380
        # Ensure we have a boolean setting available
381
        setting = InvenTreeUserSetting.get_setting_object(
1✔
382
            'SEARCH_PREVIEW_SHOW_PARTS',
383
            user=self.user
384
        )
385

386
        # Check default values
387
        self.assertEqual(setting.to_native_value(), True)
1✔
388

389
        # Fetch via API
390
        url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
1✔
391

392
        response = self.get(url, expected_code=200)
1✔
393

394
        self.assertEqual(response.data['pk'], setting.pk)
1✔
395
        self.assertEqual(response.data['key'], 'SEARCH_PREVIEW_SHOW_PARTS')
1✔
396
        self.assertEqual(response.data['description'], 'Display parts in search preview window')
1✔
397
        self.assertEqual(response.data['type'], 'boolean')
1✔
398
        self.assertEqual(len(response.data['choices']), 0)
1✔
399
        self.assertTrue(str2bool(response.data['value']))
1✔
400

401
        # Assign some truthy values
402
        for v in ['true', True, 1, 'y', 'TRUE']:
1✔
403
            self.patch(
1✔
404
                url,
405
                {
406
                    'value': str(v),
407
                },
408
                expected_code=200,
409
            )
410

411
            response = self.get(url, expected_code=200)
1✔
412

413
            self.assertTrue(str2bool(response.data['value']))
1✔
414

415
        # Assign some falsey values
416
        for v in ['false', False, '0', 'n', 'FalSe']:
1✔
417
            self.patch(
1✔
418
                url,
419
                {
420
                    'value': str(v),
421
                },
422
                expected_code=200,
423
            )
424

425
            response = self.get(url, expected_code=200)
1✔
426

427
            self.assertFalse(str2bool(response.data['value']))
1✔
428

429
        # Assign some invalid values
430
        for v in ['x', '', 'invalid', None, '-1', 'abcde']:
1✔
431
            response = self.patch(
1✔
432
                url,
433
                {
434
                    'value': str(v),
435
                },
436
                expected_code=200
437
            )
438

439
            # Invalid values evaluate to False
440
            self.assertFalse(str2bool(response.data['value']))
1✔
441

442
    def test_user_setting_choice(self):
1✔
443
        """Test a user setting with choices."""
444
        setting = InvenTreeUserSetting.get_setting_object(
1✔
445
            'DATE_DISPLAY_FORMAT',
446
            user=self.user
447
        )
448

449
        url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
1✔
450

451
        # Check default value
452
        self.assertEqual(setting.value, 'YYYY-MM-DD')
1✔
453

454
        # Check that a valid option can be assigned via the API
455
        for opt in ['YYYY-MM-DD', 'DD-MM-YYYY', 'MM/DD/YYYY']:
1✔
456

457
            self.patch(
1✔
458
                url,
459
                {
460
                    'value': opt,
461
                },
462
                expected_code=200,
463
            )
464

465
            setting.refresh_from_db()
1✔
466
            self.assertEqual(setting.value, opt)
1✔
467

468
        # Send an invalid option
469
        for opt in ['cat', 'dog', 12345]:
1✔
470

471
            response = self.patch(
1✔
472
                url,
473
                {
474
                    'value': opt,
475
                },
476
                expected_code=400,
477
            )
478

479
            self.assertIn('Chosen value is not a valid option', str(response.data))
1✔
480

481
    def test_user_setting_integer(self):
1✔
482
        """Test a integer user setting value."""
483
        setting = InvenTreeUserSetting.get_setting_object(
1✔
484
            'SEARCH_PREVIEW_RESULTS',
485
            user=self.user,
486
            cache=False,
487
        )
488

489
        url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
1✔
490

491
        # Check default value for this setting
492
        self.assertEqual(setting.value, 10)
1✔
493

494
        for v in [1, 9, 99]:
1✔
495
            setting.value = v
1✔
496
            setting.save()
1✔
497

498
            response = self.get(url)
1✔
499

500
            self.assertEqual(response.data['value'], str(v))
1✔
501

502
        # Set valid options via the api
503
        for v in [5, 15, 25]:
1✔
504
            self.patch(
1✔
505
                url,
506
                {
507
                    'value': v,
508
                },
509
                expected_code=200,
510
            )
511

512
            setting.refresh_from_db()
1✔
513
            self.assertEqual(setting.to_native_value(), v)
1✔
514

515
        # Set invalid options via the API
516
        # Note that this particular setting has a MinValueValidator(1) associated with it
517
        for v in [0, -1, -5]:
1✔
518

519
            response = self.patch(
1✔
520
                url,
521
                {
522
                    'value': v,
523
                },
524
                expected_code=400,
525
            )
526

527

528
class NotificationUserSettingsApiTest(InvenTreeAPITestCase):
1✔
529
    """Tests for the notification user settings API."""
530

531
    def test_api_list(self):
1✔
532
        """Test list URL."""
533
        url = reverse('api-notifcation-setting-list')
1✔
534

535
        self.get(url, expected_code=200)
1✔
536

537
    def test_setting(self):
1✔
538
        """Test the string name for NotificationUserSetting."""
539

540
        NotificationUserSetting.set_setting('NOTIFICATION_METHOD_MAIL', True, change_user=self.user, user=self.user)
1✔
541
        test_setting = NotificationUserSetting.get_setting_object('NOTIFICATION_METHOD_MAIL', user=self.user)
1✔
542
        self.assertEqual(str(test_setting), 'NOTIFICATION_METHOD_MAIL (for testuser): True')
1✔
543

544

545
class PluginSettingsApiTest(PluginMixin, InvenTreeAPITestCase):
1✔
546
    """Tests for the plugin settings API."""
547

548
    def test_plugin_list(self):
1✔
549
        """List installed plugins via API."""
550
        url = reverse('api-plugin-list')
1✔
551

552
        # Simple request
553
        self.get(url, expected_code=200)
1✔
554

555
        # Request with filter
556
        self.get(url, expected_code=200, data={'mixin': 'settings'})
1✔
557

558
    def test_api_list(self):
1✔
559
        """Test list URL."""
560
        url = reverse('api-plugin-setting-list')
1✔
561

562
        self.get(url, expected_code=200)
1✔
563

564
    def test_valid_plugin_slug(self):
1✔
565
        """Test that an valid plugin slug runs through."""
566
        # Activate plugin
567
        registry.set_plugin_state('sample', True)
1✔
568

569
        # get data
570
        url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'API_KEY'})
1✔
571
        response = self.get(url, expected_code=200)
1✔
572

573
        # check the right setting came through
574
        self.assertTrue(response.data['key'], 'API_KEY')
1✔
575
        self.assertTrue(response.data['plugin'], 'sample')
1✔
576
        self.assertTrue(response.data['type'], 'string')
1✔
577
        self.assertTrue(response.data['description'], 'Key required for accessing external API')
1✔
578

579
        # Failure mode tests
580

581
        # Non - exsistant plugin
582
        url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'})
1✔
583
        response = self.get(url, expected_code=404)
1✔
584
        self.assertIn("Plugin 'doesnotexist' not installed", str(response.data))
1✔
585

586
        # Wrong key
587
        url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'doesnotexsist'})
1✔
588
        response = self.get(url, expected_code=404)
1✔
589
        self.assertIn("Plugin 'sample' has no setting matching 'doesnotexsist'", str(response.data))
1✔
590

591
    def test_invalid_setting_key(self):
1✔
592
        """Test that an invalid setting key returns a 404."""
593
        ...
1✔
594

595
    def test_uninitialized_setting(self):
1✔
596
        """Test that requesting an uninitialized setting creates the setting."""
597
        ...
1✔
598

599

600
class WebhookMessageTests(TestCase):
1✔
601
    """Tests for webhooks."""
602

603
    def setUp(self):
1✔
604
        """Setup for all tests."""
605
        self.endpoint_def = WebhookEndpoint.objects.create()
1✔
606
        self.url = f'/api/webhook/{self.endpoint_def.endpoint_id}/'
1✔
607
        self.client = Client(enforce_csrf_checks=True)
1✔
608

609
    def test_bad_method(self):
1✔
610
        """Test that a wrong HTTP method does not work."""
611
        response = self.client.get(self.url)
1✔
612
        assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
1✔
613

614
    def test_missing_token(self):
1✔
615
        """Tests that token checks work."""
616
        response = self.client.post(
1✔
617
            self.url,
618
            content_type=CONTENT_TYPE_JSON,
619
        )
620

621
        assert response.status_code == HTTPStatus.FORBIDDEN
1✔
622
        assert (
1✔
623
            json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR
624
        )
625

626
    def test_bad_token(self):
1✔
627
        """Test that a wrong token is not working."""
628
        response = self.client.post(
1✔
629
            self.url,
630
            content_type=CONTENT_TYPE_JSON,
631
            **{'HTTP_TOKEN': '1234567fghj'},
632
        )
633

634
        assert response.status_code == HTTPStatus.FORBIDDEN
1✔
635
        assert (json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR)
1✔
636

637
    def test_bad_url(self):
1✔
638
        """Test that a wrongly formed url is not working."""
639
        response = self.client.post(
1✔
640
            '/api/webhook/1234/',
641
            content_type=CONTENT_TYPE_JSON,
642
        )
643

644
        assert response.status_code == HTTPStatus.NOT_FOUND
1✔
645

646
    def test_bad_json(self):
1✔
647
        """Test that malformed JSON is not accepted."""
648
        response = self.client.post(
1✔
649
            self.url,
650
            data="{'this': 123}",
651
            content_type=CONTENT_TYPE_JSON,
652
            **{'HTTP_TOKEN': str(self.endpoint_def.token)},
653
        )
654

655
        assert response.status_code == HTTPStatus.NOT_ACCEPTABLE
1✔
656
        assert (
1✔
657
            json.loads(response.content)['detail'] == 'Expecting property name enclosed in double quotes'
658
        )
659

660
    def test_success_no_token_check(self):
1✔
661
        """Test that a endpoint without a token set does not require one."""
662
        # delete token
663
        self.endpoint_def.token = ''
1✔
664
        self.endpoint_def.save()
1✔
665

666
        # check
667
        response = self.client.post(
1✔
668
            self.url,
669
            content_type=CONTENT_TYPE_JSON,
670
        )
671

672
        assert response.status_code == HTTPStatus.OK
1✔
673
        assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
1✔
674

675
    def test_bad_hmac(self):
1✔
676
        """Test that a malformed HMAC does not pass."""
677
        # delete token
678
        self.endpoint_def.token = ''
1✔
679
        self.endpoint_def.secret = '123abc'
1✔
680
        self.endpoint_def.save()
1✔
681

682
        # check
683
        response = self.client.post(
1✔
684
            self.url,
685
            content_type=CONTENT_TYPE_JSON,
686
        )
687

688
        assert response.status_code == HTTPStatus.FORBIDDEN
1✔
689
        assert (json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR)
1✔
690

691
    def test_success_hmac(self):
1✔
692
        """Test with a valid HMAC provided."""
693
        # delete token
694
        self.endpoint_def.token = ''
1✔
695
        self.endpoint_def.secret = '123abc'
1✔
696
        self.endpoint_def.save()
1✔
697

698
        # check
699
        response = self.client.post(
1✔
700
            self.url,
701
            content_type=CONTENT_TYPE_JSON,
702
            **{'HTTP_TOKEN': str('68MXtc/OiXdA5e2Nq9hATEVrZFpLb3Zb0oau7n8s31I=')},
703
        )
704

705
        assert response.status_code == HTTPStatus.OK
1✔
706
        assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
1✔
707

708
    def test_success(self):
1✔
709
        """Test full e2e webhook call.
710

711
        The message should go through and save the json payload.
712
        """
713
        response = self.client.post(
1✔
714
            self.url,
715
            data={"this": "is a message"},
716
            content_type=CONTENT_TYPE_JSON,
717
            **{'HTTP_TOKEN': str(self.endpoint_def.token)},
718
        )
719

720
        assert response.status_code == HTTPStatus.OK
1✔
721
        assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
1✔
722
        message = WebhookMessage.objects.get()
1✔
723
        assert message.body == {"this": "is a message"}
1✔
724

725

726
class NotificationTest(InvenTreeAPITestCase):
1✔
727
    """Tests for NotificationEntriy."""
728

729
    fixtures = [
1✔
730
        'users',
731
    ]
732

733
    def test_check_notification_entries(self):
1✔
734
        """Test that notification entries can be created."""
735
        # Create some notification entries
736

737
        self.assertEqual(NotificationEntry.objects.count(), 0)
1✔
738

739
        NotificationEntry.notify('test.notification', 1)
1✔
740

741
        self.assertEqual(NotificationEntry.objects.count(), 1)
1✔
742

743
        delta = timedelta(days=1)
1✔
744

745
        self.assertFalse(NotificationEntry.check_recent('test.notification', 2, delta))
1✔
746
        self.assertFalse(NotificationEntry.check_recent('test.notification2', 1, delta))
1✔
747

748
        self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta))
1✔
749

750
    def test_api_list(self):
1✔
751
        """Test list URL."""
752

753
        url = reverse('api-notifications-list')
1✔
754

755
        self.get(url, expected_code=200)
1✔
756

757
        # Test the OPTIONS endpoint for the 'api-notification-list'
758
        # Ref: https://github.com/inventree/InvenTree/pull/3154
759
        response = self.options(url)
1✔
760

761
        self.assertIn('DELETE', response.data['actions'])
1✔
762
        self.assertIn('GET', response.data['actions'])
1✔
763
        self.assertNotIn('POST', response.data['actions'])
1✔
764

765
        self.assertEqual(response.data['description'], 'List view for all notifications of the current user.')
1✔
766

767
        # POST action should fail (not allowed)
768
        response = self.post(url, {}, expected_code=405)
1✔
769

770
    def test_bulk_delete(self):
1✔
771
        """Tests for bulk deletion of user notifications"""
772

773
        from error_report.models import Error
1✔
774

775
        # Create some notification messages by throwing errors
776
        for _ii in range(10):
1✔
777
            Error.objects.create()
1✔
778

779
        # Check that messsages have been created
780
        messages = NotificationMessage.objects.all()
1✔
781

782
        # As there are three staff users (including the 'test' user) we expect 30 notifications
783
        # However, one user is marked as i nactive
784
        self.assertEqual(messages.count(), 20)
1✔
785

786
        # Only 10 messages related to *this* user
787
        my_notifications = messages.filter(user=self.user)
1✔
788
        self.assertEqual(my_notifications.count(), 10)
1✔
789

790
        # Get notification via the API
791
        url = reverse('api-notifications-list')
1✔
792
        response = self.get(url, {}, expected_code=200)
1✔
793
        self.assertEqual(len(response.data), 10)
1✔
794

795
        # Mark some as read
796
        for ntf in my_notifications[0:3]:
1✔
797
            ntf.read = True
1✔
798
            ntf.save()
1✔
799

800
        # Read out via API again
801
        response = self.get(
1✔
802
            url,
803
            {
804
                'read': True,
805
            },
806
            expected_code=200
807
        )
808

809
        # Check validity of returned data
810
        self.assertEqual(len(response.data), 3)
1✔
811
        for ntf in response.data:
1✔
812
            self.assertTrue(ntf['read'])
1✔
813

814
        # Now, let's bulk delete all 'unread' notifications via the API,
815
        # but only associated with the logged in user
816
        response = self.delete(
1✔
817
            url,
818
            {
819
                'filters': {
820
                    'read': False,
821
                }
822
            },
823
            expected_code=204,
824
        )
825

826
        # Only 7 notifications should have been deleted,
827
        # as the notifications associated with other users must remain untouched
828
        self.assertEqual(NotificationMessage.objects.count(), 13)
1✔
829
        self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3)
1✔
830

831

832
class CommonTest(InvenTreeAPITestCase):
1✔
833
    """Tests for the common config."""
834

835
    def test_restart_flag(self):
1✔
836
        """Test that the restart flag is reset on start."""
837
        import common.models
1✔
838
        from plugin import registry
1✔
839

840
        # set flag true
841
        common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
1✔
842

843
        # reload the app
844
        registry.reload_plugins()
1✔
845

846
        # now it should be false again
847
        self.assertFalse(common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'))
1✔
848

849
    def test_config_api(self):
1✔
850
        """Test config URLs."""
851
        # Not superuser
852
        self.get(reverse('api-config-list'), expected_code=403)
1✔
853

854
        # Turn into superuser
855
        self.user.is_superuser = True
1✔
856
        self.user.save()
1✔
857

858
        # Successfull checks
859
        data = [
1✔
860
            self.get(reverse('api-config-list'), expected_code=200).data[0],                                    # list endpoint
861
            self.get(reverse('api-config-detail', kwargs={'key': 'INVENTREE_DEBUG'}), expected_code=200).data,  # detail endpoint
862
        ]
863

864
        for item in data:
1✔
865
            self.assertEqual(item['key'], 'INVENTREE_DEBUG')
1✔
866
            self.assertEqual(item['env_var'], 'INVENTREE_DEBUG')
1✔
867
            self.assertEqual(item['config_key'], 'debug')
1✔
868

869
        # Turn into normal user again
870
        self.user.is_superuser = False
1✔
871
        self.user.save()
1✔
872

873

874
class ColorThemeTest(TestCase):
1✔
875
    """Tests for ColorTheme."""
876

877
    def test_choices(self):
1✔
878
        """Test that default choices are returned."""
879
        result = ColorTheme.get_color_themes_choices()
1✔
880

881
        # skip
882
        if not result:
1✔
883
            return
1✔
884
        self.assertIn(('default', 'Default'), result)
×
885

886
    def test_valid_choice(self):
1✔
887
        """Check that is_valid_choice works correctly."""
888
        result = ColorTheme.get_color_themes_choices()
1✔
889

890
        # skip
891
        if not result:
1✔
892
            return
1✔
893

894
        # check wrong reference
895
        self.assertFalse(ColorTheme.is_valid_choice('abcdd'))
×
896

897
        # create themes
898
        aa = ColorTheme.objects.create(user='aa', name='testname')
×
899
        ab = ColorTheme.objects.create(user='ab', name='darker')
×
900

901
        # check valid theme
902
        self.assertFalse(ColorTheme.is_valid_choice(aa))
×
903
        self.assertTrue(ColorTheme.is_valid_choice(ab))
×
904

905

906
class CurrencyAPITests(InvenTreeAPITestCase):
1✔
907
    """Unit tests for the currency exchange API endpoints"""
908

909
    def test_exchange_endpoint(self):
1✔
910
        """Test that the currency exchange endpoint works as expected"""
911

912
        response = self.get(reverse('api-currency-exchange'), expected_code=200)
1✔
913

914
        self.assertIn('base_currency', response.data)
1✔
915
        self.assertIn('exchange_rates', response.data)
1✔
916

917
    def test_refresh_endpoint(self):
1✔
918
        """Call the 'refresh currencies' endpoint"""
919

920
        from djmoney.contrib.exchange.models import Rate
1✔
921

922
        # Delete any existing exchange rate data
923
        Rate.objects.all().delete()
1✔
924

925
        # Updating via the external exchange may not work every time
926
        for _idx in range(5):
1✔
927
            self.post(reverse('api-currency-refresh'))
1✔
928

929
            # There should be some new exchange rate objects now
930
            if Rate.objects.all().exists():
1✔
931
                # Exit early
932
                return
1✔
933

934
            # Delay and try again
935
            time.sleep(10)
×
936

937
        raise TimeoutError("Could not refresh currency exchange data after 5 attempts")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc