diff --git a/.gitignore b/.gitignore index c3fb0bf..33e22ca 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ target/ local_settings.py *.db *.tar.gz +Pipfile # IDE specific files .idea diff --git a/.travis.yml b/.travis.yml index 0fd25ff..0195815 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ before_script: script: - jslint django_x509/static/django-x509/js/*.js - coverage run --source=django_x509 runtests.py - - SAMPLE_APP=1 ./runtests.py + - SAMPLE_APP=1 ./runtests.py --parallel --keepdb after_success: coveralls diff --git a/README.rst b/README.rst index 6515de3..4ee9aa1 100644 --- a/README.rst +++ b/README.rst @@ -329,43 +329,71 @@ be protected with authentication or not. Extending django-x509 --------------------- -The django app ``tests/openwisp2/sample_x509/`` adds some changes on -top of the ``django-x509`` module with the sole purpose of testing the -module's extensibility. It can be used as a sample for extending -``django-x509`` functionality in your own application. +One of the core values of the OpenWISP project is `Software Reusability `_, +for this reason *django-x509* provides a set of base classes +which can be imported, extended and reused to create derivative apps. -*django-x509* provides a set of models and admin classes which can be imported, -extended and reused by third party apps. +In order to implement your custom version of *django-x509*, +you need to perform the steps described in this section. -To extend *django-x509*, **you MUST NOT** add it to ``settings.INSTALLED_APPS``, -but you must create your own app (which goes into ``settings.INSTALLED_APPS``), import the -base classes from django-x509 and add your customizations. +When in doubt, the code in the `test project `_ +and the `sample app `_ +will serve you as source of truth: +just replicate and adapt that code to get a basic derivative of +*django-x509* working. -In order to help django find the static files and templates of *django-x509*, -you need to perform the steps described below. +**Premise**: if you plan on using a customized version of this module, +we suggest to start with it since the beginning, because migrating your data +from the default module to your extended version may be time consuming. -**Premise**: if you plan on using a customized version of this module, we suggest -to start with it since the beginning, because migrating your data from the default -module to your extended version may be time consuming. +1. Initialize your custom module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -1. Install ``openwisp-utils`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The first thing you need to do is to create a new django app which will +contain your custom version of *django-x509*. + +A django app is nothing more than a +`python package `_ +(a directory of python scripts), in the following examples we'll call this django app +``myx509``, but you can name it how you want:: + + django-admin startapp myx509 + +Keep in mind that the command mentioned above must be called from a directory +which is available in your `PYTHON_PATH `_ +so that you can then import the result into your project. + +Now you need to add ``myx509`` to ``INSTALLED_APPS`` in your ``settings.py``, +ensuring also that ``django_x509`` has been removed: -Install (and add to the requirement of your project) `openwisp-utils -`_:: +.. code-block:: python + + INSTALLED_APPS = [ + # ... other apps ... + # 'django_x509' <-- comment out or delete this line + 'myx509' + ] - pip install openwisp-utils +For more information about how to work with django projects and django apps, +please refer to the `django documentation `_. -2. Add ``EXTENDED_APPS`` +2. Install ``django-x509`` & ``openwisp-utils`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install (and add to the requirement of your project):: + + pip install django-x509 openwisp-utils + +3. Add ``EXTENDED_APPS`` ~~~~~~~~~~~~~~~~~~~~~~~~ Add the following to your ``settings.py``: .. code-block:: python - EXTENDED_APPS = ('django_x509',) + EXTENDED_APPS = ['django_x509'] -3. Add ``openwisp_utils.staticfiles.DependencyFinder`` +4. Add ``openwisp_utils.staticfiles.DependencyFinder`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add ``openwisp_utils.staticfiles.DependencyFinder`` to @@ -379,7 +407,7 @@ Add ``openwisp_utils.staticfiles.DependencyFinder`` to 'openwisp_utils.staticfiles.DependencyFinder', ] -4. Add ``openwisp_utils.loaders.DependencyLoader`` +5. Add ``openwisp_utils.loaders.DependencyLoader`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``settings.py``: @@ -405,130 +433,184 @@ Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``setti } ] -5. Add swapper configurations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +6. Inherit the AppConfig class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add the following to your ``settings.py``: +Please refer to the following files in the sample app of the test project: -.. code-block:: python +- `sample_x509/__init__.py `_. +- `sample_x509/apps.py `_. - # Setting models for swapper module - DJANGO_X509_CA_MODEL = '.Ca' - DJANGO_X509_CERT_MODEL = '.Cert' +You have to replicate and adapt that code in your project. -Substitute ```` with your actual django app name -(also known as ``app_label``). +For more information regarding the concept of ``AppConfig`` please refer to +the `"Applications" section in the django documentation `_. -Extending models -~~~~~~~~~~~~~~~~ +7. Create your custom models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This example provides an example of how to extend the base models of -*django-x509* by adding a relation to another django model named `Organization`. +Here we provide an example of how to extend the base models of +*django-x509*. We added a simple "details" field to the +models for demostration of modification: .. code-block:: python - # models.py of your app from django.db import models from django_x509.base.models import AbstractCa, AbstractCert - # the model ``organizations.Organization`` is omitted for brevity - # if you are curious to see a real implementation, check out django-organizations - class OrganizationMixin(models.Model): - organization = models.ForeignKey('organizations.Organization') + class DetailsModel(models.Model): + details = models.CharField(max_length=64, blank=True, null=True) class Meta: abstract = True - class Ca(OrganizationMixin, AbstractCa): + class Ca(DetailsModel, AbstractCa): + """ + Concrete Ca model + """ class Meta(AbstractCa.Meta): abstract = False - def clean(self): - # your own validation logic here... - pass - - class Cert(OrganizationMixin, AbstractCert): - ca = models.ForeignKey(Ca) - + class Cert(DetailsModel, AbstractCert): + """ + Concrete Cert model + """ class Meta(AbstractCert.Meta): abstract = False - def clean(self): - # your own validation logic here... - pass +You can add fields in a similar way in your ``models.py`` file. + +**Note**: for doubts regarding how to use, extend or develop models please refer to +the `"Models" section in the django documentation `_. -Extending admin -~~~~~~~~~~~~~~~ +8. Add swapper configurations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Following the previous `Organization` example, you can avoid duplicating the admin -code by importing the base admin classes and registering your models with. +Once you have created the models, add the following to your ``settings.py``: .. code-block:: python - # admin.py of your app - from django.contrib import admin - from django_x509.base.admin import CaAdmin as BaseCaAdmin - from django_x509.base.admin import CertAdmin as BaseCertAdmin - from .models import Ca, Cert + # Setting models for swapper module + DJANGO_X509_CA_MODEL = 'myx509.Ca' + DJANGO_X509_CERT_MODEL = 'myx509.Cert' - class CaAdmin(BaseCaAdmin): - # extend/modify the default behaviour here - pass - class CertAdmin(BaseCertAdmin): - # extend/modify the default behaviour here - pass +Substitute ``myx509`` with the name you chose in step 1. - admin.site.register(Ca, CaAdmin) - admin.site.register(Cert, CertAdmin) +9. Create database migrations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Extending AppConfig -~~~~~~~~~~~~~~~~~~~ +Create and apply database migrations:: -Extending django-x509 app config is simple, you can -use the following: + ./manage.py makemigrations + ./manage.py migrate -.. code-block:: python +For more information, refer to the +`"Migrations" section in the django documentation `_. - from django_x509.apps import DjangoX509Config +10. Create the admin +~~~~~~~~~~~~~~~~~~~~ - class SampleX509Config(DjangoX509Config): - name = 'MY_AWESOME_APP' - verbose_name = 'MY_AWESOME_APP' +Refer to the `admin.py file of the sample app `_. +To introduce changes to the admin, you can do it in two main ways which are described below. -Extending tests -~~~~~~~~~~~~~~~ +**Note**: for more information regarding how the django admin works, or how it can be customized, +please refer to `"The django admin site" section in the django documentation `_. -If you want to extend the testcases from django-x509: +1. Monkey patching +################## + +If the changes you need to add are relatively small, you can resort to monkey patching. + +For example: .. code-block:: python - import os - from django.test import TestCase + from django_x509.admin import CaAdmin, CertAdmin + + # CaAdmin.list_display.insert(1, 'my_custom_field') <-- your custom change example + # CertAdmin.list_display.insert(1, 'my_custom_field') <-- your custom change example + +2. Inheriting admin classes +########################### + +If you need to introduce significant changes and/or you don't want to resort to +monkey patching, you can proceed as follows: + +.. code-block:: python + + from django.contrib import admin from swapper import load_model - from django_x509.tests.base.test_admin import AbstractModelAdminTests - from django_x509.tests.base.test_ca import AbstractTestCa - from django_x509.tests.base.test_cert import AbstractTestCert - from .admin import CaAdmin, CertAdmin + + from django_x509.base.admin import AbstractCaAdmin, AbstractCertAdmin Ca = load_model('django_x509', 'Ca') Cert = load_model('django_x509', 'Cert') - class ModelAdminTests(AbstractModelAdminTests, TestCase): - app_name = 'YOUR_AWESOME_APP_NAME' - ca_model = Ca - cert_model = Cert + class CertAdmin(AbstractCertAdmin): + # add your changes here + + class CaAdmin(AbstractCaAdmin): + # add your changes here + + admin.site.register(Ca, CaAdmin) + admin.site.register(Cert, CertAdmin) + +11. Create root URL configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Please refer to the `urls.py `_ +file in the test project. + +For more information about URL configuration in django, please refer to the +`"URL dispatcher" section in the django documentation `_. + +12. Import the automated tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When developing a custom application based on this module, it's a good +idea to import and run the base tests too, so that you can be sure the changes +you're introducing are not breaking some of the existing features of *django-x509*. + +In case you need to add breaking changes, you can overwrite the tests defined +in the base classes to test your own behavior. + +.. code-block:: python + + from django.test import TestCase + from django_x509.tests.base import TestX509Mixin + from django_x509.tests.test_admin import ModelAdminTests as BaseModelAdminTests + from django_x509.tests.test_ca import TestCa as BaseTestCa + from django_x509.tests.test_cert import TestCert as BaseTestCert + from .admin import CaAdmin, CertAdmin + + class ModelAdminTests(BaseModelAdminTests): + app_label = 'myx509' ca_admin = CaAdmin cert_admin = CertAdmin - class TestCert(AbstractTestCert, TestCase): - ca_model = Ca - cert_model = Cert + class TestCert(BaseTestCert): + pass + + class TestCa(BaseTestCa): + pass + + del BaseModelAdminTests + del BaseTestCa + del BaseTestCert + +Substitute ``myx509`` with the name you chose in step 1. + +Now, you can then run tests with:: + + # the --parallel flag is optional + ./manage.py test --parallel myx509 + +Substitute ``myx509`` with the name you chose in step 1. - class TestCa(AbstractTestCa, TestCase): - ca_model = Ca - cert_model = Cert +For more information about automated tests in django, please refer to +`"Testing in Django" `_. Contributing ------------ diff --git a/django_x509/admin.py b/django_x509/admin.py index ebc6e62..e3d507b 100644 --- a/django_x509/admin.py +++ b/django_x509/admin.py @@ -1,3 +1,10 @@ +""" +base/admin.py and admin.py are not merged +to keep backward compatibility; otherwise, +there is no reason for the existence of the +base/admin.py file. +""" + from django.contrib import admin from swapper import load_model @@ -15,5 +22,13 @@ class CaAdmin(AbstractCaAdmin): pass -admin.site.register(Ca, CaAdmin) -admin.site.register(Cert, CertAdmin) +# Check if model is registered, if not, register it +# on admin. The check is required before registration +# because this file is imported in tests/test_admin.py +# which used in extended apps as well. The absense of +# the check will cause `AlreadyRegistered` Error in +# extended app during testing. +if not admin.site.is_registered(Ca): + admin.site.register(Ca, CaAdmin) +if not admin.site.is_registered(Cert): + admin.site.register(Cert, CertAdmin) diff --git a/django_x509/tests/base/__init__.py b/django_x509/tests/base/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/django_x509/tests/base/base.py b/django_x509/tests/base/base.py deleted file mode 100644 index 9aaa0a4..0000000 --- a/django_x509/tests/base/base.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.contrib.messages.storage.fallback import FallbackStorage -from django.http import HttpRequest - - -class MessagingRequest(HttpRequest): - session = 'session' - - def __init__(self): - super().__init__() - self._messages = FallbackStorage(self) - - def get_messages(self): - return getattr(self._messages, '_queued_messages') - - def get_message_strings(self): - return [str(m) for m in self.get_messages()] - - -class TestX509Mixin(object): - def _create_ca(self, **kwargs): - options = dict(name='Test CA', - key_length='2048', - digest='sha256', - country_code='IT', - state='RM', - city='Rome', - organization_name='OpenWISP', - email='test@test.com', - common_name='openwisp.org', - extensions=[]) - options.update(kwargs) - ca = self.ca_model(**options) - ca.full_clean() - ca.save() - return ca - - def _create_cert(self, **kwargs): - options = dict(name='TestCert', - ca=None, - key_length='2048', - digest='sha256', - country_code='IT', - state='RM', - city='Rome', - organization_name='Test', - email='test@test.com', - common_name='openwisp.org', - extensions=[]) - options.update(kwargs) - # auto create CA if not supplied - if not options.get('ca'): - options['ca'] = self._create_ca() - cert = self.cert_model(**options) - cert.full_clean() - cert.save() - return cert diff --git a/django_x509/tests/base/test_admin.py b/django_x509/tests/test_admin.py similarity index 84% rename from django_x509/tests/base/test_admin.py rename to django_x509/tests/test_admin.py index b90d84b..6ff6455 100644 --- a/django_x509/tests/base/test_admin.py +++ b/django_x509/tests/test_admin.py @@ -1,8 +1,14 @@ from copy import deepcopy from django.contrib.admin.sites import AdminSite +from django.test import TestCase +from swapper import load_model -from .base import MessagingRequest +from ..admin import CaAdmin, CertAdmin +from .utils import MessagingRequest + +Ca = load_model('django_x509', 'Ca') +Cert = load_model('django_x509', 'Cert') class MockSuperUser: @@ -87,24 +93,27 @@ def has_perm(self, perm): 'modified'] -class AbstractModelAdminTests(object): +class ModelAdminTests(TestCase): + app_label = 'django_x509' + ca_admin = CaAdmin + cert_admin = CertAdmin def setUp(self): - self.ca = self.ca_model.objects.create() - self.cert = self.cert_model.objects.create(ca_id=self.ca.pk) + self.ca = Ca.objects.create() + self.cert = Cert.objects.create(ca_id=self.ca.pk) self.cert.ca = self.ca self.site = AdminSite() def test_modeladmin_str_ca(self): - ma = self.ca_admin(self.ca_model, self.site) - self.assertEqual(str(ma), f'{self.app_name}.CaAdmin') + ma = self.ca_admin(Ca, self.site) + self.assertEqual(str(ma), f'{self.app_label}.CaAdmin') def test_modeladmin_str_certr(self): - ma = self.cert_admin(self.cert_model, self.site) - self.assertEqual(str(ma), f'{self.app_name}.CertAdmin') + ma = self.cert_admin(Cert, self.site) + self.assertEqual(str(ma), f'{self.app_label}.CertAdmin') def test_default_fields_ca(self): - ma = self.ca_admin(self.ca_model, self.site) + ma = self.ca_admin(Ca, self.site) self.assertEqual(list(ma.get_form(request).base_fields), ca_fields) ca_fields.insert(len(ca_fields), 'created') ca_fields.insert(len(ca_fields), 'modified') @@ -118,7 +127,7 @@ def test_default_fields_ca(self): ca_fields.insert(pass_index, 'passphrase') def test_default_fields_cert(self): - ma = self.cert_admin(self.cert_model, self.site) + ma = self.cert_admin(Cert, self.site) self.assertEqual(list(ma.get_form(request).base_fields), cert_fields) cert_fields.insert(4, 'revoked') cert_fields.insert(5, 'revoked_at') @@ -134,32 +143,32 @@ def test_default_fields_cert(self): cert_fields.insert(pass_index, 'passphrase') def test_default_fieldsets_ca(self): - ma = self.ca_admin(self.ca_model, self.site) + ma = self.ca_admin(Ca, self.site) self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': ca_fields})]) def test_default_fieldsets_cert(self): - ma = self.cert_admin(self.cert_model, self.site) + ma = self.cert_admin(Cert, self.site) self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': cert_fields})]) def test_readonly_fields_Ca(self): - ma = self.ca_admin(self.ca_model, self.site) + ma = self.ca_admin(Ca, self.site) self.assertEqual(ma.get_readonly_fields(request), ('created', 'modified')) self.assertEqual(ma.get_readonly_fields(request, self.ca), tuple(ca_readonly)) ca_readonly.remove('created') ca_readonly.remove('modified') def test_readonly_fields_Cert(self): - ma = self.cert_admin(self.cert_model, self.site) + ma = self.cert_admin(Cert, self.site) self.assertEqual(ma.get_readonly_fields(request), cert_readonly) ca_readonly.append('ca') self.assertEqual(ma.get_readonly_fields(request, self.cert), tuple(ca_readonly + cert_readonly)) def test_ca_url(self): - ma = self.cert_admin(self.cert_model, self.site) - self.assertEqual(ma.ca_url(self.cert), f'') + ma = self.cert_admin(Cert, self.site) + self.assertEqual(ma.ca_url(self.cert), f'') def test_revoke_action(self): - ma = self.cert_admin(self.cert_model, self.site) + ma = self.cert_admin(Cert, self.site) ma.revoke_action(request, [self.cert]) m = list(request.get_messages()) self.assertEqual(len(m), 1) @@ -167,8 +176,8 @@ def test_revoke_action(self): def test_renew_ca_action(self): req = deepcopy(request) - ca = self.ca_model.objects.create(name="test_ca") - cert = self.cert_model.objects.create(name="test_cert", ca=ca) + ca = Ca.objects.create(name="test_ca") + cert = Cert.objects.create(name="test_cert", ca=ca) old_ca_cert = ca.certificate old_ca_key = ca.private_key old_ca_end = ca.validity_end @@ -177,7 +186,7 @@ def test_renew_ca_action(self): old_cert_key = cert.private_key old_cert_end = cert.validity_end old_cert_serial_number = cert.serial_number - ma = self.ca_admin(self.ca_model, self.site) + ma = self.ca_admin(Ca, self.site) req.POST.update({ 'post': None }) @@ -203,8 +212,8 @@ def test_renew_ca_action(self): def test_renew_cert_action(self): req = deepcopy(request) - ca = self.ca_model.objects.create(name="test_ca") - cert = self.cert_model.objects.create(name="test_cert", ca=ca) + ca = Ca.objects.create(name="test_ca") + cert = Cert.objects.create(name="test_cert", ca=ca) old_ca_cert = ca.certificate old_ca_key = ca.private_key old_ca_end = ca.validity_end @@ -213,7 +222,7 @@ def test_renew_cert_action(self): old_cert_key = cert.private_key old_cert_end = cert.validity_end old_cert_serial_number = cert.serial_number - ma = self.cert_admin(self.cert_model, self.site) + ma = self.cert_admin(Cert, self.site) req.POST.update({ 'post': None }) diff --git a/django_x509/tests/base/test_ca.py b/django_x509/tests/test_ca.py similarity index 96% rename from django_x509/tests/base/test_ca.py rename to django_x509/tests/test_ca.py index 4257c8a..6803604 100644 --- a/django_x509/tests/base/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -1,16 +1,21 @@ from datetime import datetime, timedelta from django.core.exceptions import ValidationError +from django.test import TestCase from django.urls import reverse from django.utils import timezone from OpenSSL import crypto +from swapper import load_model -from ... import settings as app_settings -from ...base.models import datetime_to_string, generalized_time, utc_time -from .base import TestX509Mixin +from .. import settings as app_settings +from ..base.models import datetime_to_string, generalized_time, utc_time +from .utils import TestX509Mixin +Ca = load_model('django_x509', 'Ca') +Cert = load_model('django_x509', 'Cert') -class AbstractTestCa(TestX509Mixin): + +class TestCa(TestX509Mixin, TestCase): """ tests for Ca model """ @@ -86,21 +91,21 @@ def test_x509_property(self): self.assertEqual(ca.x509.get_issuer(), cert.get_issuer()) def test_x509_property_none(self): - self.assertIsNone(self.ca_model().x509) + self.assertIsNone(Ca().x509) def test_pkey_property(self): ca = self._create_ca() self.assertIsInstance(ca.pkey, crypto.PKey) def test_pkey_property_none(self): - self.assertIsNone(self.ca_model().pkey) + self.assertIsNone(Ca().pkey) def test_default_validity_end(self): - ca = self.ca_model() + ca = Ca() self.assertEqual(ca.validity_end.year, datetime.now().year + 10) def test_default_validity_start(self): - ca = self.ca_model() + ca = Ca() expected = datetime.now() - timedelta(days=1) self.assertEqual(ca.validity_start.year, expected.year) self.assertEqual(ca.validity_start.month, expected.month) @@ -110,7 +115,7 @@ def test_default_validity_start(self): self.assertEqual(ca.validity_start.second, 0) def test_import_ca(self): - ca = self.ca_model(name='ImportTest') + ca = Ca(name='ImportTest') ca.certificate = self.import_certificate ca.private_key = self.import_private_key ca.full_clean() @@ -151,14 +156,14 @@ def test_import_ca(self): self.assertEqual(cert.get_version(), 3) ca.delete() # test auto name - ca = self.ca_model(certificate=self.import_certificate, - private_key=self.import_private_key) + ca = Ca(certificate=self.import_certificate, + private_key=self.import_private_key) ca.full_clean() ca.save() self.assertEqual(ca.name, 'importtest') def test_import_private_key_empty(self): - ca = self.ca_model(name='ImportTest') + ca = Ca(name='ImportTest') ca.certificate = self.import_certificate try: ca.full_clean() @@ -272,7 +277,7 @@ def test_get_revoked_certs(self): ca = self._create_ca() c1 = self._create_cert(ca=ca) c2 = self._create_cert(ca=ca) - c3 = self._create_cert(ca=ca) # noqa + self._create_cert(ca=ca) self.assertEqual(ca.get_revoked_certs().count(), 0) c1.revoke() self.assertEqual(ca.get_revoked_certs().count(), 1) @@ -316,13 +321,13 @@ def test_crl_view(self): def test_crl_view_403(self): setattr(app_settings, 'CRL_PROTECTED', True) - ca, cert = self._prepare_revoked() + ca, _ = self._prepare_revoked() response = self.client.get(reverse('admin:crl', args=[ca.pk])) self.assertEqual(response.status_code, 403) setattr(app_settings, 'CRL_PROTECTED', False) def test_crl_view_404(self): - ca, cert = self._prepare_revoked() + self._prepare_revoked() response = self.client.get(reverse('admin:crl', args=[10])) self.assertEqual(response.status_code, 404) @@ -383,7 +388,7 @@ def test_x509_import_exception_fixed(self): 2yrYrwM4KOr7LrKtvz703ApicJf+oRO+vW27+N5t0pyLCjsYJyL55RpM0KWJhKhT KQV8C/ciDV+lIw2yBmlCNvUmy7GAsHSZM+C8y29+GFR7an6WV+xa -----END RSA PRIVATE KEY-----""" - ca = self.ca_model(name='ImportTest error') + ca = Ca(name='ImportTest error') ca.certificate = certificate ca.private_key = private_key ca.full_clean() @@ -392,7 +397,7 @@ def test_x509_import_exception_fixed(self): def test_fill_subject_non_strings(self): ca1 = self._create_ca() - ca2 = self.ca_model(name='ca', organization_name=ca1) + ca2 = Ca(name='ca', organization_name=ca1) x509 = crypto.X509() subject = ca2._fill_subject(x509.get_subject()) self.assertEqual(subject.organizationName, 'Test CA') @@ -462,7 +467,7 @@ def test_ca_invalid_country(self): def test_import_ca_cert_validation_error(self): certificate = self.import_certificate[20:] private_key = self.import_private_key - ca = self.ca_model(name="TestCaCertValidation") + ca = Ca(name="TestCaCertValidation") try: ca.certificate = certificate ca.private_key = private_key @@ -481,7 +486,7 @@ def test_import_ca_cert_validation_error(self): def test_import_ca_key_validation_error(self): certificate = self.import_certificate private_key = self.import_private_key[20:] - ca = self.ca_model(name="TestCaKeyValidation") + ca = Ca(name="TestCaKeyValidation") try: ca.certificate = certificate ca.private_key = private_key @@ -511,7 +516,7 @@ def test_bad_serial_number_ca(self): self.assertEqual("Serial number must be an integer", str(e.message_dict['serial_number'][0])) def test_import_ca_key_with_passphrase(self): - ca = self.ca_model(name='ImportTest') + ca = Ca(name='ImportTest') ca.certificate = """-----BEGIN CERTIFICATE----- MIICrzCCAhigAwIBAgIJANCybYj5LwUWMA0GCSqGSIb3DQEBCwUAMG8xCzAJBgNV BAYTAklOMQwwCgYDVQQIDANhc2QxDDAKBgNVBAcMA2FzZDEMMAoGA1UECgwDYXNk @@ -556,7 +561,7 @@ def test_import_ca_key_with_passphrase(self): self.assertIsInstance(ca.pkey, crypto.PKey) def test_import_ca_key_with_incorrect_passphrase(self): - ca = self.ca_model(name='ImportTest') + ca = Ca(name='ImportTest') ca.certificate = """-----BEGIN CERTIFICATE----- MIICrzCCAhigAwIBAgIJANCybYj5LwUWMA0GCSqGSIb3DQEBCwUAMG8xCzAJBgNV BAYTAklOMQwwCgYDVQQIDANhc2QxDDAKBgNVBAcMA2FzZDEMMAoGA1UECgwDYXNk diff --git a/django_x509/tests/base/test_cert.py b/django_x509/tests/test_cert.py similarity index 90% rename from django_x509/tests/base/test_cert.py rename to django_x509/tests/test_cert.py index 48b28ef..184bbe8 100644 --- a/django_x509/tests/base/test_cert.py +++ b/django_x509/tests/test_cert.py @@ -1,15 +1,20 @@ from datetime import datetime, timedelta from django.core.exceptions import ValidationError +from django.test import TestCase from django.utils import timezone from OpenSSL import crypto +from swapper import load_model -from ... import settings as app_settings -from ...base.models import generalized_time -from .base import TestX509Mixin +from .. import settings as app_settings +from ..base.models import generalized_time +from .utils import TestX509Mixin +Ca = load_model('django_x509', 'Ca') +Cert = load_model('django_x509', 'Cert') -class AbstractTestCert(TestX509Mixin): + +class TestCert(TestX509Mixin, TestCase): """ tests for Cert model """ @@ -118,21 +123,21 @@ def test_x509_property(self): self.assertEqual(cert.x509.get_issuer(), x509.get_issuer()) def test_x509_property_none(self): - self.assertIsNone(self.cert_model().x509) + self.assertIsNone(Cert().x509) def test_pkey_property(self): cert = self._create_cert() self.assertIsInstance(cert.pkey, crypto.PKey) def test_pkey_property_none(self): - self.assertIsNone(self.cert_model().pkey) + self.assertIsNone(Cert().pkey) def test_default_validity_end(self): - cert = self.cert_model() + cert = Cert() self.assertEqual(cert.validity_end.year, datetime.now().year + 1) def test_default_validity_start(self): - cert = self.cert_model() + cert = Cert() expected = datetime.now() - timedelta(days=1) self.assertEqual(cert.validity_start.year, expected.year) self.assertEqual(cert.validity_start.month, expected.month) @@ -142,15 +147,15 @@ def test_default_validity_start(self): self.assertEqual(cert.validity_start.second, 0) def test_import_cert(self): - ca = self.ca_model(name='ImportTest') + ca = Ca(name='ImportTest') ca.certificate = self.import_ca_certificate ca.private_key = self.import_ca_private_key ca.full_clean() ca.save() - cert = self.cert_model(name='ImportCertTest', - ca=ca, - certificate=self.import_certificate, - private_key=self.import_private_key) + cert = Cert(name='ImportCertTest', + ca=ca, + certificate=self.import_certificate, + private_key=self.import_private_key) cert.full_clean() cert.save() x509 = cert.x509 @@ -188,21 +193,21 @@ def test_import_cert(self): self.assertEqual(x509.get_version(), 2) cert.delete() # test auto name - cert = self.cert_model(certificate=self.import_certificate, - private_key=self.import_private_key, - ca=ca) + cert = Cert(certificate=self.import_certificate, + private_key=self.import_private_key, + ca=ca) cert.full_clean() cert.save() self.assertEqual(cert.name, '123456') def test_import_private_key_empty(self): - ca = self.ca_model(name='ImportTest') + ca = Ca(name='ImportTest') ca.certificate = self.import_ca_certificate ca.private_key = self.import_ca_private_key ca.full_clean() ca.save() - cert = self.cert_model(name='ImportTest', - ca=ca) + cert = Cert(name='ImportTest', + ca=ca) cert.certificate = self.import_certificate try: cert.full_clean() @@ -214,9 +219,9 @@ def test_import_private_key_empty(self): def test_import_wrong_ca(self): # test auto name - cert = self.cert_model(certificate=self.import_certificate, - private_key=self.import_private_key, - ca=self._create_ca()) + cert = Cert(certificate=self.import_certificate, + private_key=self.import_private_key, + ca=self._create_ca()) try: cert.full_clean() except ValidationError as e: @@ -331,7 +336,7 @@ def test_x509_text(self): def test_fill_subject_None_attrs(self): # ensure no exception raised if model attrs are set to None x509 = crypto.X509() - cert = self.cert_model(name='test', ca=self._create_ca()) + cert = Cert(name='test', ca=self._create_ca()) cert._fill_subject(x509.get_subject()) self.country_code = 'IT' cert._fill_subject(x509.get_subject()) @@ -345,11 +350,11 @@ def test_fill_subject_None_attrs(self): cert._fill_subject(x509.get_subject()) def test_cert_create(self): - ca = self.ca_model(name='Test CA') + ca = Ca(name='Test CA') ca.full_clean() ca.save() - self.cert_model.objects.create( + Cert.objects.create( ca=ca, common_name='TestCert1', name='TestCert1', @@ -358,16 +363,16 @@ def test_cert_create(self): def test_import_cert_validation_error(self): certificate = self.import_certificate[20:] private_key = self.import_private_key - ca = self.ca_model(name='TestImportCertValidation') + ca = Ca(name='TestImportCertValidation') ca.certificate = self.import_ca_certificate ca.private_key = self.import_ca_private_key ca.full_clean() ca.save() try: - cert = self.cert_model(name='TestCertValidation', - ca=ca, - certificate=certificate, - private_key=private_key) + cert = Cert(name='TestCertValidation', + ca=ca, + certificate=certificate, + private_key=private_key) cert.full_clean() except ValidationError as e: # cryptography 2.4 and 2.6 have different error message formats @@ -383,16 +388,16 @@ def test_import_cert_validation_error(self): def test_import_key_validation_error(self): certificate = self.import_certificate private_key = self.import_private_key[20:] - ca = self.ca_model(name='TestImportKeyValidation') + ca = Ca(name='TestImportKeyValidation') ca.certificate = self.import_certificate ca.private_key = self.import_private_key ca.full_clean() ca.save() try: - cert = self.cert_model(name='TestKeyValidation', - ca=ca, - certificate=certificate, - private_key=private_key) + cert = Cert(name='TestKeyValidation', + ca=ca, + certificate=certificate, + private_key=private_key) cert.full_clean() except ValidationError as e: # cryptography 2.4 and 2.6 have different error message formats @@ -418,17 +423,17 @@ def test_bad_serial_number_cert(self): self.assertEqual("Serial number must be an integer", str(e.message_dict['serial_number'][0])) def test_serial_number_clash(self): - ca = self.ca_model(name='TestSerialClash') + ca = Ca(name='TestSerialClash') ca.certificate = self.import_ca_certificate ca.private_key = self.import_ca_private_key ca.save() cert = self._create_cert(serial_number=123456, ca=ca) cert.full_clean() cert.save() - _cert = self.cert_model(name='TestClash', - ca=ca, - certificate=self.import_certificate, - private_key=self.import_private_key) + _cert = Cert(name='TestClash', + ca=ca, + certificate=self.import_certificate, + private_key=self.import_private_key) try: _cert.full_clean() except ValidationError as e: @@ -436,7 +441,7 @@ def test_serial_number_clash(self): str(e.message_dict['__all__'][0])) def test_import_cert_with_passphrase(self): - ca = self.ca_model(name='ImportTest') + ca = Ca(name='ImportTest') ca.certificate = """-----BEGIN CERTIFICATE----- MIICrzCCAhigAwIBAgIJANCybYj5LwUWMA0GCSqGSIb3DQEBCwUAMG8xCzAJBgNV BAYTAklOMQwwCgYDVQQIDANhc2QxDDAKBgNVBAcMA2FzZDEMMAoGA1UECgwDYXNk diff --git a/django_x509/tests/tests.py b/django_x509/tests/tests.py deleted file mode 100644 index d089ef2..0000000 --- a/django_x509/tests/tests.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -from unittest import skipIf - -from django.test import TestCase -from swapper import load_model - -from ..admin import CaAdmin, CertAdmin -from .base.test_admin import AbstractModelAdminTests -from .base.test_ca import AbstractTestCa -from .base.test_cert import AbstractTestCert - -Ca = load_model('django_x509', 'Ca') -Cert = load_model('django_x509', 'Cert') - - -@skipIf(os.environ.get('SAMPLE_APP', False), 'Running tests on SAMPLE_APP') -class ModelAdminTests(AbstractModelAdminTests, TestCase): - app_name = 'django_x509' - ca_model = Ca - cert_model = Cert - ca_admin = CaAdmin - cert_admin = CertAdmin - - -@skipIf(os.environ.get('SAMPLE_APP', False), 'Running tests on SAMPLE_APP') -class TestCert(AbstractTestCert, TestCase): - ca_model = Ca - cert_model = Cert - - -@skipIf(os.environ.get('SAMPLE_APP', False), 'Running tests on SAMPLE_APP') -class TestCa(AbstractTestCa, TestCase): - ca_model = Ca - cert_model = Cert diff --git a/django_x509/tests/utils.py b/django_x509/tests/utils.py new file mode 100644 index 0000000..e19ddbc --- /dev/null +++ b/django_x509/tests/utils.py @@ -0,0 +1,66 @@ +from django.contrib.messages.storage.fallback import FallbackStorage +from django.http import HttpRequest +from swapper import load_model + +Ca = load_model('django_x509', 'Ca') +Cert = load_model('django_x509', 'Cert') + + +class MessagingRequest(HttpRequest): + session = 'session' + + def __init__(self): + super().__init__() + self._messages = FallbackStorage(self) + + def get_messages(self): + return getattr(self._messages, '_queued_messages') + + def get_message_strings(self): + return [str(m) for m in self.get_messages()] + + +class TestX509Mixin(object): + def _create_ca(self, **kwargs): + options = dict( + name='Test CA', + key_length='2048', + digest='sha256', + country_code='IT', + state='RM', + city='Rome', + organization_name='OpenWISP', + email='test@test.com', + common_name='openwisp.org', + extensions=[], + ) + options.update(kwargs) + ca = Ca(**options) + ca.full_clean() + ca.save() + return ca + + def _create_cert(self, cert_model=None, **kwargs): + if not cert_model: + cert_model = Cert + options = dict( + name='TestCert', + ca=None, + key_length='2048', + digest='sha256', + country_code='IT', + state='RM', + city='Rome', + organization_name='Test', + email='test@test.com', + common_name='openwisp.org', + extensions=[], + ) + options.update(kwargs) + # auto create CA if not supplied + if not options.get('ca'): + options['ca'] = self._create_ca() + cert = cert_model(**options) + cert.full_clean() + cert.save() + return cert diff --git a/tests/openwisp2/sample_x509/admin.py b/tests/openwisp2/sample_x509/admin.py index c96fc24..1327fb4 100644 --- a/tests/openwisp2/sample_x509/admin.py +++ b/tests/openwisp2/sample_x509/admin.py @@ -1,19 +1 @@ -from django.contrib import admin -from swapper import load_model - -from django_x509.base.admin import AbstractCaAdmin, AbstractCertAdmin - -Ca = load_model('django_x509', 'Ca') -Cert = load_model('django_x509', 'Cert') - - -class CertAdmin(AbstractCertAdmin): - pass - - -class CaAdmin(AbstractCaAdmin): - pass - - -admin.site.register(Ca, CaAdmin) -admin.site.register(Cert, CertAdmin) +from django_x509.admin import CaAdmin, CertAdmin # noqa diff --git a/tests/openwisp2/sample_x509/tests.py b/tests/openwisp2/sample_x509/tests.py index 8af2899..bb72ccc 100644 --- a/tests/openwisp2/sample_x509/tests.py +++ b/tests/openwisp2/sample_x509/tests.py @@ -1,48 +1,35 @@ -import os -from unittest import skipUnless - from django.test import TestCase -from swapper import load_model -from django_x509.tests.base.base import TestX509Mixin -from django_x509.tests.base.test_admin import AbstractModelAdminTests -from django_x509.tests.base.test_ca import AbstractTestCa -from django_x509.tests.base.test_cert import AbstractTestCert +from django_x509.tests.test_admin import ModelAdminTests as BaseModelAdminTests +from django_x509.tests.test_ca import TestCa as BaseTestCa +from django_x509.tests.test_cert import TestCert as BaseTestCert +from django_x509.tests.utils import TestX509Mixin from .admin import CaAdmin, CertAdmin from .models import CustomCert -Ca = load_model('django_x509', 'Ca') -Cert = load_model('django_x509', 'Cert') - -@skipUnless(os.environ.get('SAMPLE_APP', False), 'Running tests on django-x509') class TestCustomCert(TestX509Mixin, TestCase): - ca_model = Ca - cert_model = CustomCert - def test_pk_field(self): """Test that a cert can be created without an AttributeError.""" - cert = self._create_cert(fingerprint='123') + cert = self._create_cert(cert_model=CustomCert, fingerprint='123') self.assertEqual(cert.pk, cert.fingerprint) -@skipUnless(os.environ.get('SAMPLE_APP', False), 'Running tests on django-x509') -class ModelAdminTests(AbstractModelAdminTests, TestCase): - app_name = 'sample_x509' - ca_model = Ca - cert_model = Cert +class ModelAdminTests(BaseModelAdminTests): + app_label = 'sample_x509' ca_admin = CaAdmin cert_admin = CertAdmin -@skipUnless(os.environ.get('SAMPLE_APP', False), 'Running tests on django-x509') -class TestCert(AbstractTestCert, TestCase): - ca_model = Ca - cert_model = Cert +class TestCert(BaseTestCert): + pass + + +class TestCa(BaseTestCa): + pass -@skipUnless(os.environ.get('SAMPLE_APP', False), 'Running tests on django-x509') -class TestCa(AbstractTestCa, TestCase): - ca_model = Ca - cert_model = Cert +del BaseModelAdminTests +del BaseTestCa +del BaseTestCert diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index c82f6ed..02ca9a1 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -2,8 +2,6 @@ from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns -admin.autodiscover() - urlpatterns = [ url(r'^admin/', admin.site.urls), ]