From 2195a604387fdc79af8ab12d539462aa6f619901 Mon Sep 17 00:00:00 2001 From: Arona Jones Date: Wed, 27 Jan 2021 19:25:43 +0000 Subject: [PATCH] Use pytest as our test runner for better parallelism Also rewrote some asset tests to be in the pytest style. May do some more. Some warnings cleaned up in the process. --- .github/workflows/django.yml | 17 +- PyRIGS/urls.py | 6 +- PyRIGS/views.py | 12 +- RIGS/urls.py | 10 +- .../{test_assets.py => test_interaction.py} | 290 ------------------ assets/tests/test_unit.py | 277 +++++++++++++++++ assets/views.py | 13 +- pytest.ini | 3 +- requirements.txt | 3 +- versioning/tests/test_versioning.py | 3 - 10 files changed, 306 insertions(+), 328 deletions(-) rename assets/tests/{test_assets.py => test_interaction.py} (51%) create mode 100644 assets/tests/test_unit.py diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index ad548bd9..ec9f4c68 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -14,13 +14,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - test-group: ["RIGS.tests.test_unit RIGS.tests.test_models RIGS.tests.test_functional", "versioning.tests.test_versioning", "users.tests.test_users"] - parallel: [true] - include: - - test-group: "assets.tests.test_assets" - parallel: false - - test-group: "RIGS.tests.test_interaction" - parallel: false + test-group: ["RIGS/tests/", "versioning/tests/", "users/tests/", "assets/tests/"] steps: - uses: actions/checkout@v2 - name: Set up Python @@ -52,17 +46,14 @@ jobs: python manage.py check python manage.py makemigrations --check --dry-run - name: Run Tests - if: \!${{ matrix.parallel }} - run: coverage run manage.py test ${{ matrix.test-group }} --verbosity=2 + if: ${{ matrix.parallel }} + run: coverage run -m pytest -n 8 ${{ matrix.test-group }} - uses: actions/upload-artifact@v2 - if: failure() # Screenshots only make sense for the non-parallel, i.e. interaction tests anyway + if: failure() with: name: failure-screenshots ${{ matrix.test-group }} path: screenshots/ retention-days: 5 - - name: Run Tests (Parallel) - if: ${{ matrix.parallel }} - run: coverage run manage.py test ${{ matrix.test-group }} --parallel --verbosity=2 - name: Upload Coverage run: coveralls --service=github env: diff --git a/PyRIGS/urls.py b/PyRIGS/urls.py index b2369f9c..4b678b95 100644 --- a/PyRIGS/urls.py +++ b/PyRIGS/urls.py @@ -1,5 +1,5 @@ -from django.urls import path -from django.conf.urls import include, url +from django.urls import path, re_path +from django.conf.urls import include from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib.auth.decorators import login_required @@ -39,6 +39,6 @@ if settings.DEBUG: import debug_toolbar urlpatterns = [ - url(r'^__debug__/', include(debug_toolbar.urls)), + re_path(r'^__debug__/', include(debug_toolbar.urls)), path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")), ] + urlpatterns diff --git a/PyRIGS/views.py b/PyRIGS/views.py index a70e36da..3d835dcd 100644 --- a/PyRIGS/views.py +++ b/PyRIGS/views.py @@ -23,6 +23,8 @@ from functools import reduce from django.views.decorators.cache import never_cache, cache_page from django.utils.decorators import method_decorator +def is_ajax(request): + return request.headers.get('x-requested-with') == 'XMLHttpRequest' # Displays the current rig count along with a few other bits and pieces class Index(generic.TemplateView): @@ -151,7 +153,7 @@ class SecureAPIRequest(generic.View): class ModalURLMixin: def get_close_url(self, update, detail): - if self.request.is_ajax(): + if is_ajax(self.request): url = reverse_lazy('closemodal') update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk})) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) @@ -170,7 +172,7 @@ class GenericListView(generic.ListView): def get_context_data(self, **kwargs): context = super(GenericListView, self).get_context_data(**kwargs) context['page_title'] = self.model.__name__ + "s" - if self.request.is_ajax(): + if is_ajax(self.request): context['override'] = "base_ajax.html" return context @@ -202,7 +204,7 @@ class GenericDetailView(generic.DetailView): def get_context_data(self, **kwargs): context = super(GenericDetailView, self).get_context_data(**kwargs) context['page_title'] = "{} | {}".format(self.model.__name__, self.object.name) - if self.request.is_ajax(): + if is_ajax(self.request): context['override'] = "base_ajax.html" return context @@ -213,7 +215,7 @@ class GenericUpdateView(generic.UpdateView): def get_context_data(self, **kwargs): context = super(GenericUpdateView, self).get_context_data(**kwargs) context['page_title'] = "Edit {}".format(self.model.__name__) - if self.request.is_ajax(): + if is_ajax(self.request): context['override'] = "base_ajax.html" return context @@ -224,7 +226,7 @@ class GenericCreateView(generic.CreateView): def get_context_data(self, **kwargs): context = super(GenericCreateView, self).get_context_data(**kwargs) context['page_title'] = "Create {}".format(self.model.__name__) - if self.request.is_ajax(): + if is_ajax(self.request): context['override'] = "base_ajax.html" return context diff --git a/RIGS/urls.py b/RIGS/urls.py index e86dc56a..bef63083 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import url from django.contrib.auth.decorators import login_required -from django.urls import path +from django.urls import path, re_path from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import RedirectView from PyRIGS.decorators import (api_key_required, has_oembed, @@ -45,9 +45,9 @@ urlpatterns = [ path('rigboard/', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'), path('rigboard/calendar/', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'), - url(r'^rigboard/calendar/(?P(month|week|day))/$', + re_path(r'^rigboard/calendar/(?P(month|week|day))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'), - url(r'^rigboard/calendar/(?P(month|week|day))/(?P(\d{4}-\d{2}-\d{2}))/$', + re_path(r'^rigboard/calendar/(?P(month|week|day))/(?P(\d{4}-\d{2}-\d{2}))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'), path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')), @@ -130,11 +130,11 @@ urlpatterns = [ path('event//auth/preview/', permission_required_with_403('RIGS.change_event')(rigboard.EventAuthoriseRequestEmailPreview.as_view()), name='event_authorise_preview'), - url(r'^event/(?P\d+)/(?P[-:\w]+)/$', rigboard.EventAuthorise.as_view(), + re_path(r'^event/(?P\d+)/(?P[-:\w]+)/$', rigboard.EventAuthorise.as_view(), name='event_authorise'), # ICS Calendar - API key authentication - url(r'^ical/(?P\d+)/(?P\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), + re_path(r'^ical/(?P\d+)/(?P\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"), diff --git a/assets/tests/test_assets.py b/assets/tests/test_interaction.py similarity index 51% rename from assets/tests/test_assets.py rename to assets/tests/test_interaction.py index f73be0fb..0b618409 100644 --- a/assets/tests/test_assets.py +++ b/assets/tests/test_interaction.py @@ -338,293 +338,3 @@ class TestAssetAudit(AutoLoginTest): self.wait.until(animation_is_finished()) self.assertFalse(self.driver.find_element_by_id('modal').is_displayed()) self.assertIn("Asset with that ID does not exist!", self.page.error.text) - - -class TestSupplierValidation(TestCase): - @classmethod - def setUpTestData(cls): - cls.profile = rigsmodels.Profile.objects.create(username="SupplierValidationTest", email="SVT@test.com", is_superuser=True, is_active=True, is_staff=True) - cls.supplier = models.Supplier.objects.create(name="Gadgetron Corporation") - - def setUp(self): - self.profile.set_password('testuser') - self.profile.save() - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) - - def test_create(self): - url = reverse('supplier_create') - response = self.client.post(url) - self.assertFormError(response, 'form', 'name', 'This field is required.') - - def test_edit(self): - url = reverse('supplier_update', kwargs={'pk': self.supplier.pk}) - response = self.client.post(url, {'name': ""}) - self.assertFormError(response, 'form', 'name', 'This field is required.') - - -class Test404(TestCase): - @classmethod - def setUpTestData(cls): - cls.profile = rigsmodels.Profile.objects.create(username="404Test", email="404@test.com", is_superuser=True, is_active=True, is_staff=True) - - def setUp(self): - self.profile.set_password('testuser') - self.profile.save() - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) - - def test(self): - urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'} - for url_name in urls: - request_url = reverse(url_name, kwargs={'pk': "0000"}) - response = self.client.get(request_url, follow=True) - self.assertEqual(response.status_code, 404) - - -# TODO refactor this for all of RIGS -class TestAccessLevels(TestCase): - @override_settings(DEBUG=True) - def setUp(self): - super().setUp() - # Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production - call_command('generateSampleData') - # Create an asset with ID 1 to make things easier in loops (we can always use pk=1) - self.category = models.AssetCategory.objects.create(name="Number One") - self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True) - models.Asset.objects.create(asset_id="1", description="Half Price Fish", status=self.status, category=self.category, date_acquired=datetime.date(2020, 2, 1)) - - # Nothing should be available to the unauthenticated - def test_unauthenticated(self): - self.client.logout() - for url in filter(lambda url: url.name is not None and "json" not in str(url), urls.urlpatterns): - pattern = str(url.pattern) - request_url = "" - if ":pk>" in pattern: - request_url = reverse(url.name, kwargs={'pk': 1}) - else: - request_url = reverse(url.name) - if request_url: - response = self.client.get(request_url, follow=True, HTTP_HOST='example.com') - self.assertContains(response, 'Login') - - def test_basic_access(self): - self.assertTrue(self.client.login(username="basic", password="basic")) - - url = reverse('asset_list') - response = self.client.get(url) - # Check edit and duplicate buttons not shown in list - self.assertNotContains(response, 'Edit') - self.assertNotContains(response, 'Duplicate') - - url = reverse('asset_detail', kwargs={'pk': "9000"}) - response = self.client.get(url) - self.assertNotContains(response, 'Purchase Details') - self.assertNotContains(response, 'View Revision History') - - urls = {'asset_history', 'asset_update', 'asset_duplicate'} - for url_name in urls: - request_url = reverse(url_name, kwargs={'pk': "9000"}) - response = self.client.get(request_url, follow=True) - self.assertEqual(response.status_code, 403) - - request_url = reverse('supplier_create') - response = self.client.get(request_url, follow=True) - self.assertEqual(response.status_code, 403) - - request_url = reverse('supplier_update', kwargs={'pk': "1"}) - response = self.client.get(request_url, follow=True) - self.assertEqual(response.status_code, 403) - - def test_keyholder_access(self): - self.assertTrue(self.client.login(username="keyholder", password="keyholder")) - - url = reverse('asset_list') - response = self.client.get(url) - # Check edit and duplicate buttons shown in list - self.assertContains(response, 'Edit') - self.assertContains(response, 'Duplicate') - - url = reverse('asset_detail', kwargs={'pk': "9000"}) - response = self.client.get(url) - self.assertContains(response, 'Purchase Details') - self.assertContains(response, 'View Revision History') - - # def test_finance_access(self): Level not used in assets currently - - def test_page_titles(self): - self.assertTrue(self.client.login(username="superuser", password="superuser")) - for url in filter(lambda url: url.name is not None and not any(s in url.name for s in ["json", "embed"]), urls.urlpatterns): - request_url = "" - if ":pk>" in str(url.pattern): - request_url = reverse(url.name, kwargs={'pk': "1"}) - else: - request_url = reverse(url.name) - response = self.client.get(request_url) - if hasattr(response, "context_data") and "page_title" in response.context_data: - expected_title = response.context_data["page_title"] - self.assertContains(response, '{} | Rig Information Gathering System'.format(expected_title)) - - -class TestFormValidation(TestCase): - @classmethod - def setUpTestData(cls): - cls.profile = rigsmodels.Profile.objects.create(username="AssetCreateValidationTest", email="acvt@test.com", is_superuser=True, is_active=True, is_staff=True) - cls.category = models.AssetCategory.objects.create(name="Sound") - cls.status = models.AssetStatus.objects.create(name="Broken", should_show=True) - cls.asset = models.Asset.objects.create(asset_id="9999", description="The Office", status=cls.status, category=cls.category, date_acquired=datetime.date(2018, 6, 15)) - cls.connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3) - cls.cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=cls.connector, socket=cls.connector) - cls.cable_asset = models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=cls.status, category=cls.category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cls.cable_type, length=10, csa="1.5") - - def setUp(self): - self.profile.set_password('testuser') - self.profile.save() - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) - - def test_asset_create(self): - url = reverse('asset_create') - response = self.client.post(url, {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'}) - self.assertFormError(response, 'form', 'asset_id', 'This field is required.') - self.assertFormError(response, 'form', 'description', 'This field is required.') - self.assertFormError(response, 'form', 'status', 'This field is required.') - self.assertFormError(response, 'form', 'category', 'This field is required.') - - self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired') - self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative') - self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative') - - def test_cable_create(self): - url = reverse('asset_create') - response = self.client.post(url, {'asset_id': 'X$%A', 'is_cable': True}) - self.assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number') - - self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type') - self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') - self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') - - # Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway! - def test_asset_edit(self): - url = reverse('asset_update', kwargs={'pk': self.asset.asset_id}) - response = self.client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""}) - # self.assertFormError(response, 'form', 'asset_id', 'This field is required.') - self.assertFormError(response, 'form', 'description', 'This field is required.') - self.assertFormError(response, 'form', 'status', 'This field is required.') - self.assertFormError(response, 'form', 'category', 'This field is required.') - - self.assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired') - self.assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative') - self.assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative') - - def test_cable_edit(self): - url = reverse('asset_update', kwargs={'pk': self.cable_asset.asset_id}) - # TODO Why do I have to send is_cable=True here? - response = self.client.post(url, {'is_cable': True, 'length': -3, 'csa': -3}) - - # TODO Can't figure out how to select the 'none' option... - # self.assertFormError(response, 'form', 'cable_type', 'A cable must have a type') - self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') - self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') - - def test_asset_duplicate(self): - url = reverse('asset_duplicate', kwargs={'pk': self.cable_asset.asset_id}) - response = self.client.post(url, {'is_cable': True, 'length': 0, 'csa': 0}) - - self.assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') - self.assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') - - -class TestSampleDataGenerator(TestCase): - @override_settings(DEBUG=True) - def test_generate_sample_data(self): - # Run the management command and check there are no exceptions - call_command('generateSampleAssetsData') - - # Check there are lots - self.assertTrue(models.Asset.objects.all().count() > 50) - self.assertTrue(models.Supplier.objects.all().count() > 50) - - @override_settings(DEBUG=True) - def test_delete_sample_data(self): - call_command('deleteSampleData') - - self.assertTrue(models.Asset.objects.all().count() == 0) - self.assertTrue(models.Supplier.objects.all().count() == 0) - - def test_production_exception(self): - from django.core.management.base import CommandError - - self.assertRaisesRegex(CommandError, ".*production", call_command, 'generateSampleAssetsData') - self.assertRaisesRegex(CommandError, ".*production", call_command, 'deleteSampleData') - - -class TestEmbeddedViews(TestCase): - @classmethod - def setUpTestData(cls): - cls.profile = rigsmodels.Profile.objects.create(username="EmbeddedViewsTest", email="embedded@test.com", is_superuser=True, is_active=True, is_staff=True) - - working = models.AssetStatus.objects.create(name="Working", should_show=True) - lighting = models.AssetCategory.objects.create(name="Lighting") - - cls.assets = { - 1: models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26)) - } - - def setUp(self): - self.profile.set_password('testuser') - self.profile.save() - - def testLoginRedirect(self): - request_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id}) - expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url) - - # Request the page and check it redirects - response = self.client.get(request_url, follow=True) - self.assertRedirects(response, expected_url, status_code=302, target_status_code=200) - - # Now login - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) - - # And check that it no longer redirects - response = self.client.get(request_url, follow=True) - self.assertEqual(len(response.redirect_chain), 0) - - def testLoginCookieWarning(self): - login_url = reverse('login_embed') - response = self.client.post(login_url, follow=True) - self.assertContains(response, "Cookies do not seem to be enabled") - - def testXFrameHeaders(self): - asset_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id}) - login_url = reverse('login_embed') - - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) - - response = self.client.get(asset_url, follow=True) - with self.assertRaises(KeyError): - response._headers["X-Frame-Options"] - - response = self.client.get(login_url, follow=True) - with self.assertRaises(KeyError): - response._headers["X-Frame-Options"] - - def testOEmbed(self): - asset_url = reverse('asset_detail', kwargs={'pk': self.assets[1].asset_id}) - asset_embed_url = reverse('asset_embed', kwargs={'pk': self.assets[1].asset_id}) - oembed_url = reverse('asset_oembed', kwargs={'pk': self.assets[1].asset_id}) - - alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999}) - alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999}) - - # Test the meta tag is in place - response = self.client.get(asset_url, follow=True, HTTP_HOST='example.com') - self.assertContains(response, ' 50 + assert models.Supplier.objects.all().count() > 50 + +@override_settings(DEBUG=True) + +def test_delete_sample_data(client): + call_command('deleteSampleData') + + assert models.Asset.objects.all().count() == 0 + assert models.Supplier.objects.all().count() == 0 + + +def test_production_exception(client): + from django.core.management.base import CommandError + + with pytest.raises(CommandError, match=".*production"): + call_command('generateSampleAssetsData') + call_command('deleteSampleData') + + +def test_asset_create(client, django_user_model): + login(client, django_user_model) + response = client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'salvage_value': '-30'}) + assertFormError(response, 'form', 'asset_id', 'This field is required.') + assertFormError(response, 'form', 'description', 'This field is required.') + assertFormError(response, 'form', 'status', 'This field is required.') + assertFormError(response, 'form', 'category', 'This field is required.') + + assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired') + assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative') + assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative') + + +def test_cable_create(client, django_user_model): + login(client, django_user_model) + response = client.post(reverse('asset_create'), {'asset_id': 'X$%A', 'is_cable': True}) + assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number') + + assertFormError(response, 'form', 'cable_type', 'A cable must have a type') + assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') + assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') + +# Given that validation is done at model level it *shouldn't* need retesting...gonna do it anyway! +def test_asset_edit(client, django_user_model): + login(client, django_user_model) + url = reverse('asset_update', kwargs={'pk': create_test_asset().asset_id}) + response = client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'salvage_value': '-50', 'description': "", 'status': "", 'category': ""}) + # assertFormError(response, 'form', 'asset_id', 'This field is required.') + assertFormError(response, 'form', 'description', 'This field is required.') + assertFormError(response, 'form', 'status', 'This field is required.') + assertFormError(response, 'form', 'category', 'This field is required.') + assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired') + assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative') + assertFormError(response, 'form', 'salvage_value', 'A price cannot be negative') + +def test_cable_edit(client, django_user_model): + login(client, django_user_model) + url = reverse('asset_update', kwargs={'pk': create_test_cable().asset_id}) + # TODO Why do I have to send is_cable=True here? + response = client.post(url, {'is_cable': True, 'length': -3, 'csa': -3}) + + # TODO Can't figure out how to select the 'none' option... + # assertFormError(response, 'form', 'cable_type', 'A cable must have a type') + assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') + assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') + +def test_asset_duplicate(client, django_user_model): + login(client, django_user_model) + url = reverse('asset_duplicate', kwargs={'pk': create_test_cable().asset_id}) + response = client.post(url, {'is_cable': True, 'length': 0, 'csa': 0}) + + assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') + assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') + +@override_settings(DEBUG=True) +def create_asset_one(): + # Shortcut to create the levels - bonus side effect of testing the command (hopefully) matches production + call_command('generateSampleData') + # Create an asset with ID 1 to make things easier in loops (we can always use pk=1) + category = models.AssetCategory.objects.create(name="Number One") + status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True) + return models.Asset.objects.create(asset_id="1", description="Half Price Fish", status=status, category=category, date_acquired=datetime.date(2020, 2, 1)) + +# Nothing should be available to the unauthenticated +def test_unauthenticated(client): + for url in filter(lambda url: url.name is not None and "json" not in str(url), urls.urlpatterns): + pattern = str(url.pattern) + request_url = "" + if ":pk>" in pattern: + request_url = reverse(url.name, kwargs={'pk': 1}) + else: + request_url = reverse(url.name) + if request_url: + response = client.get(request_url, follow=True, HTTP_HOST='example.com') + # TODO Check the URL here + assert response_contains(response, 'Login') + +def test_basic_access(client): + create_asset_one() + client.login(username="basic", password="basic") + + url = reverse('asset_list') + response = client.get(url) + # Check edit and duplicate buttons NOT shown in list + assert not response_contains(response, 'Edit') + assert not response_contains(response, 'Duplicate') + + url = reverse('asset_detail', kwargs={'pk': "9000"}) + response = client.get(url) + assert not response_contains(response, 'Purchase Details') + assert not response_contains(response, 'View Revision History') + + urls = {'asset_history', 'asset_update', 'asset_duplicate'} + for url_name in urls: + request_url = reverse(url_name, kwargs={'pk': "9000"}) + response = client.get(request_url, follow=True) + assert response.status_code == 403 + + request_url = reverse('supplier_create') + response = client.get(request_url, follow=True) + assert response.status_code == 403 + + request_url = reverse('supplier_update', kwargs={'pk': "1"}) + response = client.get(request_url, follow=True) + assert response.status_code == 403 + +def test_keyholder_access(client): + create_asset_one() + client.login(username="keyholder", password="keyholder") + + url = reverse('asset_list') + response = client.get(url) + # Check edit and duplicate buttons shown in list + assert response_contains(response, 'Edit') + assert response_contains(response, 'Duplicate') + + url = reverse('asset_detail', kwargs={'pk': "9000"}) + response = client.get(url) + assert response_contains(response, 'Purchase Details') + assert response_contains(response, 'View Revision History') + +def test_page_titles(admin_client): + for url in filter(lambda url: url.name is not None and not any(s in url.name for s in ["json", "embed"]), urls.urlpatterns): + request_url = "" + if ":pk>" in str(url.pattern): + request_url = reverse(url.name, kwargs={'pk': "1"}) + else: + request_url = reverse(url.name) + response = admin_client.get(request_url) + if hasattr(response, "context_data") and "page_title" in response.context_data: + expected_title = response.context_data["page_title"] + assert response_contains(response, '{} | Rig Information Gathering System'.format(expected_title)) diff --git a/assets/views.py b/assets/views.py index 3461b020..d5b61999 100644 --- a/assets/views.py +++ b/assets/views.py @@ -14,10 +14,9 @@ from django.utils.decorators import method_decorator from django.views import generic from django.views.decorators.csrf import csrf_exempt from versioning import versioning -from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin +from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, is_ajax from itertools import chain - @method_decorator(csrf_exempt, name='dispatch') class AssetList(LoginRequiredMixin, generic.ListView): model = models.Asset @@ -121,7 +120,7 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView): return context def get_success_url(self): - if self.request.is_ajax(): + if is_ajax(self.request): url = reverse_lazy('closemodal') update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk})) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) @@ -235,7 +234,7 @@ class SupplierList(GenericListView): context['edit'] = 'supplier_update' context['can_edit'] = self.request.user.has_perm('assets.change_supplier') context['detail'] = 'supplier_detail' - if self.request.is_ajax(): + if is_ajax(self.request): context['override'] = "base_ajax.html" else: context['override'] = 'base_assets.html' @@ -263,7 +262,7 @@ class SupplierDetail(GenericDetailView): context['detail_link'] = 'supplier_detail' context['associated'] = 'partials/associated_assets.html' context['associated2'] = '' - if self.request.is_ajax(): + if is_ajax(self.request): context['override'] = "base_ajax.html" else: context['override'] = 'base_assets.html' @@ -277,7 +276,7 @@ class SupplierCreate(GenericCreateView, ModalURLMixin): def get_context_data(self, **kwargs): context = super(SupplierCreate, self).get_context_data(**kwargs) - if self.request.is_ajax(): + if is_ajax(self.request): context['override'] = "base_ajax.html" else: context['override'] = 'base_assets.html' @@ -293,7 +292,7 @@ class SupplierUpdate(GenericUpdateView, ModalURLMixin): def get_context_data(self, **kwargs): context = super(SupplierUpdate, self).get_context_data(**kwargs) - if self.request.is_ajax(): + if is_ajax(self.request): context['override'] = "base_ajax.html" else: context['override'] = 'base_assets.html' diff --git a/pytest.ini b/pytest.ini index 95b8798b..8f8bc103 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,3 @@ [pytest] -DJANGO_SETTINGS_MODULE = myproject.settings \ No newline at end of file +DJANGO_SETTINGS_MODULE = PyRIGS.settings +# FAIL_INVALID_TEMPLATE_VARS = True diff --git a/requirements.txt b/requirements.txt index 4dc551f3..abe17093 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,7 +42,6 @@ premailer==3.7.0 progress==1.5 psutil==5.8.0 psycopg2==2.8.6 -psycopg2-binary==2.8.6 Pygments==2.7.4 pyparsing==2.4.7 PyPDF2==1.26.0 @@ -50,6 +49,8 @@ PyPOM==2.2.0 python-dateutil==2.8.1 pytoml==0.1.21 pytz==2020.5 +pytest_django==4.1.0 +pytest-xdist==2.2.0 raven==6.10.0 reportlab==3.5.60 requests==2.25.1 diff --git a/versioning/tests/test_versioning.py b/versioning/tests/test_versioning.py index a907b252..98ac04b2 100644 --- a/versioning/tests/test_versioning.py +++ b/versioning/tests/test_versioning.py @@ -305,6 +305,3 @@ class TestVersioningViews(TestCase): response = self.client.get(request_url, follow=True) self.assertContains(response, "Test Person") self.assertEqual(response.status_code, 200) - - -# Functional Tests