Browse Source

Creating unit tests (#179)

Initial implementation of #2 unit tests
Cory Sutyak 5 years ago
parent
commit
e7cde12ce2

+ 1 - 2
.gitignore

@@ -12,5 +12,4 @@ __pycache__
 codered_cms.egg-info/
 coderedcms.egg-info/
 .vscode/
-testapp*/
-testproject*/
+testproject*/

+ 0 - 0
coderedcms/blocks/tests/__init__.py


+ 19 - 0
coderedcms/blocks/tests/test_blocks.py

@@ -0,0 +1,19 @@
+from coderedcms.blocks import base_blocks
+from django.test import SimpleTestCase, TestCase
+
+from wagtail.tests.utils import WagtailTestUtils
+
+class TestMultiSelectBlock(WagtailTestUtils, SimpleTestCase):
+    def test_render_single_choice(self):
+        block = base_blocks.MultiSelectBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee'), ('water', 'Water')])
+        html = block.render_form(['tea'])
+        self.assertInHTML('<option value="tea" selected>Tea</option>', html)
+        self.assertTrue(html.count('selected'), 1)
+
+    def test_render_multi_choice(self):
+        block = base_blocks.MultiSelectBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee'), ('water', 'Water')])
+        html = block.render_form(['coffee', 'tea'])
+        self.assertInHTML('<option value="tea" selected>Tea</option>', html)
+        self.assertInHTML('<option value="coffee" selected>Coffee</option>', html)
+        self.assertTrue(html.count('selected'), 2)
+

+ 0 - 0
coderedcms/models/tests/__init__.py


+ 167 - 0
coderedcms/models/tests/test_page_models.py

@@ -0,0 +1,167 @@
+from django.contrib.auth.models import AnonymousUser
+from wagtail.tests.utils import WagtailPageTests
+from django.test.client import RequestFactory
+from wagtail.core.models import Site
+
+from coderedcms.models.page_models import (
+    CoderedArticleIndexPage,
+    CoderedArticlePage,
+    CoderedEventIndexPage,
+    CoderedEventPage,
+    CoderedFormPage,
+    CoderedPage,
+    CoderedWebPage,
+    CoderedLocationIndexPage,
+    CoderedLocationPage,
+    get_page_models
+)
+from coderedcms.tests.testapp.models import (
+    ArticlePage,
+    ArticleIndexPage,
+    FormPage,
+    WebPage,
+    EventPage,
+    EventIndexPage,
+    LocationPage,
+    LocationIndexPage
+)
+
+class BasicPageTestCase():
+    """
+    This is a testing mixin used to run common tests for basic versions of page types.
+    """
+    class Meta:
+        abstract=True
+
+    def setUp(self):
+        self.request_factory = RequestFactory()
+        self.basic_page = self.model(
+            title=str(self.model._meta.verbose_name)
+        )
+        self.homepage = WebPage.objects.get(url_path='/home/')
+        self.homepage.add_child(instance=self.basic_page)
+
+    def test_get(self):
+        """
+        Tests to make sure a basic version of the page serves a 200 from a GET request.
+        """
+        request = self.request_factory.get(self.basic_page.url)
+        request.user = AnonymousUser()
+        request.site = Site.objects.all()[0]
+        response = self.basic_page.serve(request)
+        self.assertEqual(response.status_code, 200)
+
+class AbstractPageTestCase():
+    """
+    This is a testing mixin used to run common tests for abstract page types.
+    """
+    class Meta:
+        abstract=True
+
+    def test_not_available(self):
+        """
+        Tests to make sure the page is not creatable and not in CodeRed CMS's global list of page models.
+        """
+        self.assertFalse(self.model.is_creatable)
+        self.assertFalse(self.model in get_page_models())
+
+
+class ConcretePageTestCase():
+    """
+    This is a testing mixin used to run common tests for concrete page types.
+    """
+    class Meta:
+        abstract=True
+
+    def test_is_available(self):
+        """
+        Tests to make sure the page is creatable and in CodeRed CMS's global list of page models.
+        """
+        self.assertTrue(self.model.is_creatable)
+        self.assertTrue(self.model in get_page_models())
+
+
+class ConcreteBasicPageTestCase(ConcretePageTestCase, BasicPageTestCase):
+    class Meta:
+        abstract=True
+
+class CoderedArticleIndexPageTestCase(AbstractPageTestCase, WagtailPageTests):
+    model = CoderedArticleIndexPage
+
+
+class CoderedArticlePageTestCase(AbstractPageTestCase, WagtailPageTests):
+    model = CoderedArticlePage
+
+
+class CoderedFormPageTestCase(AbstractPageTestCase, WagtailPageTests):
+    model = CoderedFormPage
+
+
+class CoderedPageTestCase(WagtailPageTests):
+    model = CoderedPage
+
+    def test_not_available(self):
+        self.assertFalse(self.model.is_creatable)
+        self.assertTrue(self.model in get_page_models())
+
+
+class CoderedWebPageTestCase(AbstractPageTestCase, WagtailPageTests):
+    model = CoderedWebPage
+
+
+class CoderedLocationIndexPageTestCase(AbstractPageTestCase, WagtailPageTests):
+    model = CoderedLocationIndexPage
+
+
+class CoderedLocationPageTestCase(AbstractPageTestCase, WagtailPageTests):
+    model = CoderedLocationPage
+
+
+class CoderedEventIndexPageTestCase(AbstractPageTestCase, WagtailPageTests):
+    model = CoderedEventIndexPage
+
+
+class CoderedEventPageTestCase(AbstractPageTestCase, WagtailPageTests):
+    model = CoderedEventPage
+
+
+class ArticlePageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+    model = ArticlePage
+
+
+class ArticleIndexPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+    model = ArticleIndexPage
+
+
+class FormPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+    model = FormPage
+
+    def test_post(self):
+        """
+        Tests to make sure a basic version of the page serves a 200 from a POST request.
+        """
+        request = self.request_factory.post(self.basic_page.url)
+        request.user = AnonymousUser()
+        request.site = Site.objects.all()[0]
+        response = self.basic_page.serve(request)
+        self.assertEqual(response.status_code, 200)
+
+
+class WebPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+    model = WebPage
+
+
+class EventIndexPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+    model = EventIndexPage
+
+
+class EventPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+    model = EventPage
+
+
+class LocationIndexPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+    model = LocationIndexPage
+
+
+class LocationPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+    model = LocationPage

+ 208 - 0
coderedcms/project_template/project_name/settings/base.py

@@ -0,0 +1,208 @@
+"""
+Django settings for {{ project_name }} project.
+
+Generated by 'django-admin startproject' using Django {{ django_version }}.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
+"""
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+
+PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+BASE_DIR = os.path.dirname(PROJECT_DIR)
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    # Test
+    'coderedcms.tests.testapp',
+
+    # CodeRed CMS
+    'coderedcms',
+    'bootstrap4',
+    'modelcluster',
+    'taggit',
+    'wagtailfontawesome',
+    'wagtailcache',
+    'wagtailimportexport',
+
+    # Wagtail
+    'wagtail.contrib.forms',
+    'wagtail.contrib.redirects',
+    'wagtail.embeds',
+    'wagtail.sites',
+    'wagtail.users',
+    'wagtail.snippets',
+    'wagtail.documents',
+    'wagtail.images',
+    'wagtail.search',
+    'wagtail.core',
+    'wagtail.contrib.settings',
+    'wagtail.contrib.modeladmin',
+    'wagtail.contrib.table_block',
+    'wagtail.admin',
+
+    # Django
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    "django.contrib.sitemaps",
+]
+
+MIDDLEWARE = [
+    # Save pages to cache. Must be FIRST.
+    'wagtailcache.cache.UpdateCacheMiddleware',
+
+    # Common functionality
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.common.CommonMiddleware',
+
+    # Security
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'django.middleware.security.SecurityMiddleware',
+
+    # Error reporting. Uncomment this to recieve emails when a 404 is triggered.
+    #'django.middleware.common.BrokenLinkEmailsMiddleware',
+
+    # CMS functionality
+    'wagtail.core.middleware.SiteMiddleware',
+    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
+
+    # Fetch from cache. Must be LAST.
+    'wagtailcache.cache.FetchFromCacheMiddleware',
+]
+
+ROOT_URLCONF = 'coderedcms.tests.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+                'wagtail.contrib.settings.context_processors.settings',
+            ],
+        },
+    },
+]
+
+
+# Database
+# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+        'TEST': {
+            'NAME': os.path.join(BASE_DIR, 'test_db.sqlite3'),
+        }
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'America/New_York'
+
+USE_I18N = False
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
+
+STATICFILES_FINDERS = [
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+]
+
+STATIC_ROOT = os.path.join(BASE_DIR, 'static')
+STATIC_URL = '/static/'
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+MEDIA_URL = '/media/'
+
+
+# Login
+
+LOGIN_URL = 'wagtailadmin_login'
+LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+
+
+# Wagtail settings
+
+WAGTAIL_SITE_NAME = ""
+
+WAGTAIL_ENABLE_UPDATE_CHECK = False
+
+# Base URL to use when referring to full URLs within the Wagtail admin backend -
+# e.g. in notification emails. Don't include '/admin' or a trailing slash
+BASE_URL = ''
+
+
+# Bootstrap
+
+BOOTSTRAP4 = {
+    # set to blank since coderedcms already loads jquery and bootstrap
+    'jquery_url': '',
+    'base_url': '',
+    # remove green highlight on inputs
+    'success_css_class': ''
+}
+
+
+# Tags
+
+TAGGIT_CASE_INSENSITIVE = True
+
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+
+WAGTAIL_CACHE = False
+
+SECRET_KEY = 'not needed'

File diff suppressed because it is too large
+ 32 - 0
coderedcms/tests/testapp/migrations/0001_initial.py


+ 54 - 0
coderedcms/tests/testapp/migrations/0002_initial.py

@@ -0,0 +1,54 @@
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django import VERSION as DJANGO_VERSION
+from django.db import migrations
+
+
+def initial_data(apps, schema_editor):
+    ContentType = apps.get_model('contenttypes.ContentType')
+    Page = apps.get_model('wagtailcore.Page')
+    Site = apps.get_model('wagtailcore.Site')
+    WebPage = apps.get_model('testapp.WebPage')
+
+    # Create page content type
+    webpage_content_type, created = ContentType.objects.get_or_create(
+        model='webpage',
+        app_label='testapp',
+    )
+
+    # Delete the default home page generated by wagtail,
+    # and replace it with a more useful page type.
+    curr_homepage = Page.objects.filter(slug='home').delete()
+
+    homepage = WebPage.objects.create(
+        title = "Home",
+        slug='home',
+        custom_template='coderedcms/pages/home_page.html',
+        content_type=webpage_content_type,
+        path='00010001',
+        depth=2,
+        numchild=0,
+        url_path='/home/',
+    )
+
+    # Create a new default site
+    Site.objects.create(
+        hostname='',
+        site_name='Test Site',
+        root_page_id=homepage.id,
+        is_default_site=True
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('testapp', '0001_initial'),
+        ('wagtailcore', '0002_initial_data'),
+    ]
+
+    operations = [
+        migrations.RunPython(initial_data),
+    ]

File diff suppressed because it is too large
+ 31 - 0
coderedcms/tests/testapp/migrations/0003_eventindexpage_eventoccurrence_eventpage_locationindexpage_locationpage.py


+ 0 - 0
coderedcms/tests/testapp/migrations/__init__.py


+ 141 - 0
coderedcms/tests/testapp/models.py

@@ -0,0 +1,141 @@
+from modelcluster.fields import ParentalKey
+from coderedcms.forms import CoderedFormField
+from coderedcms.models import (
+    CoderedArticlePage,
+    CoderedArticleIndexPage,
+    CoderedEventIndexPage,
+    CoderedEventPage,
+    CoderedEventOccurrence,
+    CoderedEmail,
+    CoderedFormPage,
+    CoderedWebPage,
+    CoderedLocationIndexPage,
+    CoderedLocationPage
+)
+
+
+class ArticlePage(CoderedArticlePage):
+    """
+    Article, suitable for news or blog content.
+    """
+    class Meta:
+        verbose_name = 'Article'
+        ordering = ['-first_published_at',]
+
+    # Only allow this page to be created beneath an ArticleIndexPage.
+    parent_page_types = ['testapp.ArticleIndexPage']
+
+    template = 'coderedcms/pages/article_page.html'
+    amp_template = 'coderedcms/pages/article_page.amp.html'
+    search_template = 'coderedcms/pages/article_page.search.html'
+
+
+class ArticleIndexPage(CoderedArticleIndexPage):
+    """
+    Shows a list of article sub-pages.
+    """
+    class Meta:
+        verbose_name = 'Article Landing Page'
+    index_order_by_default = ''
+
+    # Override to specify custom index ordering choice/default.
+    index_query_pagemodel = 'testapp.ArticlePage'
+
+    # Only allow ArticlePages beneath this page.
+    subpage_types = ['testapp.ArticlePage']
+
+    template = 'coderedcms/pages/article_index_page.html'
+
+
+class FormPage(CoderedFormPage):
+    """
+    A page with an html <form>.
+    """
+    class Meta:
+        verbose_name = 'Form'
+
+    template = 'coderedcms/pages/form_page.html'
+
+
+class FormPageField(CoderedFormField):
+    """
+    A field that links to a FormPage.
+    """
+    class Meta:
+        ordering = ['sort_order']
+
+    page = ParentalKey('FormPage', related_name='form_fields')
+
+class FormConfirmEmail(CoderedEmail):
+    """
+    Sends a confirmation email after submitting a FormPage.
+    """
+    page = ParentalKey('FormPage', related_name='confirmation_emails')
+
+
+class WebPage(CoderedWebPage):
+    """
+    General use page with featureful streamfield and SEO attributes.
+    Template renders all Navbar and Footer snippets in existance.
+    """
+    class Meta:
+        verbose_name = 'Web Page'
+
+    template = 'coderedcms/pages/web_page.html'
+
+class EventPage(CoderedEventPage):
+    class Meta:
+        verbose_name = 'Event Page'
+
+    parent_page_types = ['testapp.EventIndexPage']
+    subpage_types = []
+    template = 'coderedcms/pages/event_page.html'
+
+
+class EventIndexPage(CoderedEventIndexPage):
+    """
+    Shows a list of event sub-pages.
+    """
+    class Meta:
+        verbose_name = 'Events Landing Page'
+
+    index_query_pagemodel = 'testapp.EventPage'
+    index_order_by_default = ''
+
+    # Only allow EventPages beneath this page.
+    subpage_types = ['testapp.EventPage']
+
+    template = 'coderedcms/pages/event_index_page.html'
+
+
+class EventOccurrence(CoderedEventOccurrence):
+    event = ParentalKey(EventPage, related_name='occurrences')
+
+class LocationPage(CoderedLocationPage):
+    """
+    A page that holds a location.  This could be a store, a restaurant, etc.
+    """
+    class Meta:
+        verbose_name = 'Location Page'
+
+    template = 'coderedcms/pages/location_page.html'
+
+    # Only allow LocationIndexPages above this page.
+    parent_page_types = ['testapp.LocationIndexPage']
+
+
+class LocationIndexPage(CoderedLocationIndexPage):
+    """
+    A page that holds a list of locations and displays them with a Google Map.
+    This does require a Google Maps API Key that can be defined in Settings > Google API Settings
+    """
+    class Meta:
+        verbose_name = 'Location Landing Page'
+
+    # Override to specify custom index ordering choice/default.
+    index_query_pagemodel = 'testapp.LocationPage'
+
+    # Only allow LocationPages beneath this page.
+    subpage_types = ['testapp.LocationPage']
+
+    template = 'coderedcms/pages/location_index_page.html'

+ 28 - 0
coderedcms/tests/urls.py

@@ -0,0 +1,28 @@
+from django.conf import settings
+from django.urls import include, path, re_path
+from django.contrib import admin
+from wagtail.documents import urls as wagtaildocs_urls
+from coderedcms import admin_urls as coderedadmin_urls
+from coderedcms import search_urls as coderedsearch_urls
+from coderedcms import urls as codered_urls
+
+urlpatterns = [
+    # Admin
+    path('django-admin/', admin.site.urls),
+    path('admin/', include(coderedadmin_urls)),
+
+    # Documents
+    path('docs/', include(wagtaildocs_urls)),
+
+    # Search
+    path('search/', include(coderedsearch_urls)),
+
+    # For anything not caught by a more specific rule above, hand over to
+    # the page serving mechanism. This should be the last pattern in
+    # the list:
+    re_path(r'', include(codered_urls)),
+
+    # Alternatively, if you want CMS pages to be served from a subpath
+    # of your site, rather than the site root:
+    #    re_path(r'^pages/', include(codered_urls)),
+]

+ 38 - 1
docs/contributing/index.rst

@@ -2,7 +2,7 @@ Contributing
 ============
 
 
-Developing and testing coderedcms
+Developing CodeRed CMS
 ---------------------------------
 
 To create a test project locally:
@@ -36,6 +36,43 @@ coderedcms should specify the appropriate version in its requirements.txt to pre
     be sure to use a disposable database, as it is likely that the migrations in master will
     not be the same migrations that get released.
 
+Testing CodeRed CMS
+-------------------
+
+To run the built in tests for CodeRed CMS, run the following in your test project's directory:
+
+``python manage.py test coderedcms --settings=coderedcms.tests.settings``
+
+
+Adding New Tests
+----------------
+
+Test coverage at the moment is fairly minimal and it is highly recommended that new features and models include proper unit tests.
+Any testing infrastructure (ie implementations of abstract models and migrations) needed should be added to the ``tests`` app in your
+local copy of CodeRed CMS.  The tests themselves should be in their relevant section in CodeRed CMS (ie tests for 
+models in ``coderedcms.models.page_models`` should be located in ``coderedcms.models.tests.test_page_models``).
+
+For example, here is how you would add tests for a new abstract page type, ``CoderedCustomPage`` that would live in ``coderedcms/models/page_models.py``:
+
+1. Navigate to ``coderedcms/tests/testapp/models.py``
+2. Add the following import: ``from coderedcms.models.page_models import CoderedCustomPage``
+3. Implement a concrete version of ``CoderedCustomPage``, ie ``CustomPage(CoderedCustomPage)``.
+4. Run ``python manage.py makemigrations`` to make new testing migrations.
+5. Navigate to ``coderedcms/models/tests/test_page_models.py``
+6. Add the following import: ``from coderedcms.models import CoderedCustomPage``
+7. Add the following import: ``from coderedcms.tests.testapp.models import CustomPage``
+8. Add the following to the bottom of the file: 
+  ::
+      
+      class CoderedCustomPageTestCase(AbstractPageTestCase, WagtailPageTests):
+          model = CoderedCustomPage
+9. Add the following to the bottom of the file: 
+  ::
+      
+      class CustomPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
+          model = CustomPage
+10. Write any specific test cases that ``CoderedCustomPage`` and ``CustomPage`` may require.
+
 
 Contributor guidelines
 ----------------------

Some files were not shown because too many files changed in this diff