diff --git a/.gitignore b/.gitignore index 5d236f7..c6fb477 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ *.pyc *.mo +geckodriver.log .idea/ django_dav_events.egg-info/ backup/ env/ +test-screenshots/ diff --git a/dav_base/console_scripts/django_project_config/settings-dav_base.py b/dav_base/console_scripts/django_project_config/settings-dav_base.py index 3833852..a86f7bc 100644 --- a/dav_base/console_scripts/django_project_config/settings-dav_base.py +++ b/dav_base/console_scripts/django_project_config/settings-dav_base.py @@ -4,3 +4,7 @@ EMAIL_SENDER = 'DAV heinzel ' EMAIL_BASE_URL = 'http://localhost:8000' EMAIL_SUBJECT_PREFIX = u'[DAV heinzel]' + +# The following settings are for the test suite. Do not change them. +TEST_SETTING = 'do not change this value' +# NO_TEST_SETTING = 'do not set this' diff --git a/dav_base/emails.py b/dav_base/emails.py index 6943d5b..023c109 100644 --- a/dav_base/emails.py +++ b/dav_base/emails.py @@ -48,7 +48,7 @@ class AbstractMail(object): def _get_reply_to(self): return None - def send(self): + def send(self, fail_silently=False): subject = self._get_subject() body = self._get_body() sender = self._get_sender() @@ -56,5 +56,8 @@ class AbstractMail(object): reply_to = self._get_reply_to() email = EmailMessage(subject=subject, body=body, from_email=sender, to=recipients, reply_to=reply_to) - logger.info(u'Send %s to %s', self.__class__.__name__, recipients) - email.send() + if fail_silently: + logger.info(u'Fake sending %s to %s', self.__class__.__name__, recipients) + else: + logger.info(u'Send %s to %s', self.__class__.__name__, recipients) + email.send(fail_silently=fail_silently) diff --git a/dav_base/tests/generic.py b/dav_base/tests/generic.py new file mode 100644 index 0000000..20b028e --- /dev/null +++ b/dav_base/tests/generic.py @@ -0,0 +1,180 @@ +import datetime +import os +import sys +import urllib +from unittest import skip +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from django.test import SimpleTestCase, TestCase, tag +from django.urls import reverse +from selenium import webdriver + + +def skip_unless_tag_option(): + if '--tag' in sys.argv: + return lambda func: func + else: + return skip('Skipped unless --tag option is used') + + +class Url(object): + def __init__(self, location, name=None, func=None, **kwargs): + self.location = location + self.name = name + self.func = func + self.redirect = kwargs.get('redirect', False) + self.status_code = kwargs.get('status_code', 200) + self.follow = kwargs.get('follow', False) + + +class UrlsTestCase(SimpleTestCase): + urls = () + + def test_locations(self): + for url in self.urls: + if url.location: + response = self.client.get(url.location, follow=url.follow) + + if url.redirect: + self.assertRedirects(response, url.redirect) + else: + self.assertEqual(response.status_code, url.status_code, + 'Getting \'{}\' is not OK'.format(url.location)) + + if url.func: + self.assertEqual(response.resolver_match.func.__name__, + url.func.__name__, + 'Getting \'{}\' resolve to wrong function'.format(url.location)) + + def test_names(self): + for url in self.urls: + if url.name: + response = self.client.get(reverse(url.name), follow=url.follow) + + if url.redirect: + self.assertRedirects(response, url.redirect) + else: + self.assertEqual(response.status_code, url.status_code, + 'Getting url named \'{}\' is not OK'.format(url.name)) + + if url.func: + self.assertEqual(response.resolver_match.func.__name__, + url.func.__name__, + 'Getting url named \'{}\' resolve to wrong function'.format(url.name)) + + +class FormDataSet(object): + def __init__(self, data, expected_errors=None, form_kwargs=None): + self.data = data + self.expected_errors = expected_errors + self.form_kwargs = form_kwargs + + +class FormsTestCase(TestCase): + form_class = None + valid_data_sets = () + invalid_data_sets = () + + def test_valid_data(self, form_class=None, data_sets=None, form_kwargs=None): + if form_class is None: + form_class = self.form_class + if form_class is None: + return True + + if data_sets is None: + data_sets = self.valid_data_sets + + for data_set in data_sets: + fk = {} + if form_kwargs is not None: + fk.update(form_kwargs) + if data_set.form_kwargs is not None: + fk.update(data_set.form_kwargs) + fk['data'] = data_set.data + form = form_class(**fk) + if not form.is_valid(): + errors = [] + for key in form.errors.as_data(): + for ve in form.errors[key].as_data(): + errors.append(u'%s (%s)' % (ve.code, ve.message)) + self.fail(u'Invalid form data \'%s\': %s' % (data_set.data, errors)) + + def test_invalid_data(self, form_class=None, data_sets=None, form_kwargs=None): + if form_class is None: + form_class = self.form_class + if form_class is None: + return True + + if data_sets is None: + data_sets = self.invalid_data_sets + + for data_set in data_sets: + fk = {} + if form_kwargs is not None: + fk.update(form_kwargs) + if data_set.form_kwargs is not None: + fk.update(data_set.form_kwargs) + fk['data'] = data_set.data + + form = form_class(**fk) + + if form.is_valid(): + self.fail('Valid form data: \'%s\'' % data_set.data) + if data_set.expected_errors: + error_dicts = form.errors.as_data() + for key, code in data_set.expected_errors: + error_codes = [ve.code for ve in error_dicts[key]] + self.assertIn(code, error_codes) + + +class ScreenshotTestCase(StaticLiveServerTestCase): + locations = () + + @classmethod + def setUpClass(cls): + super(ScreenshotTestCase, cls).setUpClass() + + # screenshot_base_dir = os.path.join('/', 'tmp', 'test-screenshots') + screenshot_base_dir = 'test-screenshots' + cls.screenshot_path = screenshot_base_dir + + def setUp(self): + super(ScreenshotTestCase, self).setUp() + self.selenium = webdriver.Firefox() + self.selenium.set_window_size(1024, 768) + + def tearDown(self): + self.selenium.quit() + super(ScreenshotTestCase, self).tearDown() + + def complete_url(self, location): + return '{}/{}'.format(self.live_server_url, location.lstrip('/')) + + def sanitize_filename(self, location): + location = location.lstrip('/') + if location == '': + return 'root' + r = urllib.quote(location, '') + return r + + def save_screenshot(self, name=None): + if self.screenshot_path: + if name is None: + location = self.selenium.current_url + if location.startswith(self.live_server_url): + location = location[len(self.live_server_url):] + name = location + + now = datetime.datetime.now() + base_name = '{timestamp}-{name}.png'.format(name=self.sanitize_filename(name), + timestamp=now.strftime('%Y%m%d-%H%M%S')) + path = os.path.join(self.screenshot_path, base_name) + if not os.path.isdir(self.screenshot_path): + os.makedirs(self.screenshot_path, 0700) + self.selenium.save_screenshot(path) + + @skip_unless_tag_option() + @tag('screenshots') + def test_screenshots(self): + for location in self.locations: + self.selenium.get(self.complete_url(location)) + self.save_screenshot() diff --git a/dav_base/tests/test_apps.py b/dav_base/tests/test_apps.py index 128a213..740b3f7 100644 --- a/dav_base/tests/test_apps.py +++ b/dav_base/tests/test_apps.py @@ -1,8 +1,8 @@ from django.apps import apps -from django.test import TestCase +from django.test import SimpleTestCase -class AppsTestCase(TestCase): +class AppsTestCase(SimpleTestCase): def setUp(self): app_config = apps.get_containing_app_config(__package__) self.settings = app_config.settings diff --git a/dav_base/tests/test_config.py b/dav_base/tests/test_config.py new file mode 100644 index 0000000..d2ed44c --- /dev/null +++ b/dav_base/tests/test_config.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +from django.apps import apps +from django.core.exceptions import ImproperlyConfigured +from django.test import SimpleTestCase + +from ..config.apps import DefaultSetting, AppSettings, AppConfig +from ..apps import AppConfig as RealAppConfig + +TEST_SETTING_VALUE = 'do not change this value' + + +class DefaultSettingTestCase(SimpleTestCase): + def test_key_name(self): + name = 'somename' + key_name = 'SoMeKeY' + + setting = DefaultSetting(name, None) + self.assertEqual(setting.key_name, name.upper()) + + setting = DefaultSetting(name, None, key_name=key_name) + self.assertEqual(setting.key_name, key_name) + + def test_validate(self): + name = 'somename' + + setting = DefaultSetting(name, None, validator=None) + try: + setting.validate(False) + except ImproperlyConfigured: + self.fail('Disabled validation validated invalid :(') + + def callable_validator(value): + return (value % 2) == 0 + + valid_values = (2, 4) + invalid_values = (3, 5) + + setting = DefaultSetting(name, None, validator=callable_validator) + for val in valid_values: + try: + setting.validate(val) + except ImproperlyConfigured: + self.fail('Callable validated valid value invalid :(') + + for val in invalid_values: + with self.assertRaises(ImproperlyConfigured): + setting.validate(val) + + re_validator = r'[a-z]' + + valid_values = u'aocd' + invalid_values = u'Aƶ1.' + + setting = DefaultSetting(name, None, validator=re_validator) + for val in valid_values: + try: + setting.validate(val) + except ImproperlyConfigured: + self.fail('Regular Expression validated valid value invalid :(') + + for val in invalid_values: + with self.assertRaises(ImproperlyConfigured): + setting.validate(val) + + +class AppSettingsTestCase(SimpleTestCase): + def setUp(self): + self.app_name = 'dav_base' + self.test_setting_value = TEST_SETTING_VALUE + + def test_defaults(self): + """Test the default value for optional and unset settings""" + default_settings = ( + DefaultSetting('no_test_setting', 1), + DefaultSetting('no_test_setting_2', 2), + ) + app_settings = AppSettings(self.app_name, default_settings) + self.assertEqual(app_settings.no_test_setting, 1) + self.assertEqual(app_settings.no_test_setting_2, 2) + + def test_improperlyconfigured(self): + """Test if mandatory but unset setting raise correct exception""" + default_settings = ( + DefaultSetting('no_test_setting', ImproperlyConfigured), + ) + try: + app_settings = AppSettings(self.app_name, default_settings) + except ImproperlyConfigured as e: + self.assertEqual(str(e), 'NO_TEST_SETTING must be defined in main.settings-dav_base') + + def test_settings_from_file(self): + """Test if value from settings file eliminate exception""" + default_settings = ( + DefaultSetting('test_setting', ImproperlyConfigured), + ) + app_settings = AppSettings(self.app_name, default_settings) + self.assertEqual(app_settings.test_setting, self.test_setting_value) + + # Test if value from settings file overwrites default value + default_settings = ( + DefaultSetting('test_setting', 1), + ) + app_settings = AppSettings(self.app_name, default_settings) + self.assertEqual(app_settings.test_setting, self.test_setting_value) + + def test_key_name(self): + default_settings = ( + DefaultSetting('test_setting', 1, key_name='NO_TEST_SETTING'), + DefaultSetting('no_test_setting', ImproperlyConfigured, key_name='TEST_SETTING'), + ) + app_settings = AppSettings(self.app_name, default_settings) + self.assertEqual(app_settings.test_setting, 1) + self.assertEqual(app_settings.no_test_setting, self.test_setting_value) + + +class AppConfigTestCase(SimpleTestCase): + def test_init(self): + app_name = 'dav_base' + import dav_base as app_module + + test_default_settings = ( + DefaultSetting('test_setting', ImproperlyConfigured), + DefaultSetting('no_test_setting', 1), + ) + + class TestAppConfig(AppConfig): + name = 'not_dav_base' + verbose_name = u'DAV Base App' + default_settings = test_default_settings + + app_config = TestAppConfig(app_name, app_module) + app_settings = app_config.settings + + self.assertEqual(app_config.name, app_name) + self.assertEqual(app_settings.test_setting, TEST_SETTING_VALUE) + self.assertEqual(app_settings.no_test_setting, 1) + + def test_integrated(self): + app_config = apps.get_containing_app_config(__package__) + self.assertEqual(app_config.__class__, RealAppConfig) + self.assertIsInstance(app_config, AppConfig) + self.assertEqual(app_config.settings.__class__, AppSettings) + + +class ModuleMetaTestCase(SimpleTestCase): + def test_some(self): + # TODO + pass + + +class ModuleConfigTestCase(SimpleTestCase): + def test_some(self): + # TODO + pass diff --git a/dav_base/tests/test_emails.py b/dav_base/tests/test_emails.py index 6152ba8..c5484cc 100644 --- a/dav_base/tests/test_emails.py +++ b/dav_base/tests/test_emails.py @@ -1,20 +1,20 @@ from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase +from django.test import SimpleTestCase from ..emails import AbstractMail -class EmailsTestCase(TestCase): +class EmailsTestCase(SimpleTestCase): def setUp(self): self.email = AbstractMail() def test_send(self): try: self.email.send() - self.assertTrue(False, 'AbstractEmail.send() does not raise an Exception') + self.fail('AbstractEmail.send() does not raise an Exception') except NotImplementedError: pass except ImproperlyConfigured: pass except Exception: - self.assertTrue(False, 'AbstractEmail.send() raised unexpected Exception') + self.fail('AbstractEmail.send() raised unexpected Exception') diff --git a/dav_base/tests/test_screenshots.py b/dav_base/tests/test_screenshots.py new file mode 100644 index 0000000..c076e2c --- /dev/null +++ b/dav_base/tests/test_screenshots.py @@ -0,0 +1,8 @@ +from .generic import ScreenshotTestCase + + +class TestCase(ScreenshotTestCase): + locations = ( + '/', + '404', + ) diff --git a/dav_base/tests/test_templates.py b/dav_base/tests/test_templates.py new file mode 100644 index 0000000..b8b193c --- /dev/null +++ b/dav_base/tests/test_templates.py @@ -0,0 +1,48 @@ +from django.test import SimpleTestCase + + +class TemplatesTestCase(SimpleTestCase): + def setUp(self): + super(TemplatesTestCase, self).setUp() + self.response_from_root_view = self.client.get('/') + + def test_template_usage(self): + response = self.response_from_root_view + self.assertTemplateUsed(response, 'dav_base/base.html') + self.assertTemplateUsed(response, 'project_name.html') + + def test_html_head(self): + response = self.response_from_root_view + + html_needles = ( + # favicon + '', + # bootstrap css + '', + # css file for jquery.dataTables.js + '', + # local css file + '', + # jquery.js file + '', + # jquery.dataTables.js file + '', + # bootstrap js file + '', + ) + + for needle in html_needles: + self.assertInHTML(needle, response.content) + + def test_page_footer(self): + response = self.response_from_root_view + + html = """ + +""" + + self.assertInHTML(html, response.content) diff --git a/dav_base/tests/test_urls.py b/dav_base/tests/test_urls.py index a612642..d76af9c 100644 --- a/dav_base/tests/test_urls.py +++ b/dav_base/tests/test_urls.py @@ -1,33 +1,9 @@ -from django.test import TestCase, Client -from django.urls import reverse - +from generic import Url, UrlsTestCase from ..views import RootView -class UrlsTestCase(TestCase): - def setUp(self): - self.client = Client() - - def test_root(self): - url = '/' - response = self.client.get(url, follow=False) - self.assertEqual(response.status_code, 200, - 'Getting {} is not OK'.format(url)) - self.assertEqual(response.resolver_match.func.__name__, - RootView.as_view().__name__, - 'Getting {} resolve to wrong view'.format(url)) - - def test_root_by_name(self): - name = 'root' - response = self.client.get(reverse(name), follow=False) - self.assertEqual(response.status_code, 200, - 'Getting url named \'{}\' is not OK'.format(name)) - self.assertEqual(response.resolver_match.func.__name__, - RootView.as_view().__name__, - 'Getting url named \'{}\' resolve to wrong view'.format(name)) - - def test_djangoadmin(self): - url = '/djangoadmin' - response = self.client.get(url, follow=True) - self.assertEqual(response.status_code, 200, - 'Getting {} is not OK'.format(url)) +class TestCase(UrlsTestCase): + urls = ( + Url('/', 'root', RootView.as_view()), + Url('/djangoadmin', follow=True), + ) diff --git a/dav_base/tests/test_views.py b/dav_base/tests/test_views.py index 22874ac..ab20307 100644 --- a/dav_base/tests/test_views.py +++ b/dav_base/tests/test_views.py @@ -1,10 +1,22 @@ -from django.test import TestCase, Client +from django.test import SimpleTestCase + +from ..views import RootView -class ViewsTestCase(TestCase): - def setUp(self): - self.client = Client() - +class ViewsTestCase(SimpleTestCase): def test_root(self): - response = self.client.get('/', follow=False) + view = RootView() + template_names = view.get_template_names() + self.assertEqual(len(template_names), 1) + self.assertIn('dav_base/root.html', template_names) + context = view.get_context_data() + self.assertIn('root_urls', context) + + def test_integrated_root(self): + response = self.client.get('/') + self.assertTemplateUsed(response, 'dav_base/root.html') self.assertIn('root_urls', response.context, '\'root_urls\' not in context of root view') + + # TODO + # Maybe we should set a defined module config, so we could + # test the content of the root_urls context variable.