Merge pull request #86 from BobGirard/unit-tests-1

Updated: Unit tests (#1)
This commit is contained in:
fonfon 2015-02-23 14:32:41 +01:00
commit 16fb4c2f3c
6 changed files with 479 additions and 1 deletions

View File

@ -57,8 +57,10 @@ def read():
directory = os.path.dirname(os.path.realpath(__file__))
directory = os.path.join(directory, '..')
CONFIG_FILE = os.path.join(directory, 'plinth.config')
if not os.path.isfile(CONFIG_FILE):
raise FileNotFoundError('No plinth.config file could be found.')
parser = configparser.SafeConfigParser(
parser = configparser.ConfigParser(
defaults={
'root': os.path.realpath(directory),
})

View File

@ -0,0 +1,8 @@
[Name]
product_name = Plinth
box_name = FreedomBox
[Path]
[Network]

View File

@ -0,0 +1,4 @@
[Name]
product_name = Plinth
box_name = FreedomBox

250
plinth/tests/test_cfg.py Normal file
View File

@ -0,0 +1,250 @@
#!/usr/bin/python3
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import configparser
import os
import shutil
import unittest
from plinth import cfg
CONFIG_FILENAME = 'plinth.config'
TEST_CONFIG_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'data')
TEST_CONFIG_FILE = os.path.join(TEST_CONFIG_DIR, CONFIG_FILENAME)
SAVED_CONFIG_FILE = os.path.join(TEST_CONFIG_DIR,
CONFIG_FILENAME + '.official')
CONFIG_FILE_WITH_MISSING_OPTIONS = os.path.join(TEST_CONFIG_DIR,
CONFIG_FILENAME +
'.with_missing_options')
CONFIG_FILE_WITH_MISSING_SECTIONS = os.path.join(TEST_CONFIG_DIR,
CONFIG_FILENAME +
'.with_missing_sections')
class CfgTestCase(unittest.TestCase):
"""Verify that the Plinth configuration module behaves as expected.
This class deals with involving the plinth.config file in testing by
(1) independently locating the copy of the file that the cfg module would
find and read, then (2) copying that file to plinth/tests/data for use for
the actual tests, and finally (3) redirecting cfg (via its
DEFAULT_CONFIG_FILE attribute) to read that test copy. The test copy is
then deleted as part of the test case cleanup.
"""
default_config_file = ''
default_root = ''
default_config_file_present = False
fallback_config_file = ''
fallback_root = ''
fallback_config_file_present = False
# Setup and Teardown
@classmethod
def setUpClass(cls):
"""Locate and copy the official plinth.config file."""
# Save the cfg module default values
cls.default_config_file = cfg.DEFAULT_CONFIG_FILE
cls.default_root = cfg.DEFAULT_ROOT
# Look for default config file
cls.default_config_file_present =\
os.path.isfile(cls.default_config_file)
# Look for fallback (non-default) config file
cls.fallback_root = os.path.realpath('.')
cls.fallback_config_file = os.path.join(cls.fallback_root,
CONFIG_FILENAME)
cls.fallback_config_file_present =\
os.path.isfile(cls.fallback_config_file)
# If neither file is found...
if not (cls.default_config_file_present or
cls.fallback_config_file_present):
raise FileNotFoundError('File {} could not be found.'
.format(CONFIG_FILENAME))
# Copy an official config file to the plinth/tests/data directory...
if cls.default_config_file_present:
shutil.copy2(cls.default_config_file, TEST_CONFIG_FILE)
elif cls.fallback_config_file_present:
shutil.copy2(cls.fallback_config_file, TEST_CONFIG_FILE)
# ...and point cfg to that file as the default
cfg.DEFAULT_CONFIG_FILE = TEST_CONFIG_FILE
cfg.DEFAULT_ROOT = TEST_CONFIG_DIR
@classmethod
def tearDownClass(cls):
"""Cleanup after all tests are completed."""
# Restore the cfg module default values
cfg.DEFAULT_CONFIG_FILE = cls.default_config_file
cfg.DEFAULT_ROOT = cls.default_root
# Delete the test config file(s)
if os.path.isfile(TEST_CONFIG_FILE):
os.remove(TEST_CONFIG_FILE)
if os.path.isfile(SAVED_CONFIG_FILE):
os.remove(SAVED_CONFIG_FILE)
# Tests
def test_read_main_menu(self):
"""Verify that the cfg.main_menu container is initially empty."""
# Menu should be empty before...
self.assertEqual(len(cfg.main_menu.items), 0)
cfg.read()
# ...and after reading the config file
self.assertEqual(len(cfg.main_menu.items), 0)
def test_read_default_config_file(self):
"""Verify that the default config file can be read correctly."""
# Read the plinth.config file directly
parser = self.read_config_file(TEST_CONFIG_FILE, TEST_CONFIG_DIR)
# Read the plinth.config file via the cfg module
cfg.read()
# Compare the two results
self.compare_configurations(parser)
def test_read_fallback_config_file(self):
"""Verify that the fallback config file can be read correctly.
This test will be executed only if there is a fallback (non-default)
configuration file available for reading. If so, the cfg default
values for config filename and root will be temporarily modified to
prevent any default file from being found, thus allowing the fallback
file to be located and read.
"""
if not self.fallback_config_file_present:
self.skipTest('A fallback copy of {} is not available.'
.format(CONFIG_FILENAME))
else:
try:
cfg.DEFAULT_CONFIG_FILE = '/{}'.format(CONFIG_FILENAME)
cfg.DEFAULT_ROOT = '/'
parser = self.read_config_file(self.fallback_config_file,
self.fallback_root)
cfg.read()
self.compare_configurations(parser)
finally:
cfg.DEFAULT_CONFIG_FILE = self.default_config_file
cfg.DEFAULT_ROOT = self.default_root
def test_read_missing_config_file(self):
"""Verify that an exception is raised when there's no config file.
This test will be executed only if the fallback (non-default) copy of
plinth.config is NOT present. If there is only a single, default
config file available, then that file can be copied to a test area and
be hidden by temporary renaming. But if the default file is hidden
and the fallback file can be found in its place, the fallback file
will not be renamed. Instead, the entire test will be skipped.
"""
if self.fallback_config_file_present:
self.skipTest(
'Fallback copy of {} cannot be hidden to establish the test'
'pre-condition.'.format(CONFIG_FILENAME))
else:
with self.assertRaises(FileNotFoundError):
try:
self.rename_test_config_file()
cfg.read()
finally:
self.restore_test_config_file()
def test_read_config_file_with_missing_sections(self):
"""Verify that missing configuration sections can be detected."""
self.assertRaises(configparser.NoSectionError,
self.read_temp_config_file,
CONFIG_FILE_WITH_MISSING_SECTIONS)
def test_read_config_file_with_missing_options(self):
"""Verify that missing configuration options can be detected."""
self.assertRaises(configparser.NoOptionError,
self.read_temp_config_file,
CONFIG_FILE_WITH_MISSING_OPTIONS)
# Helper Methods
def read_config_file(self, config_file, root):
"""Read the specified configuration file independently from cfg.py."""
parser = configparser.ConfigParser(defaults={'root': root})
parser.read(config_file)
return parser
def compare_configurations(self, parser):
"""Compare two sets of configuration values."""
# Note that the count of items within each section includes the number
# of default items (1, for 'root').
self.assertEqual(3, len(parser.items('Name')))
self.assertEqual(parser.get('Name', 'product_name'), cfg.product_name)
self.assertEqual(parser.get('Name', 'box_name'), cfg.box_name)
self.assertEqual(13, len(parser.items('Path')))
self.assertEqual(parser.get('Path', 'root'), cfg.root)
self.assertEqual(parser.get('Path', 'file_root'), cfg.file_root)
self.assertEqual(parser.get('Path', 'config_dir'), cfg.config_dir)
self.assertEqual(parser.get('Path', 'data_dir'), cfg.data_dir)
self.assertEqual(parser.get('Path', 'store_file'), cfg.store_file)
self.assertEqual(parser.get('Path', 'actions_dir'),
cfg.actions_dir)
self.assertEqual(parser.get('Path', 'doc_dir'), cfg.doc_dir)
self.assertEqual(parser.get('Path', 'status_log_file'),
cfg.status_log_file)
self.assertEqual(parser.get('Path', 'access_log_file'),
cfg.access_log_file)
self.assertEqual(parser.get('Path', 'pidfile'), cfg.pidfile)
self.assertEqual(5, len(parser.items('Network')))
self.assertEqual(parser.get('Network', 'host'), cfg.host)
self.assertEqual(int(parser.get('Network', 'port')), cfg.port)
self.assertEqual(parser.get('Network', 'secure_proxy_ssl_header'),
cfg.secure_proxy_ssl_header)
self.assertEqual(parser.get('Network', 'use_x_forwarded_host'),
cfg.use_x_forwarded_host)
def read_temp_config_file(self, test_file):
"""Read the specified test configuration file."""
self.replace_test_config_file(test_file)
try:
cfg.read()
finally:
self.restore_test_config_file()
def rename_test_config_file(self):
"""Rename the test config file so that it can't be read."""
shutil.move(TEST_CONFIG_FILE, SAVED_CONFIG_FILE)
def replace_test_config_file(self, test_file):
"""Replace plinth.config with the specified temporary config file."""
self.rename_test_config_file()
shutil.copy2(test_file, TEST_CONFIG_FILE)
def restore_test_config_file(self):
"""Restore the test plinth.config file."""
if os.path.isfile(TEST_CONFIG_FILE):
os.remove(TEST_CONFIG_FILE)
shutil.move(SAVED_CONFIG_FILE, TEST_CONFIG_FILE)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,64 @@
#!/usr/bin/python3
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from django.http import HttpRequest
import unittest
from plinth import context_processors as cp
class ContextProcessorsTestCase(unittest.TestCase):
"""Verify behavior of the context_processors module."""
def test_common(self):
"""Verify that the 'common' function returns the correct values."""
request = HttpRequest()
request.path = '/aaa/bbb/ccc/'
response = cp.common(request)
self.assertIsNotNone(response)
config = response['cfg']
self.assertIsNotNone(config)
self.assertEqual('Plinth', config.product_name)
self.assertEqual('FreedomBox', config.box_name)
submenu = response['submenu']
self.assertIsNone(submenu)
urls = response['active_menu_urls']
self.assertIsNotNone(urls)
self.assertEqual(['/', '/aaa/', '/aaa/bbb/', '/aaa/bbb/ccc/'], urls)
def test_common_border_conditions(self):
"""Verify that the 'common' functions works for border conditions."""
request = HttpRequest()
request.path = ''
response = cp.common(request)
self.assertEqual([], response['active_menu_urls'])
request.path = '/'
response = cp.common(request)
self.assertEqual(['/'], response['active_menu_urls'])
request.path = '/aaa/bbb'
response = cp.common(request)
self.assertEqual(['/', '/aaa/'], response['active_menu_urls'])
if __name__ == '__main__':
unittest.main()

150
plinth/tests/test_menu.py Normal file
View File

@ -0,0 +1,150 @@
#!/usr/bin/python3
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from django.http import HttpRequest
import random
import unittest
from plinth.menu import Menu
URL_TEMPLATE = '/a{}/b{}/c{}/'
class MenuTestCase(unittest.TestCase):
"""Verify the behavior of the Plinth Menu class."""
# Test methods
def test_menu_creation_without_arguments(self):
"""Verify the Menu state without initialization parameters."""
menu = Menu()
self.assertEqual('', menu.label)
self.assertEqual('', menu.icon)
self.assertEqual('#', menu.url)
self.assertEqual(50, menu.order)
self.assertEqual(0, len(menu.items))
def test_menu_creation_with_arguments(self):
"""Verify the Menu state with initialization parameters."""
expected_label = 'Label'
expected_icon = 'Icon'
expected_url = '/aaa/bbb/ccc/'
expected_order = 42
menu = Menu(expected_label, expected_icon, expected_url,
expected_order)
self.assertEqual(expected_label, menu.label)
self.assertEqual(expected_icon, menu.icon)
self.assertEqual(expected_url, menu.url)
self.assertEqual(expected_order, menu.order)
self.assertEqual(0, len(menu.items))
@unittest.skip('requires configuring Django beforehand')
def test_get(self):
"""Verify that a menu item can be correctly retrieved."""
expected_label = 'Label2'
expected_icon = 'Icon2'
expected_url = '/ddd/eee/fff/'
expected_order = 2
menu = Menu()
menu.add_item(expected_label, expected_icon, expected_url,
expected_order)
actual_item = menu.get(expected_url)
self.assertIsNotNone(actual_item)
self.assertEqual(expected_label, actual_item.label)
self.assertEqual(expected_icon, actual_item.icon)
self.assertEqual(expected_url, actual_item.url)
self.assertEqual(expected_order, actual_item.order)
self.assertEqual(0, len(actual_item.items))
def test_sort_items(self):
"""Verify that menu items are sorted correctly."""
menu = self.build_menu()
# Verify that the order of every item is equal to or greater
# than the order of the item preceding it
for index in range(1, 5):
self.assertGreaterEqual(menu.items[index].order,
menu.items[index - 1].order)
@unittest.skip('requires configuring Django beforehand')
def test_add_urlname(self):
"""Verify that a named URL can be added to a menu correctly."""
def test_add_item(self):
"""Verify that a menu item can be correctly added."""
expected_label = 'Label3'
expected_icon = 'Icon3'
expected_url = '/ggg/hhh/iii/'
expected_order = 3
menu = Menu()
actual_item = menu.add_item(expected_label, expected_icon,
expected_url, expected_order)
self.assertIsNotNone(actual_item)
self.assertEqual(expected_label, actual_item.label)
self.assertEqual(expected_icon, actual_item.icon)
self.assertEqual(expected_url, actual_item.url)
self.assertEqual(expected_order, actual_item.order)
self.assertEqual(0, len(actual_item.items))
def test_active_item(self):
"""Verify that an active menu item can be correctly retrieved."""
menu = self.build_menu()
for index in range(1, 8):
request = HttpRequest()
request.path = URL_TEMPLATE.format(index, index, index)
item = menu.active_item(request)
if index <= 5:
self.assertEqual('Item' + str(index), item.label)
self.assertEqual(request.path, item.url)
else:
self.assertIsNone(item)
def test_active_item_when_inside_subpath(self):
"""Verify that the current URL could be a sub-path of menu item."""
menu = self.build_menu()
expected_url = URL_TEMPLATE.format(1, 1, 1)
request = HttpRequest()
request.path = expected_url + 'd/e/f/'
item = menu.active_item(request)
self.assertEqual('Item1', item.label)
self.assertEqual(expected_url, item.url)
# Helper methods
def build_menu(self, size=5):
"""Build a menu with the specified number of items."""
random.seed()
item_data = []
for index in range(1, size + 1):
item_data.append(['Item' + str(index),
'Icon' + str(index),
URL_TEMPLATE.format(index, index, index),
random.randint(0, 1000)])
menu = Menu()
for data in item_data:
menu.add_item(data[0], data[1], data[2], data[3])
return menu
if __name__ == '__main__':
unittest.main()