diff --git a/.gitignore b/.gitignore index b6ae075..1ba1595 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ TGTest-* devtools/tests/data/ dist/ build/ +\#*\# +.\#* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 1bee481..348420a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - "3.6" - "3.7" - "3.8" + - "3.9-dev" install: - "pip install --upgrade setuptools" diff --git a/devtools/gearbox/quickstart/command.py b/devtools/gearbox/quickstart/command.py index 9b29435..ecb9f95 100644 --- a/devtools/gearbox/quickstart/command.py +++ b/devtools/gearbox/quickstart/command.py @@ -276,3 +276,14 @@ def overwrite_templates(template_type): # remove existing migrations directory package_migrations_dir = os.path.abspath('migration') shutil.rmtree(package_migrations_dir, ignore_errors=True) + + tests_dir = os.path.abspath(os.path.join(opts.package, 'tests')) + if not opts.database: + print('database support disabled, stripping away model tests') + os.remove(os.path.abspath(tests_dir + '/_conftest/models.py')) + shutil.rmtree(os.path.abspath(tests_dir + '/models')) + if not opts.auth: + print('auth disabled, removing relative tests') + os.remove(os.path.abspath(tests_dir + '/functional/test_authentication.py')) + if opts.database: + os.remove(os.path.abspath(tests_dir + '/models/test_auth.py')) diff --git a/devtools/gearbox/quickstart/template/+dot+coveragerc b/devtools/gearbox/quickstart/template/+dot+coveragerc new file mode 100644 index 0000000..3452541 --- /dev/null +++ b/devtools/gearbox/quickstart/template/+dot+coveragerc @@ -0,0 +1,19 @@ +# refer to https://coverage.readthedocs.io/en/latest/config.html +[run] +source = {{package}} + +[report] +show_missing = True + +# if you don't omit tests directory you have more chances of seeing two tests with same name +omit = + setup.py + migration/* + # tests/* + +# fail test suite if coverage drops below 100% (if you uncomment it) +# this does not work for nosetests, set it in setup.cfg (min-cover-percentage) +# fail_under = 100 + +# Don’t include files in the report that are 100% covered files +skip_covered = True \ No newline at end of file diff --git a/devtools/gearbox/quickstart/template/+package+/config/app_cfg.py_tmpl b/devtools/gearbox/quickstart/template/+package+/config/app_cfg.py_tmpl index 0d53903..f9bdc61 100644 --- a/devtools/gearbox/quickstart/template/+package+/config/app_cfg.py_tmpl +++ b/devtools/gearbox/quickstart/template/+package+/config/app_cfg.py_tmpl @@ -10,6 +10,16 @@ from tg import FullStackApplicationConfigurator import {{package}} from {{package}} import model, lib +{{if auth}} +from tg.exceptions import HTTPFound +try: + from urllib.parse import parse_qs, urlencode +except ImportError: # pragma: no cover # py2.7 compatibility + from urlparse import parse_qs + from urllib import urlencode +{{endif}} + + base_config = FullStackApplicationConfigurator() # General configuration @@ -115,13 +125,6 @@ class ApplicationAuthMetadata(TGAuthMetadata): login = None if login is None: - try: - from urllib.parse import parse_qs, urlencode - except ImportError: - from urlparse import parse_qs - from urllib import urlencode - from tg.exceptions import HTTPFound - params = parse_qs(environ['QUERY_STRING']) params.pop('password', None) # Remove password in case it was there if user is None: @@ -162,13 +165,6 @@ class ApplicationAuthMetadata(TGAuthMetadata): login = None if login is None: - try: - from urllib.parse import parse_qs, urlencode - except ImportError: - from urlparse import parse_qs - from urllib import urlencode - from tg.exceptions import HTTPFound - params = parse_qs(environ['QUERY_STRING']) params.pop('password', None) # Remove password in case it was there if user is None: @@ -235,7 +231,7 @@ try: # Enable DebugBar if available, install tgext.debugbar to turn it on from tgext.debugbar import enable_debugbar enable_debugbar(base_config) -except ImportError: +except ImportError: # pragma: no cover pass {{endif}} diff --git a/devtools/gearbox/quickstart/template/+package+/lib/helpers.py_tmpl b/devtools/gearbox/quickstart/template/+package+/lib/helpers.py_tmpl index f9f6bf7..d69effc 100644 --- a/devtools/gearbox/quickstart/template/+package+/lib/helpers.py_tmpl +++ b/devtools/gearbox/quickstart/template/+package+/lib/helpers.py_tmpl @@ -19,7 +19,4 @@ def icon(icon_name): # Import commonly used helpers from WebHelpers2 and TG from tg.util.html import script_json_encode -try: - from webhelpers2 import date, html, number, misc, text -except SyntaxError: - log.error("WebHelpers2 helpers not available with this Python Version") +from webhelpers2 import date, html, number, misc, text diff --git a/devtools/gearbox/quickstart/template/+package+/model/auth.py_tmpl b/devtools/gearbox/quickstart/template/+package+/model/auth.py_tmpl index 9908cb9..0c890cb 100644 --- a/devtools/gearbox/quickstart/template/+package+/model/auth.py_tmpl +++ b/devtools/gearbox/quickstart/template/+package+/model/auth.py_tmpl @@ -74,7 +74,7 @@ class Group(DeclarativeBase): def __repr__(self): return '' % repr(self.group_name) - def __unicode__(self): + def __str__(self): return self.group_name @@ -102,7 +102,7 @@ class User(DeclarativeBase): repr(self.display_name) ) - def __unicode__(self): + def __str__(self): return self.display_name or self.user_name @property @@ -110,7 +110,7 @@ class User(DeclarativeBase): """Return a set with all permissions granted to the user.""" perms = set() for g in self.groups: - perms = perms | set(g.permissions) + perms.update(g.permissions) return perms @classmethod @@ -192,7 +192,7 @@ class Permission(DeclarativeBase): def __repr__(self): return '' % repr(self.permission_name) - def __unicode__(self): + def __str__(self): return self.permission_name {{elif auth == 'ming'}} @@ -292,6 +292,11 @@ class User(MappedClass): def permissions(self): return Permission.query.find(dict(_groups={'$in':self._groups})).all() + @classmethod + def by_user_name(cls, user_name): + """Return the user object whose user name is ``user_name``.""" + return cls.query.get(user_name=user_name) + @classmethod def by_email_address(cls, email): """Return the user object whose email address is ``email``.""" @@ -313,4 +318,7 @@ class User(MappedClass): hash.update((password + self.password[:64]).encode('utf-8')) return self.password[64:] == hash.hexdigest() + def __eq__(self, other): + return self._id == other._id + {{endif}} diff --git a/devtools/gearbox/quickstart/template/+package+/tests/__init__.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/__init__.py_tmpl deleted file mode 100644 index 6d053ca..0000000 --- a/devtools/gearbox/quickstart/template/+package+/tests/__init__.py_tmpl +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -"""Unit and functional test suite for {{project}}.""" - -from os import getcwd -from paste.deploy import loadapp -from webtest import TestApp -from gearbox.commands.setup_app import SetupAppCommand -from tg import config -from tg.util import Bunch - -from {{package}} import model - -__all__ = ['setup_app', 'setup_db', 'teardown_db', 'TestController'] - -application_name = 'main_without_authn' - - -def load_app(name=application_name): - """Load the test application.""" - return TestApp(loadapp('config:test.ini#%s' % name, relative_to=getcwd())) - - -def setup_app(): - """Setup the application.""" - cmd = SetupAppCommand(Bunch(options=Bunch(verbose_level=1)), Bunch()) - cmd.run(Bunch(config_file='config:test.ini', section_name=None)) - - - -{{if ming}} -def setup_db(): - """Create the database schema (not needed when you run setup_app).""" - datastore = config['tg.app_globals'].ming_datastore - model.init_model(datastore) - - -def teardown_db(): - """Destroy the database schema.""" - datastore = config['tg.app_globals'].ming_datastore - try: - # On MIM drop all data - datastore.conn.drop_all() - except TypeError: - # On MongoDB drop database - datastore.conn.drop_database(datastore.db) -{{elif sqlalchemy}} -def setup_db(): - """Create the database schema (not needed when you run setup_app).""" - engine = config['tg.app_globals'].sa_engine - model.init_model(engine) - model.metadata.create_all(engine) - - -def teardown_db(): - """Destroy the database schema.""" - engine = config['tg.app_globals'].sa_engine - model.metadata.drop_all(engine) -{{endif}} - - -class TestController(object): - """Base functional test case for the controllers. - - The {{project}} application instance (``self.app``) set up in this test - case (and descendants) has authentication disabled, so that developers can - test the protected areas independently of the :mod:`repoze.who` plugins - used initially. This way, authentication can be tested once and separately. - - Check {{package}}.tests.functional.test_authentication for the repoze.who - integration tests. - - This is the officially supported way to test protected areas with - repoze.who-testutil (http://code.gustavonarea.net/repoze.who-testutil/). - - """ - application_under_test = application_name - - def setUp(self): - """Setup test fixture for each functional test method.""" - self.app = load_app(self.application_under_test) - setup_app() - - def tearDown(self): - """Tear down test fixture for each functional test method.""" -{{if sqlalchemy}} - model.DBSession.remove() -{{endif}} -{{if database}} - teardown_db() -{{else}} - pass -{{endif}} diff --git a/devtools/gearbox/quickstart/template/+package+/tests/_conftest/app.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/_conftest/app.py_tmpl new file mode 100644 index 0000000..9843717 --- /dev/null +++ b/devtools/gearbox/quickstart/template/+package+/tests/_conftest/app.py_tmpl @@ -0,0 +1,67 @@ +import pytest +from webtest import TestApp as WebTestApp # rename due to pytest warning +from paste.deploy import loadapp, appconfig +from tg import config +from {{package}} import websetup +{{if database}}from {{package}} import model{{endif}} +from os import getcwd +{{if sqlalchemy}}import transaction{{endif}} + + +{{if database}} +def teardown_db(): + {{if sqlalchemy}} + model.DBSession.remove() + engine = config['tg.app_globals'].sa_engine + model.metadata.drop_all(engine) + transaction.abort() + {{elif ming}} + datastore = config['tg.app_globals'].ming_datastore + model.DBSession.clear() # before dropping flush is performed + try: + # On MIM drop all data + datastore.conn.drop_all() + except TypeError: # pragma: no cover + # On MongoDB drop database + datastore.conn.drop_database(datastore.db) + {{endif}} +{{endif}} + + +@pytest.fixture(scope='function') +def _app(): + """This fixture allows you to reconfigure the application configuration. + Also, you can omit setup_app command""" + def __app(name='main_without_authn', reconfig=None, setup_app=True): + paste_config = 'config:test.ini#%s' % name + app = WebTestApp(loadapp( + paste_config, + relative_to=getcwd(), + global_conf=reconfig or {}, + )) + if setup_app: + _config = appconfig(paste_config, relative_to=getcwd(), global_conf=reconfig or {}) + websetup.setup_app(None, _config, {}) + return app + yield __app + {{if database}}teardown_db(){{endif}} + + +@pytest.fixture(scope='function') +def app(_app): + """This fixture is the default application""" + return _app() + + +{{if auth}} +@pytest.fixture() +def env(): + """this is just a shorthand intended to be passed to extra_environ in webtest calls, + remote_user got as parameter will go into REMOTE_USER key, + other kwarguments are included in the returned dictionary""" + def _environ(remote_user, **kwargs): + r = {'REMOTE_USER': remote_user} if remote_user else {} + r.update(kwargs) + return r + return _environ +{{endif}} \ No newline at end of file diff --git a/devtools/gearbox/quickstart/template/+package+/tests/_conftest/models.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/_conftest/models.py_tmpl new file mode 100644 index 0000000..96a6c15 --- /dev/null +++ b/devtools/gearbox/quickstart/template/+package+/tests/_conftest/models.py_tmpl @@ -0,0 +1,21 @@ +import pytest +from {{package}}.model import DBSession +from {{package}}.tests._conftest.app import teardown_db + + +@pytest.fixture() +def obj(): + def _obj(klass, attrs): + new_attrs = {} + new_attrs.update(attrs) + created = klass(**new_attrs) + {{if sqlalchemy}} + DBSession.add(created) + DBSession.flush() + {{elif ming}} + created.__mongometa__.session.flush() + created.__mongometa__.session.clear() + {{endif}} + return created + yield _obj + teardown_db() diff --git a/devtools/gearbox/quickstart/template/+package+/tests/conftest.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/conftest.py_tmpl new file mode 100644 index 0000000..8ea65af --- /dev/null +++ b/devtools/gearbox/quickstart/template/+package+/tests/conftest.py_tmpl @@ -0,0 +1,6 @@ +{{if database}}from {{package}}.tests._conftest.models import obj{{endif}} +from {{package}}.tests._conftest.app import app, _app{{if auth}}, env{{endif}} + +# Fixtures defined in _conftest came from turbogears2, change them if you wish + +# Place here your own fixtures. diff --git a/devtools/gearbox/quickstart/template/+package+/tests/functional/__init__.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/functional/__init__.py_tmpl deleted file mode 100644 index 70b6e02..0000000 --- a/devtools/gearbox/quickstart/template/+package+/tests/functional/__init__.py_tmpl +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -"""Functional test suite for the controllers of the application.""" diff --git a/devtools/gearbox/quickstart/template/+package+/tests/functional/test_authentication.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/functional/test_authentication.py_tmpl index 0763aee..c681499 100644 --- a/devtools/gearbox/quickstart/template/+package+/tests/functional/test_authentication.py_tmpl +++ b/devtools/gearbox/quickstart/template/+package+/tests/functional/test_authentication.py_tmpl @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -{{if auth}} """ Integration tests for the :mod:`repoze.who`-powered authentication sub-system. @@ -7,91 +5,93 @@ As {{project}} grows and the authentication method changes, only these tests should be updated. """ -from __future__ import unicode_literals -from nose.tools import eq_, ok_ - -from {{package}}.tests import TestController +{{if not skip_tw}} +def test_forced_login(_app): + """Anonymous users are forced to login + Test that anonymous users are automatically redirected to the login + form when authorization is denied. Next, upon successful login they + should be redirected to the initially requested page. -class TestAuthentication(TestController): """ - Tests for the default authentication setup. + app = _app('main') + # Requesting a protected area + resp = app.get('/admin/', status=302) + assert resp.location.startswith('http://localhost/login') + # Getting the login form: + resp = resp.follow(status=200) + form = resp.form + # Submitting the login form: + form['login'] = 'manager' + form['password'] = 'managepass' + post_login = form.submit(status=302) + # Being redirected to the initially requested page: + assert post_login.location.startswith('http://localhost/post_login') + initial_page = post_login.follow(status=302) + assert 'authtkt' in initial_page.request.cookies + assert initial_page.location.startswith('http://localhost/admin/') +{{endif}} - If your application changes how the authentication layer is configured - those tests should be updated accordingly - """ - application_under_test = 'main' +def test_voluntary_login(_app): + """Voluntary logins must work correctly""" + app = _app('main') + # Going to the login form voluntarily: + resp = app.get('/login', status=200) + form = resp.form + # Submitting the login form: + form['login'] = 'manager' + form['password'] = 'managepass' + post_login = form.submit(status=302) + # Being redirected to the home page: + assert post_login.location.startswith('http://localhost/post_login') + home_page = post_login.follow(status=302) + assert 'authtkt' in home_page.request.cookies + assert home_page.location == 'http://localhost/' -{{if not skip_tw}} - def test_forced_login(self): - """Anonymous users are forced to login - Test that anonymous users are automatically redirected to the login - form when authorization is denied. Next, upon successful login they - should be redirected to the initially requested page. +def test_logout(_app): + """Logouts must work correctly""" + # Logging in voluntarily the quick way: + app = _app('main') + resp = app.get('/login_handler?login=manager&password=managepass', + status=302) + resp = resp.follow(status=302) + assert 'authtkt' in resp.request.cookies + # Logging out: + resp = app.get('/logout_handler', status=302) + assert resp.location.startswith('http://localhost/post_logout') + # Finally, redirected to the home page: + home_page = resp.follow(status=302) + authtkt = home_page.request.cookies.get('authtkt') + assert not authtkt or authtkt == 'INVALID' + assert home_page.location == 'http://localhost/' - """ - # Requesting a protected area - resp = self.app.get('/admin/', status=302) - ok_(resp.location.startswith('http://localhost/login')) - # Getting the login form: - resp = resp.follow(status=200) - form = resp.form - # Submitting the login form: - form['login'] = 'manager' - form['password'] = 'managepass' - post_login = form.submit(status=302) - # Being redirected to the initially requested page: - ok_(post_login.location.startswith('http://localhost/post_login')) - initial_page = post_login.follow(status=302) - ok_('authtkt' in initial_page.request.cookies, - "Session cookie wasn't defined: %s" % initial_page.request.cookies) - ok_(initial_page.location.startswith('http://localhost/admin/'), - initial_page.location) -{{endif}} - def test_voluntary_login(self): - """Voluntary logins must work correctly""" - # Going to the login form voluntarily: - resp = self.app.get('/login', status=200) - form = resp.form - # Submitting the login form: - form['login'] = 'manager' - form['password'] = 'managepass' - post_login = form.submit(status=302) - # Being redirected to the home page: - ok_(post_login.location.startswith('http://localhost/post_login')) - home_page = post_login.follow(status=302) - ok_('authtkt' in home_page.request.cookies, - 'Session cookie was not defined: %s' % home_page.request.cookies) - eq_(home_page.location, 'http://localhost/') +def test_failed_login_keeps_username(_app): + """Wrong password keeps user_name in login form""" + app = _app('main') + resp = app.get('/login_handler?login=manager&password=badpassword', + status=302) + resp = resp.follow(status=200) + assert 'Invalid Password' in resp + assert resp.form['login'].value, 'manager' - def test_logout(self): - """Logouts must work correctly""" - # Logging in voluntarily the quick way: - resp = self.app.get('/login_handler?login=manager&password=managepass', - status=302) - resp = resp.follow(status=302) - ok_('authtkt' in resp.request.cookies, - 'Session cookie was not defined: %s' % resp.request.cookies) - # Logging out: - resp = self.app.get('/logout_handler', status=302) - ok_(resp.location.startswith('http://localhost/post_logout')) - # Finally, redirected to the home page: - home_page = resp.follow(status=302) - authtkt = home_page.request.cookies.get('authtkt') - ok_(not authtkt or authtkt == 'INVALID', - 'Session cookie was not deleted: %s' % home_page.request.cookies) - eq_(home_page.location, 'http://localhost/') - def test_failed_login_keeps_username(self): - """Wrong password keeps user_name in login form""" - resp = self.app.get('/login_handler?login=manager&password=badpassword', - status=302) - resp = resp.follow(status=200) - ok_('Invalid Password' in resp, resp) - eq_(resp.form['login'].value, 'manager') +def test_failed_invalid_name(_app): + """before verifying password the username should be checked""" + app = _app('main') + resp = app.get('/login_handler?login=baduser&password=', + status=302) + resp = resp.follow(status=200) + assert resp.form['login'].value, 'baduser' + assert 'User not found' in resp -{{endif}} + +def test_calling_post_login_directly(_app): + app = _app('main') + resp = app.get('/post_login', status=302) + assert '__logins=1' in resp.location + resp = resp.follow() + assert 'Wrong credentials' in resp.body.decode('utf-8') \ No newline at end of file diff --git a/devtools/gearbox/quickstart/template/+package+/tests/functional/test_root.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/functional/test_root.py_tmpl index d78c628..eaf7bab 100644 --- a/devtools/gearbox/quickstart/template/+package+/tests/functional/test_root.py_tmpl +++ b/devtools/gearbox/quickstart/template/+package+/tests/functional/test_root.py_tmpl @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Functional test suite for the root controller. @@ -10,68 +9,73 @@ functional tests exercise the whole application and its WSGI stack. Please read https://webtest.readthedocs.io/ for more information. """ +import pytest -from nose.tools import ok_ -from {{package}}.tests import TestController +def test_index(app): + """The front page is working properly""" + response = app.get('/') + msg = 'TurboGears 2 is rapid web application development toolkit '\ + 'designed to make your life easier.' + # You can look for specific strings: + assert msg in response + # You can also access a BeautifulSoup'ed response in your tests + # (First run $ pip install BeautifulSoup + # and then uncomment the next two lines) -class TestRootController(TestController): - """Tests for the method in the root controller.""" + # links = response.html.findAll('a') + # print(links) + # ok_(links, "Mummy, there are no links here!") + # ## otherwise give pyquery a try - def test_index(self): - """The front page is working properly""" - response = self.app.get('/') - msg = 'TurboGears 2 is rapid web application development toolkit '\ - 'designed to make your life easier.' - # You can look for specific strings: - ok_(msg in response) - # You can also access a BeautifulSoup'ed response in your tests - # (First run $ easy_install BeautifulSoup - # and then uncomment the next two lines) +{{if not minimal_quickstart}} +def test_environ(app): + """Displaying the wsgi environ works""" + response = app.get('/environ.html') + assert 'The keys in the environment are:' in response.body.decode('utf-8') - # links = response.html.findAll('a') - # print(links) - # ok_(links, "Mummy, there are no links here!") -{{if not minimal_quickstart}} - def test_environ(self): - """Displaying the wsgi environ works""" - response = self.app.get('/environ.html') - ok_('The keys in the environment are:' in response) - - def test_data(self): - """The data display demo works with HTML""" - response = self.app.get('/data.html?a=1&b=2') - response.mustcontain("a", "1", - "b", "2") - - def test_data_json(self): - """The data display demo works with JSON""" - resp = self.app.get('/data.json?a=1&b=2') - ok_( - dict(page='data', params={'a': '1', 'b': '2'}) == resp.json, - resp.json - ) - -{{if auth}} - def test_secc_with_manager(self): - """The manager can access the secure controller""" - # Note how authentication is forged: - environ = {'REMOTE_USER': 'manager'} - resp = self.app.get('/secc', extra_environ=environ, status=200) - ok_('Secure Controller here' in resp.text, resp.text) - - def test_secc_with_editor(self): - """The editor cannot access the secure controller""" - environ = {'REMOTE_USER': 'editor'} - self.app.get('/secc', extra_environ=environ, status=403) - # It's enough to know that authorization was denied with a 403 status - - def test_secc_with_anonymous(self): - """Anonymous users must not access the secure controller""" - self.app.get('/secc', status=401) - # It's enough to know that authorization was denied with a 401 status +def test_data(app): + """The data display demo works with HTML""" + response = app.get('/data.html?a=1&b=2') + response.mustcontain("a", "1", + "b", "2") + + +def test_data_json(app): + """The data display demo works with JSON""" + resp = app.get('/data.json?a=1&b=2') + assert dict(page='data', params={'a': '1', 'b': '2'}) == resp.json + + +{{if auth == "sqlalchemy" or auth == "ming"}} +@pytest.mark.parametrize('url,user,status', ( + ('/secc', None, 401), + ('/secc', 'manager', 200), + ('/secc', 'editor', 403), + ('/secc/some_where', 'manager', 200), + ('/secc/some_where', 'editor', 403), +)) +def test_secc_access(app, env, url, user, status): + app.get(url, extra_environ=env(user), status=status) + {{endif}} {{endif}} + +{{if auth == "sqlalchemy" or auth == "ming"}} +@pytest.mark.parametrize('url,user,status', ( + {{if not minimal_quickstart}}('/about', None, 200),{{endif}} + ('/manage_permission_only', 'manager', 200), + ('/manage_permission_only', 'editor', 403), + ('/editor_user_only', 'manager', 403), + ('/editor_user_only', 'editor', 200), +)) +def test_page_access(app, env, url, user, status): + app.get(url, extra_environ=env(user), status=status) +{{endif}} + +def test_error_document_500(app): + resp = app.get('/error/document', expect_errors=True) + assert "We're sorry but we weren't able to process this request" in resp diff --git a/devtools/gearbox/quickstart/template/+package+/tests/models/__init__.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/models/__init__.py_tmpl deleted file mode 100644 index 62bba39..0000000 --- a/devtools/gearbox/quickstart/template/+package+/tests/models/__init__.py_tmpl +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -"""Unit test suite for the models of the application.""" - -from nose.tools import eq_ - -{{if sqlalchemy}} -from {{package}}.model import DBSession -{{elif ming}} -from tg import config -{{endif}} -from {{package}}.tests import load_app -{{if database}} -from {{package}}.tests import setup_db, teardown_db -{{endif}} - -__all__ = ['ModelTest'] - - -def setup(): - """Setup test fixture for all model tests.""" - load_app() -{{if database}} - setup_db() -{{endif}} - - -def teardown(): - """Tear down test fixture for all model tests.""" -{{if database}} - teardown_db() -{{else}} - pass -{{endif}} - - -class ModelTest(object): - """Base unit test case for the models.""" - - klass = None - attrs = {} - - def setUp(self): - """Setup test fixture for each model test method.""" - try: - new_attrs = {} - new_attrs.update(self.attrs) - new_attrs.update(self.do_get_dependencies()) - self.obj = self.klass(**new_attrs) - {{if sqlalchemy}} - DBSession.add(self.obj) - DBSession.flush() - {{elif ming}} - self.obj.__mongometa__.session.flush() - self.obj.__mongometa__.session.clear() - {{endif}} - return self.obj - except: - {{if ming}} - datastore = config['tg.app_globals'].ming_datastore - try: - # On MIM drop all data - datastore.conn.clear_all() - except: - # On MongoDB drop database - datastore.conn.drop_database(datastore.db) - {{elif sqlalchemy}} - DBSession.rollback() - {{endif}} - raise - - def tearDown(self): - """Tear down test fixture for each model test method.""" - {{if ming}} - datastore = config['tg.app_globals'].ming_datastore - try: - # On MIM drop all data - datastore.conn.clear_all() - except: - # On MongoDB drop database - datastore.conn.drop_database(datastore.db) - {{elif sqlalchemy}} - DBSession.rollback() - {{endif}} - - def do_get_dependencies(self): - """Get model test dependencies. - - Use this method to pull in other objects that need to be created - for this object to be build properly. - - """ - return {} - - def test_create_obj(self): - """Model objects can be created""" - pass - - def test_query_obj(self): - """Model objects can be queried""" - {{if ming}} - obj = self.klass.query.find({}).first() - {{elif sqlalchemy}} - obj = DBSession.query(self.klass).one() - {{endif}} - for key, value in self.attrs.items(): - eq_(getattr(obj, key), value) diff --git a/devtools/gearbox/quickstart/template/+package+/tests/models/test_auth.py_tmpl b/devtools/gearbox/quickstart/template/+package+/tests/models/test_auth.py_tmpl index 24ebfdf..2ca4d56 100644 --- a/devtools/gearbox/quickstart/template/+package+/tests/models/test_auth.py_tmpl +++ b/devtools/gearbox/quickstart/template/+package+/tests/models/test_auth.py_tmpl @@ -1,62 +1,101 @@ -# -*- coding: utf-8 -*- -"""Test suite for the TG app's models""" -from __future__ import unicode_literals -from nose.tools import eq_ - +import pytest from {{package}} import model -from {{package}}.tests.models import ModelTest - {{if auth}} -class TestGroup(ModelTest): - """Unit test case for the ``Group`` model.""" - - klass = model.Group - attrs = dict( - group_name="test_group", - display_name="Test Group" - ) - - -class TestUser(ModelTest): - """Unit test case for the ``User`` model.""" - - klass = model.User - attrs = dict( - user_name="ignucius", - email_address="ignucius@example.org" - ) - - def test_obj_creation_username(self): - """The obj constructor must set the user name right""" - eq_(self.obj.user_name, "ignucius") - - def test_obj_creation_email(self): - """The obj constructor must set the email right""" - eq_(self.obj.email_address, "ignucius@example.org") - - def test_no_permissions_by_default(self): - """User objects should have no permission by default.""" - eq_(len(self.obj.permissions), 0) - - def test_getting_by_email(self): - """Users should be fetcheable by their email addresses""" - him = model.User.by_email_address("ignucius@example.org") - {{if auth == "ming"}} - eq_(him._id, self.obj._id) - {{elif auth == "sqlalchemy"}} - eq_(him, self.obj) - {{endif}} - - -class TestPermission(ModelTest): - """Unit test case for the ``Permission`` model.""" +default_user = (model.User, dict( + user_name="ignucius", + email_address="ignucius@example.org", +)) + +default_permission = (model.Permission, dict( + permission_name="test_permission", + description="This is a test Description", +)) + +default_group = (model.Group, dict( + group_name="test_group", + display_name="Test Group", +)) + + +@pytest.mark.parametrize('klass, attrs', ( + default_user, + default_permission, + default_group, +)) +def test_create_obj(app, klass, attrs, obj): + """This test ensures auth objects can be created""" + obj = obj(klass, attrs) + assert isinstance(obj, klass) + for k, v in attrs.items(): + if isinstance(v, (str, int, float, bool)): + assert v == obj.__getattribute__(k) + +@pytest.mark.parametrize('klass, attrs', ( + default_user, + default_permission, + default_group, +)) +def test_repr_and_str_obj(app, klass, attrs, obj): + obj = obj(klass, attrs) + {{if sqlalchemy}} + assert repr(obj).startswith('<%s: ' % klass.__name__) + {{elif ming}} + assert repr(obj).startswith('<%s ' % klass.__name__) + {{endif}} + assert str(obj) + + +def test_query_user_by_mail(app, obj): + """tests by_email_address works properly""" + obj = obj(*default_user) + assert model.User.by_email_address("ignucius@example.org") == obj + + +def test_query_user_by_username(app, obj): + """tests by_email_address works properly""" + obj = obj(*default_user) + assert model.User.by_user_name("ignucius") == obj + + +def test_assert_no_permission_by_default(app, obj): + """User objects should have no permission by default.""" + obj = obj(*default_user) + assert len(obj.permissions) == 0 + + +def test_assign_permission_to_user(app, obj): + """it should be possible to assign a permission to a user""" + u = obj(*default_user) + p = obj(*default_permission) + g = obj(*default_group) + {{if sqlalchemy}} + g.permissions.append(p) + g.users.append(u) + {{elif ming}} + p.groups = [g] + u.groups = [g] + model.DBSession.save(p) + model.DBSession.save(g) + model.DBSession.save(u) + model.DBSession.flush() + u = model.DBSession.refresh(u) + {{endif}} + assert len(u.groups) == 1 + assert len(u.permissions) == 1 +{{endif}} - klass = model.Permission - attrs = dict( - permission_name="test_permission", - description="This is a test Description" - ) -{{endif}} +def test_bootstap_again(app, capsys, obj): + from {{package}}.websetup import bootstrap + u = obj(*default_user) + bootstrap(None, {}, {}) + assert 'there was a problem adding your auth data' in capsys.readouterr().out + {{if sqlalchemy}} + # the user just created should be rolled back + assert model.User.by_email_address("ignucius@example.org") is None + {{elif ming}} + # the user should be there anyway + assert model.User.by_email_address("ignucius@example.org") is not None + {{endif}} \ No newline at end of file diff --git a/devtools/gearbox/quickstart/template/+package+/websetup/bootstrap.py_tmpl b/devtools/gearbox/quickstart/template/+package+/websetup/bootstrap.py_tmpl index 37d86ec..3d8272d 100644 --- a/devtools/gearbox/quickstart/template/+package+/websetup/bootstrap.py_tmpl +++ b/devtools/gearbox/quickstart/template/+package+/websetup/bootstrap.py_tmpl @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- """Setup the {{project}} application""" from __future__ import print_function, unicode_literals +{{if auth == 'sqlalchemy'}}import transaction{{endif}} -{{if sqlalchemy}} -import transaction -{{endif}} from {{package}} import model @@ -55,30 +53,37 @@ def bootstrap(command, conf, vars): transaction.abort() print('Continuing with bootstrapping...') {{elif auth == "ming"}} - g = model.Group() - g.group_name = 'managers' - g.display_name = 'Managers Group' - - p = model.Permission() - p.permission_name = 'manage' - p.description = 'This permission gives an administrative right' - p.groups = [g] - - u = model.User() - u.user_name = 'manager' - u.display_name = 'Example manager' - u.email_address = 'manager@somedomain.com' - u.groups = [g] - u.password = 'managepass' - - u1 = model.User() - u1.user_name = 'editor' - u1.display_name = 'Example editor' - u1.email_address = 'editor@somedomain.com' - u1.password = 'editpass' - - model.DBSession.flush() - model.DBSession.clear() - {{endif}} + from pymongo.errors import DuplicateKeyError + try: + g = model.Group() + g.group_name = 'managers' + g.display_name = 'Managers Group' + + p = model.Permission() + p.permission_name = 'manage' + p.description = 'This permission gives an administrative right' + p.groups = [g] + + u = model.User() + u.user_name = 'manager' + u.display_name = 'Example manager' + u.email_address = 'manager@somedomain.com' + u.groups = [g] + u.password = 'managepass' + + u1 = model.User() + u1.user_name = 'editor' + u1.display_name = 'Example editor' + u1.email_address = 'editor@somedomain.com' + u1.password = 'editpass' + model.DBSession.flush() + model.DBSession.clear() + except DuplicateKeyError as e: + print('Warning, there was a problem adding your auth data, ' + 'it may have already been added:') + import traceback + print(traceback.format_exc()) + print('Continuing with bootstrapping...') + {{endif}} # diff --git a/devtools/gearbox/quickstart/template/setup.cfg_tmpl b/devtools/gearbox/quickstart/template/setup.cfg_tmpl index 18c6c53..ef83839 100755 --- a/devtools/gearbox/quickstart/template/setup.cfg_tmpl +++ b/devtools/gearbox/quickstart/template/setup.cfg_tmpl @@ -1,9 +1,5 @@ -[nosetests] -verbosity = 2 -detailed-errors = 1 -with-coverage = false -cover-erase = true -cover-package = {{package}} +[tool:pytest] +addopts = --cov=. # Babel configuration [compile_catalog] diff --git a/devtools/gearbox/quickstart/template/setup.py_tmpl b/devtools/gearbox/quickstart/template/setup.py_tmpl index e275914..d2ea3d7 100755 --- a/devtools/gearbox/quickstart/template/setup.py_tmpl +++ b/devtools/gearbox/quickstart/template/setup.py_tmpl @@ -28,7 +28,9 @@ except ImportError: testpkgs = [ 'WebTest >= 1.2.3', - 'nose', + 'pytest', + 'pytest-cov', + 'pytest-randomly', 'coverage', 'gearbox' ] @@ -46,6 +48,7 @@ install_requires = [ "Mako", {{endif}} {{if sqlalchemy}} + "transaction", "zope.sqlalchemy >= 1.2", "sqlalchemy", {{endif}} @@ -91,7 +94,6 @@ setup( packages=find_packages(exclude=['ez_setup']), install_requires=install_requires, include_package_data=True, - test_suite='nose.collector', tests_require=testpkgs, extras_require={ 'testing': testpkgs diff --git a/devtools/tests/test_quickstart.py b/devtools/tests/test_quickstart.py index 46df7d2..12a6858 100644 --- a/devtools/tests/test_quickstart.py +++ b/devtools/tests/test_quickstart.py @@ -4,6 +4,7 @@ import sys import site import pkg_resources +import re from nose.tools import ok_ from webtest import TestApp @@ -25,28 +26,48 @@ QUIET = '-q' # Set this to -v to enable installed packages logging, or to -q to disable it -def get_passed_and_failed(env_cmd, python_cmd, testpath): +def run_pytest(env_cmd, testpath): """Run test suite under testpath, return set of passed tests.""" + result = {'errors': 0, 'failed': 0, 'out': b'', 'coverage': {'passed': 0, 'missing': 0, 'cover': 0}} os.chdir(testpath) - args = '. %s; %s -W ignore setup.py test' % (env_cmd, python_cmd) - out = subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True).communicate()[1] - passed, failed = [], [] - test = None - lines = out.splitlines() + args = '. %s; pytest' % env_cmd + print('running tests %s' % testpath) + out = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + ).communicate()[0] + result['out'] = out = out.decode('utf-8') + assert out[0] # faster than calculating len of out that should be > 0 + # print('pytest output:') + # print(out) + + cov_pattern = re.compile(r'TOTAL\W*(\d*)\W*(\d*)\W*(\d*)') + for line in out.splitlines()[::-1]: + match = cov_pattern.match(line) + if match: + break + else: + raise Exception('no coverage output found. output: %s' % out) + result['coverage']['line'] = match.group(0) + result['coverage']['passed'] = match.group(1) + result['coverage']['missing'] = match.group(2) + result['coverage']['cover'] = match.group(3) + + # detecting number of errors and failed tests from short summary + try: + summary = out[out.index('short test summary info'):] + except ValueError: + summary = '' + lines = summary.splitlines() for line in lines: - line = line.decode('utf-8').split(' ... ', 1) - if line[0].startswith('tgtest'): - test = line[0] - if test and len(line) == 2: - if line[1] in ('ok', 'OK'): - passed.append(test) - test = None - elif line[1] in ('ERROR', 'FAIL'): - failed.append(test) - test = None - return passed, failed, lines + if 'ERROR' in line: + result['errors'] += 1 + elif 'FAIL' in line: + result['failed'] += 1 + + return result class BaseTestQuickStart(object): @@ -74,6 +95,8 @@ def setUpClass(cls): shutil.rmtree(cls.env_dir, ignore_errors=True) # Create virtualenv for current fixture + print('creating virtual environment in %s' % cls.env_dir) + # if you get errors about setuptools, maybe you made an error in command.py create_environment(cls.env_dir) # Enable the newly created virtualenv @@ -89,7 +112,8 @@ def setUpClass(cls): # we run them with python setup.py test that is unable # download dependencies on systems without TLS1.2 support. subprocess.call([cls.pip_cmd, QUIET, 'install', '--pre', '-I', 'coverage']) - subprocess.call([cls.pip_cmd, QUIET, 'install', '--pre', '-I', 'nose']) + subprocess.call([cls.pip_cmd, QUIET, 'install', '--pre', '-I', 'pytest']) + subprocess.call([cls.pip_cmd, QUIET, 'install', '--pre', '-I', 'pytest-cov']) subprocess.call([cls.pip_cmd, QUIET, 'install', '--pre', '-I', 'webtest']) # Then install specific requirements @@ -106,7 +130,7 @@ def setUpClass(cls): subprocess.call([cls.pip_cmd, QUIET, 'install', '--pre', '-I', 'git+git://github.com/TurboGears/tg2.git@development']) # Install tg.devtools inside the virtualenv - subprocess.call([cls.pip_cmd, QUIET, 'install', '--pre', '-e', cls.base_dir]) + subprocess.call([cls.pip_cmd, QUIET, 'install', '--pre', '-e', cls.base_dir + '[testing]']) # Install All Template Engines inside the virtualenv so that # They all get configured as we share a single python process @@ -136,21 +160,6 @@ def setUpClass(cls): pkg_resources.working_set.add_entry(site_packages) pkg_resources.working_set.add_entry(cls.proj_dir) - def setUp(self): - os.chdir(self.proj_dir) - from paste.deploy import loadapp - self.app = loadapp('config:test.ini', relative_to=self.proj_dir) - self.app = TestApp(self.app) - - def init_database(self): - os.chdir(self.proj_dir) - cmd = SetupAppCommand(Bunch(options=Bunch(verbose_level=1)), Bunch()) - try: - cmd.run(Bunch(config_file='config:test.ini', section_name=None)) - except: - # DB already initialised, ignore it. - pass - @classmethod def tearDownClass(cls): # This is in case the tests have been skipped @@ -215,219 +224,69 @@ def venv_uninstall(cls, package): class CommonTestQuickStart(BaseTestQuickStart): - - # tests that must be passed - pass_tests = [ - '.tests.functional.test_authentication.', - '.tests.functional.test_root.', - '.tests.models.test_auth.'] - # tests that must fail (should not exist) - fail_tests = [] - # tests that must not be run - skip_tests = [] - - def test_index(self): - resp = self.app.get('/') - ok_('Welcome to TurboGears' in resp, resp) - - def test_login(self): - resp = self.app.get('/login') - ok_('

Login

' in resp) - - def test_unauthenticated_admin(self): - ok_('

Login

' - in self.app.get('/admin/', status=302).follow()) - - def test_subtests(self): - passed, failed, lines = get_passed_and_failed(self.env_cmd, - self.python_cmd, - os.path.join(self.proj_dir)) - for has_failed in failed: - for must_fail in self.fail_tests: - if must_fail in has_failed: - break - else: - ok_(False, 'Failed %s\n\n%s' % (has_failed, lines)) - for must_pass in self.pass_tests: - for has_passed in passed: - if must_pass in has_passed: - break - else: - print("Passed:\n" + '\n'.join(passed)) - ok_(False, 'Did not pass %s\n\n%s' % (must_pass, lines)) - for must_fail in self.fail_tests: - for has_failed in failed: - if must_fail in has_failed: - break - else: - print("Failed:\n" + '\n'.join(failed)) - ok_(False, 'Did not fail %s' % must_fail) - for must_skip in self.skip_tests: - for has_run in passed + failed: - if must_skip in has_run: - print("Run:\n" + '\n'.join(passed + failed)) - ok_(False, 'Did not skip %s' % must_skip) - - -class CommonTestQuickStartWithAuth(CommonTestQuickStart): - def test_unauthenticated_admin_with_prefix(self): - resp1 = self.app.get('/prefix/admin/', extra_environ={'SCRIPT_NAME': '/prefix'}, status=302) - ok_(resp1.headers['Location'] == 'http://localhost/prefix/login?came_from=%2Fprefix%2Fadmin%2F', - resp1.headers['Location']) - resp2 = resp1.follow(extra_environ={'SCRIPT_NAME': '/prefix'}) - ok_('/prefix/login_handler' in resp2, resp2) - - def test_login_with_prefix(self): - self.init_database() - resp1 = self.app.post('/prefix/login_handler?came_from=%2Fprefix%2Fadmin%2F', - params={'login': 'editor', 'password': 'editpass'}, - extra_environ={'SCRIPT_NAME': '/prefix'}) - ok_(resp1.headers['Location'] == 'http://localhost/prefix/post_login?came_from=%2Fprefix%2Fadmin%2F', - resp1.headers['Location']) - resp2 = resp1.follow(extra_environ={'SCRIPT_NAME': '/prefix'}) - ok_(resp2.headers['Location'] == 'http://localhost/prefix/admin/', - resp2.headers['Location']) - - def test_login_failure_with_prefix(self): - self.init_database() - resp = self.app.post('/prefix/login_handler?came_from=%2Fprefix%2Fadmin%2F', - params={'login': 'WRONG', 'password': 'WRONG'}, - extra_environ={'SCRIPT_NAME': '/prefix'}) - location = resp.headers['Location'] - ok_('http://localhost/prefix/login' in location, location) - ok_('came_from=%2Fprefix%2Fadmin%2F' in location, location) - - -class TestDefaultQuickStart(CommonTestQuickStartWithAuth): + def test_quickstarted_tests(self): + result = run_pytest( + self.env_cmd, os.path.join(self.proj_dir) + ) + if result['failed'] + result['errors'] != 0: + print(result['out']) + assert False, (result['failed'], result['errors']) + if result['coverage']['passed'] == 0: + assert False, result['coverage'] + if result['coverage']['missing'] == 0: + assert False, result['coverage'] + if result['coverage']['cover'] == 100: + assert False, result['coverage'] + + +class TestDefaultQuickStart(CommonTestQuickStart): args = '' - @classmethod - def setUpClass(cls): - super(TestDefaultQuickStart, cls).setUpClass() - - def setUp(self): - super(TestDefaultQuickStart, self).setUp() - class TestMakoQuickStart(CommonTestQuickStart): args = '--mako --nosa --noauth --skip-tw' - pass_tests = ['.tests.functional.test_root.'] - skip_tests = [ - '.tests.functional.test_root.test_secc', - '.tests.functional.test_authentication.', - '.tests.models.test_auth.'] - - def test_login(self): - self.app.get('/login', status=404) - - def test_unauthenticated_admin(self): - self.app.get('/admin', status=404) - class TestGenshiQuickStart(CommonTestQuickStart): args = '--genshi --nosa --noauth --skip-tw' - pass_tests = ['.tests.functional.test_root.'] - skip_tests = [ - '.tests.functional.test_root.test_secc', - '.tests.functional.test_authentication.', - '.tests.models.test_auth.'] - - @classmethod - def setUpClass(cls): - if PY_VERSION >= (3, 5): - raise SkipTest('Genshi not supported on Python 3.5') - super(TestGenshiQuickStart, cls).setUpClass() - - def test_login(self): - self.app.get('/login', status=404) - - def test_unauthenticated_admin(self): - self.app.get('/admin', status=404) - - class TestJinjaQuickStart(CommonTestQuickStart): args = '--jinja --nosa --noauth --skip-tw' - pass_tests = ['.tests.functional.test_root.'] - skip_tests = [ - '.tests.functional.test_root.test_secc', - '.tests.functional.test_authentication.', - '.tests.models.test_auth.'] - - def test_login(self): - self.app.get('/login', status=404) - - def test_unauthenticated_admin(self): - self.app.get('/admin', status=404) - class TestNoDBQuickStart(CommonTestQuickStart): - - pass_tests = ['.tests.functional.test_root.'] - skip_tests = [ - '.tests.functional.test_root.test_secc', - '.tests.functional.test_authentication.', - '.tests.models.test_auth.'] - args = '--nosa --noauth --skip-tw' - def test_login(self): - self.app.get('/login', status=404) - - def test_unauthenticated_admin(self): - self.app.get('/admin', status=404) - class TestNoAuthQuickStart(CommonTestQuickStart): - - pass_tests = ['.tests.functional.test_root.'] - skip_tests = [ - '.tests.functional.test_root.test_secc', - '.tests.functional.test_authentication.', - '.tests.models.test_auth.'] - args = '--noauth' - @classmethod - def setUpClass(cls): - super(TestNoAuthQuickStart, cls).setUpClass() - - def setUp(self): - super(TestNoAuthQuickStart, self).setUp() - - def test_login(self): - self.app.get('/login', status=404) - - def test_unauthenticated_admin(self): - self.app.get('/admin', status=404) - - -class TestMingBQuickStart(CommonTestQuickStartWithAuth): +class TestMingBQuickStart(CommonTestQuickStart): args = '--ming' - # preinstall = ['Paste', 'PasteScript'] # Ming doesn't require those anymore - - @classmethod - def setUpClass(cls): - super(TestMingBQuickStart, cls).setUpClass() - - def setUp(self): - super(TestMingBQuickStart, self).setUp() class TestNoTWQuickStart(CommonTestQuickStart): - args = '--skip-tw' - def test_unauthenticated_admin(self): - self.app.get('/admin', status=404) - class TestMinimalQuickStart(CommonTestQuickStart): - args = '--minimal-quickstart' - def test_secc_is_removed(self): - self.app.get('/secc', status=404) +class TestGitQuickStart(CommonTestQuickStart): + args = '--enable-git' + + def test_git_present(self): + assert os.path.isdir('.git') + assert os.path.isfile('.gitignore') + assert not os.path.isdir('.hg') + assert not os.path.isfile('.hgignore') + +class TestMercurialQuickStart(CommonTestQuickStart): + args = '--enable-mercurial' + + def test_hg_present(self): + assert not os.path.isdir('.git') + assert not os.path.isfile('.gitignore') + assert os.path.isdir('.hg') + assert os.path.isfile('.hgignore')