|
@@ -3,10 +3,13 @@ from contextlib import contextmanager
|
|
|
from unittest.mock import patch
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
|
+from django.core.files.uploadedfile import SimpleUploadedFile
|
|
|
from django.http import HttpResponse
|
|
from django.http import HttpResponse
|
|
|
from django.test import Client, TransactionTestCase, override_settings
|
|
from django.test import Client, TransactionTestCase, override_settings
|
|
|
from django.urls import reverse
|
|
from django.urls import reverse
|
|
|
|
|
+from django.utils import timezone
|
|
|
|
|
|
|
|
|
|
+from core.models import DataFile, DataSource
|
|
|
from dcim.choices import DeviceStatusChoices, InterfaceTypeChoices, SiteStatusChoices
|
|
from dcim.choices import DeviceStatusChoices, InterfaceTypeChoices, SiteStatusChoices
|
|
|
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site, VirtualChassis
|
|
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site, VirtualChassis
|
|
|
from extras.events import enqueue_event
|
|
from extras.events import enqueue_event
|
|
@@ -14,6 +17,7 @@ from extras.models import ImageAttachment
|
|
|
from extras.validators import CustomValidator
|
|
from extras.validators import CustomValidator
|
|
|
from ipam.choices import VLANStatusChoices
|
|
from ipam.choices import VLANStatusChoices
|
|
|
from ipam.models import VLAN, VLANGroup
|
|
from ipam.models import VLAN, VLANGroup
|
|
|
|
|
+from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices
|
|
|
from netbox.constants import EMPTY_TABLE_TEXT
|
|
from netbox.constants import EMPTY_TABLE_TEXT
|
|
|
from netbox.search.backends import search_backend
|
|
from netbox.search.backends import search_backend
|
|
|
from users.models import User
|
|
from users.models import User
|
|
@@ -29,6 +33,103 @@ class HomeViewTestCase(TestCase):
|
|
|
self.assertHttpStatus(response, 200)
|
|
self.assertHttpStatus(response, 200)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class BulkImportViewTabsTestCase(TestCase):
|
|
|
|
|
+ """
|
|
|
|
|
+ Verify which bulk import tab is rendered active.
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ def test_get_activates_direct_import(self):
|
|
|
|
|
+ """An initial GET activates the Direct Import tab."""
|
|
|
|
|
+ self.add_permissions('dcim.add_site')
|
|
|
|
|
+
|
|
|
|
|
+ response = self.client.get(reverse('dcim:site_bulk_import'))
|
|
|
|
|
+
|
|
|
|
|
+ self.assertHttpStatus(response, 200)
|
|
|
|
|
+ content = response.content.decode()
|
|
|
|
|
+ self.assertIn('<div class="tab-pane show active" id="import-form"', content)
|
|
|
|
|
+ self.assertIn('<div class="tab-pane show" id="upload-form"', content)
|
|
|
|
|
+
|
|
|
|
|
+ def test_rejected_upload_activates_its_tab(self):
|
|
|
|
|
+ """A rejected file upload activates the Upload File tab rather than the default."""
|
|
|
|
|
+ self.add_permissions('dcim.add_site')
|
|
|
|
|
+ upload_file = SimpleUploadedFile('sites.csv', b'no delimiters here', content_type='text/plain')
|
|
|
|
|
+
|
|
|
|
|
+ response = self.client.post(reverse('dcim:site_bulk_import'), {
|
|
|
|
|
+ 'import_method': ImportMethodChoices.UPLOAD,
|
|
|
|
|
+ 'upload_file': upload_file,
|
|
|
|
|
+ 'format': ImportFormatChoices.AUTO,
|
|
|
|
|
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ self.assertHttpStatus(response, 200)
|
|
|
|
|
+ content = response.content.decode()
|
|
|
|
|
+ self.assertIn('<div class="tab-pane show active" id="upload-form"', content)
|
|
|
|
|
+ self.assertIn('<div class="tab-pane show" id="import-form"', content)
|
|
|
|
|
+
|
|
|
|
|
+ def test_rejected_json_upload_reports_on_the_upload_field(self):
|
|
|
|
|
+ """A malformed JSON upload reports its parse error inside the Upload File tab."""
|
|
|
|
|
+ self.add_permissions('dcim.add_site')
|
|
|
|
|
+ upload_file = SimpleUploadedFile('sites.json', b'{', content_type='application/json')
|
|
|
|
|
+
|
|
|
|
|
+ response = self.client.post(reverse('dcim:site_bulk_import'), {
|
|
|
|
|
+ 'import_method': ImportMethodChoices.UPLOAD,
|
|
|
|
|
+ 'upload_file': upload_file,
|
|
|
|
|
+ 'format': ImportFormatChoices.AUTO,
|
|
|
|
|
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ self.assertHttpStatus(response, 200)
|
|
|
|
|
+ content = response.content.decode()
|
|
|
|
|
+ self.assertIn('<div class="tab-pane show active" id="upload-form"', content)
|
|
|
|
|
+ self.assertIn('id="id_upload_file_errors"', content)
|
|
|
|
|
+ self.assertIn('Invalid JSON data', content)
|
|
|
|
|
+
|
|
|
|
|
+ def test_rejected_json_data_file_reports_on_the_data_file_field(self):
|
|
|
|
|
+ """A malformed JSON data file reports its parse error inside the Data File tab."""
|
|
|
|
|
+ self.add_permissions('dcim.add_site')
|
|
|
|
|
+ data_source = DataSource.objects.create(
|
|
|
|
|
+ name='Data Source 1',
|
|
|
|
|
+ type='local',
|
|
|
|
|
+ source_url='file:///var/tmp/source1/'
|
|
|
|
|
+ )
|
|
|
|
|
+ data_file = DataFile.objects.create(
|
|
|
|
|
+ source=data_source,
|
|
|
|
|
+ path='sites.json',
|
|
|
|
|
+ last_updated=timezone.now(),
|
|
|
|
|
+ size=1,
|
|
|
|
|
+ hash='442da078f0111cbdf42f21903724f6597c692535f55bdfbbea758a1ae99ad9e1',
|
|
|
|
|
+ data=b'{'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ response = self.client.post(reverse('dcim:site_bulk_import'), {
|
|
|
|
|
+ 'import_method': ImportMethodChoices.DATA_FILE,
|
|
|
|
|
+ 'data_source': data_source.pk,
|
|
|
|
|
+ 'data_file': data_file.pk,
|
|
|
|
|
+ 'format': ImportFormatChoices.AUTO,
|
|
|
|
|
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ self.assertHttpStatus(response, 200)
|
|
|
|
|
+ content = response.content.decode()
|
|
|
|
|
+ self.assertIn('<div class="tab-pane show active" id="datafile-form"', content)
|
|
|
|
|
+ self.assertIn('id="id_data_file_errors"', content)
|
|
|
|
|
+ self.assertIn('Invalid JSON data', content)
|
|
|
|
|
+
|
|
|
|
|
+ def test_unknown_import_method_falls_back_to_direct(self):
|
|
|
|
|
+ """An unrecognised import method leaves the Direct Import tab active."""
|
|
|
|
|
+ self.add_permissions('dcim.add_site')
|
|
|
|
|
+
|
|
|
|
|
+ response = self.client.post(reverse('dcim:site_bulk_import'), {
|
|
|
|
|
+ 'import_method': 'bogus',
|
|
|
|
|
+ 'format': ImportFormatChoices.AUTO,
|
|
|
|
|
+ 'csv_delimiter': CSVDelimiterChoices.AUTO,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ self.assertHttpStatus(response, 200)
|
|
|
|
|
+ content = response.content.decode()
|
|
|
|
|
+ self.assertIn('<div class="tab-pane show active" id="import-form"', content)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class SearchViewTestCase(TestCase):
|
|
class SearchViewTestCase(TestCase):
|
|
|
|
|
|
|
|
@classmethod
|
|
@classmethod
|