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.
This commit is contained in:
2021-01-27 19:25:43 +00:00
parent 12c4b63947
commit 2195a60438
10 changed files with 306 additions and 328 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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<view>(month|week|day))/$',
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/$',
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
re_path(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\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/<int:pk>/auth/preview/',
permission_required_with_403('RIGS.change_event')(rigboard.EventAuthoriseRequestEmailPreview.as_view()),
name='event_authorise_preview'),
url(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
re_path(r'^event/(?P<pk>\d+)/(?P<hmac>[-:\w]+)/$', rigboard.EventAuthorise.as_view(),
name='event_authorise'),
# ICS Calendar - API key authentication
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
re_path(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()),
name="ics_calendar"),

View File

@@ -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, '<title>{} | Rig Information Gathering System</title>'.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, '<link rel="alternate" type="application/json+oembed"')
self.assertContains(response, oembed_url)
# Test that the JSON exists
response = self.client.get(oembed_url, follow=True, HTTP_HOST='example.com')
self.assertEqual(response.status_code, 200)
self.assertContains(response, asset_embed_url)
# Should also work for non-existant
response = self.client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
self.assertEqual(response.status_code, 200)
self.assertContains(response, alt_asset_embed_url)

277
assets/tests/test_unit.py Normal file
View File

@@ -0,0 +1,277 @@
from django.core.management import call_command
from assets import models
from django.test.utils import override_settings
from django.urls import reverse
from urllib.parse import urlparse
from assets import models, urls
from reversion import revisions as reversion
import datetime
from django.utils import timezone
from django.test import tag
import pytest
from pytest_django.asserts import assertFormError, assertRedirects
pytestmark = pytest.mark.django_db # TODO
def response_contains(response, needle):
return needle in str(response.content)
def login(client, django_user_model):
pwd = 'testuser'
usr = "TestUser"
profile = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd, is_superuser=True, is_active=True, is_staff=True)
assert client.login(username=usr, password=pwd)
def create_test_asset():
working = models.AssetStatus.objects.create(name="Working", should_show=True)
lighting = models.AssetCategory.objects.create(name="Lighting")
asset = models.Asset.objects.create(asset_id="1991", description="Spaceflower", status=working, category=lighting, date_acquired=datetime.date(1991, 12, 26))
return asset
def create_test_cable():
category = models.AssetCategory.objects.create(name="Sound")
status = models.AssetStatus.objects.create(name="Broken", should_show=True)
connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3)
cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector)
return models.Asset.objects.create(asset_id="666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5")
def test_supplier_create(client, django_user_model):
login(client, django_user_model)
url = reverse('supplier_create')
response = client.post(url)
assertFormError(response, 'form', 'name', 'This field is required.')
def test_supplier_edit(client, django_user_model):
login(client, django_user_model)
supplier = models.Supplier.objects.create(name="Gadgetron Corporation")
url = reverse('supplier_update', kwargs={'pk': supplier.pk})
response = client.post(url, {'name': ""})
assertFormError(response, 'form', 'name', 'This field is required.')
def test_404(client, django_user_model):
login(client, django_user_model)
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 = client.get(request_url, follow=True)
assert response.status_code == 404
def test_embed_login_redirect(client, django_user_model):
request_url = reverse('asset_embed', kwargs={'pk': create_test_asset().asset_id})
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
# Request the page and check it redirects
response = client.get(request_url, follow=True)
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
# Now login
login(client, django_user_model)
# And check that it no longer redirects
response = client.get(request_url, follow=True)
assert len(response.redirect_chain) == 0
def test_login_cookie_warning(client, django_user_model):
login_url = reverse('login_embed')
response = client.post(login_url, follow=True)
assert "Cookies do not seem to be enabled" in str(response.content)
def test_x_frame_headers(client, django_user_model):
asset_url = reverse('asset_embed', kwargs={'pk': create_test_asset().asset_id})
login_url = reverse('login_embed')
login(client, django_user_model)
response = client.get(asset_url, follow=True)
with pytest.raises(KeyError):
response._headers["X-Frame-Options"]
response = client.get(login_url, follow=True)
with pytest.raises(KeyError):
response._headers["X-Frame-Options"]
def test_oembed(client):
asset = create_test_asset()
asset_url = reverse('asset_detail', kwargs={'pk': asset.asset_id})
asset_embed_url = reverse('asset_embed', kwargs={'pk': asset.asset_id})
oembed_url = reverse('asset_oembed', kwargs={'pk': asset.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 = client.get(asset_url, follow=True, HTTP_HOST='example.com')
assert '<link rel="alternate" type="application/json+oembed"' in str(response.content)
assert oembed_url in str(response.content)
# Test that the JSON exists
response = client.get(oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assert asset_embed_url in str(response.content)
# Should also work for non-existant
response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
assert response.status_code == 200
assert alt_asset_embed_url in str(response.content)
@override_settings(DEBUG=True)
def test_generate_sample_data(client):
# Run the management command and check there are no exceptions
call_command('generateSampleAssetsData')
# Check there are lots
assert models.Asset.objects.all().count() > 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, '<title>{} | Rig Information Gathering System</title>'.format(expected_title))

View File

@@ -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'

View File

@@ -1,2 +1,3 @@
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
DJANGO_SETTINGS_MODULE = PyRIGS.settings
# FAIL_INVALID_TEMPLATE_VARS = True

View File

@@ -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

View File

@@ -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