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