diff --git a/dev-requirements.txt b/dev-requirements.txt index 8795457f..4c93840f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ zest.releaser tox pytest +Flask-Testing diff --git a/ihatemoney/default_settings.py b/ihatemoney/default_settings.py index 15fe9cdd..fc4d3678 100644 --- a/ihatemoney/default_settings.py +++ b/ihatemoney/default_settings.py @@ -1,5 +1,5 @@ DEBUG = False -SQLALCHEMY_DATABASE_URI = 'sqlite:///budget.db' +SQLALCHEMY_DATABASE_URI = 'sqlite://' SQLACHEMY_ECHO = DEBUG # Will likely become the default value in flask-sqlalchemy >=3 ; could be removed # then: diff --git a/ihatemoney/run.py b/ihatemoney/run.py index 4bc1bb07..d9c6dbdd 100644 --- a/ihatemoney/run.py +++ b/ihatemoney/run.py @@ -10,7 +10,7 @@ from raven.contrib.flask import Sentry from ihatemoney.api import api from ihatemoney.models import db -from ihatemoney.utils import PrefixedWSGI +from ihatemoney.utils import PrefixedWSGI, minimal_round from ihatemoney.web import main as web_interface from ihatemoney import default_settings @@ -43,22 +43,23 @@ def setup_database(app): upgrade(migrations_path) -def load_configuration(app): - """ A way to (re)configure the app, specially reset the settings - """ - default_config_file = os.path.join(app.root_path, 'default_settings.py') - config_file = os.environ.get('IHATEMONEY_SETTINGS_FILE_PATH') +def load_configuration(app, configuration=None): + """ Find the right configuration file for the application and load it. - # Load default settings first - # Then load the settings from the path set in IHATEMONEY_SETTINGS_FILE_PATH var - # If not set, default to /etc/ihatemoney/ihatemoney.cfg - # If the latter doesn't exist no error is raised and the default settings are used - app.config.from_pyfile(default_config_file) - if config_file: - app.config.from_pyfile(config_file) + By order of preference: + - Use the IHATEMONEY_SETTINGS_FILE_PATH env var if defined ; + - If not, use /etc/ihatemoney/ihatemoney.cfg ; + - Otherwise, load the default settings. + """ + + env_var_config = os.environ.get('IHATEMONEY_SETTINGS_FILE_PATH') + app.config.from_object('ihatemoney.default_settings') + if configuration: + app.config.from_object(configuration) + elif env_var_config: + app.config.from_pyfile(env_var_config) else: app.config.from_pyfile('ihatemoney.cfg', silent=True) - app.wsgi_app = PrefixedWSGI(app) def validate_configuration(app): @@ -92,10 +93,17 @@ def validate_configuration(app): ) -def create_app(instance_path='/etc/ihatemoney'): - app = Flask(__name__, instance_path=instance_path, - instance_relative_config=True) - load_configuration(app) +def create_app(configuration=None, instance_path='/etc/ihatemoney', + instance_relative_config=True): + app = Flask( + __name__, + instance_path=instance_path, + instance_relative_config=instance_relative_config) + + # If a configuration object is passed, use it. Otherwise try to find one. + load_configuration(app, configuration) + app.wsgi_app = PrefixedWSGI(app) + validate_configuration(app) app.register_blueprint(web_interface) app.register_blueprint(api) @@ -110,6 +118,9 @@ def create_app(instance_path='/etc/ihatemoney'): # Error reporting Sentry(app) + # Jinja filters + app.jinja_env.filters['minimal_round'] = minimal_round + # Translations babel = Babel(app) diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py index afd6af41..331a411d 100644 --- a/ihatemoney/tests/tests.py +++ b/ihatemoney/tests/tests.py @@ -1,4 +1,4 @@ - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from __future__ import unicode_literals try: import unittest2 as unittest @@ -12,139 +12,118 @@ import six from werkzeug.security import generate_password_hash from flask import session +from flask_testing import TestCase + +from ihatemoney.run import create_app, db +from ihatemoney import models +from ihatemoney import utils # Unset configuration file env var if previously set if 'IHATEMONEY_SETTINGS_FILE_PATH' in os.environ: del os.environ['IHATEMONEY_SETTINGS_FILE_PATH'] -from ihatemoney import run -from ihatemoney import models -from ihatemoney import utils - __HERE__ = os.path.dirname(os.path.abspath(__file__)) -class TestCase(unittest.TestCase): +class BaseTestCase(TestCase): + + def create_app(self): + # Pass the test object as a configuration. + return create_app(self) def setUp(self): - run.app.config['TESTING'] = True - - run.app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///memory" - run.app.config['WTF_CSRF_ENABLED'] = False # simplify the tests - self.app = run.app.test_client() - try: - models.db.init_app(run.app) - run.mail.init_app(run.app) - except: - pass - - models.db.app = run.app - models.db.create_all() + db.create_all() def tearDown(self): # clean after testing - models.db.session.remove() - models.db.drop_all() - # reconfigure app with default settings - run.configure() + db.session.remove() + db.drop_all() def login(self, project, password=None, test_client=None): password = password or project - return self.app.post('/authenticate', data=dict( + return self.client.post('/authenticate', data=dict( id=project, password=password), follow_redirects=True) def post_project(self, name): """Create a fake project""" # create the project - self.app.post("/create", data={ - 'name': name, - 'id': name, - 'password': name, - 'contact_email': '%s@notmyidea.org' % name + self.client.post("/create", data={ + 'name': name, + 'id': name, + 'password': name, + 'contact_email': '%s@notmyidea.org' % name }) def create_project(self, name): - models.db.session.add(models.Project(id=name, name=six.text_type(name), - password=name, contact_email="%s@notmyidea.org" % name)) + project = models.Project( + id=name, + name=six.text_type(name), + password=name, + contact_email="%s@notmyidea.org" % name) + models.db.session.add(project) models.db.session.commit() -class BudgetTestCase(TestCase): +class IhatemoneyTestCase(BaseTestCase): + SQLALCHEMY_DATABASE_URI = "sqlite://" + TESTING = True + WTF_CSRF_ENABLED = False # Simplifies the tests. + +class DefaultConfigurationTestCase(BaseTestCase): def test_default_configuration(self): """Test that default settings are loaded when no other configuration file is specified""" - run.configure() - self.assertFalse(run.app.config['DEBUG']) - self.assertEqual(run.app.config['SQLALCHEMY_DATABASE_URI'], 'sqlite:///budget.db') - self.assertFalse(run.app.config['SQLALCHEMY_TRACK_MODIFICATIONS']) - self.assertEqual(run.app.config['SECRET_KEY'], 'tralala') - self.assertEqual(run.app.config['MAIL_DEFAULT_SENDER'], + self.assertFalse(self.app.config['DEBUG']) + self.assertEqual(self.app.config['SQLALCHEMY_DATABASE_URI'], 'sqlite://') + self.assertFalse(self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS']) + self.assertEqual(self.app.config['SECRET_KEY'], 'tralala') + self.assertEqual(self.app.config['MAIL_DEFAULT_SENDER'], ("Budget manager", "budget@notmyidea.org")) - def test_env_var_configuration_file(self): - """Test that settings are loaded from the specified configuration file""" - os.environ['IHATEMONEY_SETTINGS_FILE_PATH'] = os.path.join(__HERE__, - "ihatemoney_envvar.cfg") - run.configure() - self.assertEqual(run.app.config['SECRET_KEY'], 'lalatra') - # Test that the specified configuration file is loaded - # even if the default configuration file ihatemoney.cfg exists - os.environ['IHATEMONEY_SETTINGS_FILE_PATH'] = os.path.join(__HERE__, - "ihatemoney_envvar.cfg") - run.app.config.root_path = __HERE__ - run.configure() - self.assertEqual(run.app.config['SECRET_KEY'], 'lalatra') - if 'IHATEMONEY_SETTINGS_FILE_PATH' in os.environ: - del os.environ['IHATEMONEY_SETTINGS_FILE_PATH'] - - def test_default_configuration_file(self): - """Test that settings are loaded from the default configuration file""" - run.app.config.root_path = __HERE__ - run.configure() - self.assertEqual(run.app.config['SECRET_KEY'], 'supersecret') +class BudgetTestCase(IhatemoneyTestCase): def test_notifications(self): """Test that the notifications are sent, and that email adresses are checked properly. """ # sending a message to one person - with run.mail.record_messages() as outbox: + with self.app.mail.record_messages() as outbox: # create a project self.login("raclette") self.post_project("raclette") - self.app.post("/raclette/invite", - data={"emails": 'alexis@notmyidea.org'}) + self.client.post("/raclette/invite", + data={"emails": 'alexis@notmyidea.org'}) self.assertEqual(len(outbox), 2) self.assertEqual(outbox[0].recipients, ["raclette@notmyidea.org"]) self.assertEqual(outbox[1].recipients, ["alexis@notmyidea.org"]) # sending a message to multiple persons - with run.mail.record_messages() as outbox: - self.app.post("/raclette/invite", - data={"emails": 'alexis@notmyidea.org, toto@notmyidea.org'}) + with self.app.mail.record_messages() as outbox: + self.client.post("/raclette/invite", + data={"emails": 'alexis@notmyidea.org, toto@notmyidea.org'}) # only one message is sent to multiple persons self.assertEqual(len(outbox), 1) self.assertEqual(outbox[0].recipients, - ["alexis@notmyidea.org", "toto@notmyidea.org"]) + ["alexis@notmyidea.org", "toto@notmyidea.org"]) # mail address checking - with run.mail.record_messages() as outbox: - response = self.app.post("/raclette/invite", - data={"emails": "toto"}) + with self.app.mail.record_messages() as outbox: + response = self.client.post("/raclette/invite", + data={"emails": "toto"}) self.assertEqual(len(outbox), 0) # no message sent self.assertIn("The email toto is not valid", response.data.decode('utf-8')) # mixing good and wrong adresses shouldn't send any messages - with run.mail.record_messages() as outbox: - self.app.post("/raclette/invite", - data={"emails": 'alexis@notmyidea.org, alexis'}) # not valid + with self.app.mail.record_messages() as outbox: + self.client.post("/raclette/invite", + data={"emails": 'alexis@notmyidea.org, alexis'}) # not valid # only one message is sent to multiple persons self.assertEqual(len(outbox), 0) @@ -155,19 +134,19 @@ class BudgetTestCase(TestCase): self.create_project("raclette") - with run.mail.record_messages() as outbox: + with self.app.mail.record_messages() as outbox: # a nonexisting project should not send an email - self.app.post("/password-reminder", data={"id": "unexisting"}) + self.client.post("/password-reminder", data={"id": "unexisting"}) self.assertEqual(len(outbox), 0) # a mail should be sent when a project exists - self.app.post("/password-reminder", data={"id": "raclette"}) + self.client.post("/password-reminder", data={"id": "raclette"}) self.assertEqual(len(outbox), 1) self.assertIn("raclette", outbox[0].body) self.assertIn("raclette@notmyidea.org", outbox[0].recipients) def test_project_creation(self): - with run.app.test_client() as c: + with self.app.test_client() as c: # add a valid project c.post("/create", data={ @@ -198,7 +177,7 @@ class BudgetTestCase(TestCase): def test_project_deletion(self): - with run.app.test_client() as c: + with self.app.test_client() as c: c.post("/create", data={ 'name': 'raclette party', 'id': 'raclette', @@ -219,37 +198,37 @@ class BudgetTestCase(TestCase): self.login("raclette") # adds a member to this project - self.app.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) self.assertEqual(len(models.Project.query.get("raclette").members), 1) # adds him twice - result = self.app.post("/raclette/members/add", - data={'name': 'alexis'}) + result = self.client.post("/raclette/members/add", + data={'name': 'alexis'}) # should not accept him self.assertEqual(len(models.Project.query.get("raclette").members), 1) # add fred - self.app.post("/raclette/members/add", data={'name': 'fred'}) + self.client.post("/raclette/members/add", data={'name': 'fred'}) self.assertEqual(len(models.Project.query.get("raclette").members), 2) # check fred is present in the bills page - result = self.app.get("/raclette/") + result = self.client.get("/raclette/") self.assertIn("fred", result.data.decode('utf-8')) # remove fred - self.app.post("/raclette/members/%s/delete" % - models.Project.query.get("raclette").members[-1].id) + self.client.post("/raclette/members/%s/delete" % + models.Project.query.get("raclette").members[-1].id) # as fred is not bound to any bill, he is removed self.assertEqual(len(models.Project.query.get("raclette").members), 1) # add fred again - self.app.post("/raclette/members/add", data={'name': 'fred'}) + self.client.post("/raclette/members/add", data={'name': 'fred'}) fred_id = models.Project.query.get("raclette").members[-1].id # bound him to a bill - result = self.app.post("/raclette/add", data={ + result = self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': fred_id, @@ -258,47 +237,47 @@ class BudgetTestCase(TestCase): }) # remove fred - self.app.post("/raclette/members/%s/delete" % fred_id) + self.client.post("/raclette/members/%s/delete" % fred_id) # he is still in the database, but is deactivated self.assertEqual(len(models.Project.query.get("raclette").members), 2) self.assertEqual( - len(models.Project.query.get("raclette").active_members), 1) + len(models.Project.query.get("raclette").active_members), 1) # as fred is now deactivated, check that he is not listed when adding # a bill or displaying the balance - result = self.app.get("/raclette/") + result = self.client.get("/raclette/") self.assertNotIn(("/raclette/members/%s/delete" % fred_id), result.data.decode('utf-8')) - result = self.app.get("/raclette/add") + result = self.client.get("/raclette/add") self.assertNotIn("fred", result.data.decode('utf-8')) # adding him again should reactivate him - self.app.post("/raclette/members/add", data={'name': 'fred'}) + self.client.post("/raclette/members/add", data={'name': 'fred'}) self.assertEqual( - len(models.Project.query.get("raclette").active_members), 2) + len(models.Project.query.get("raclette").active_members), 2) # adding an user with the same name as another user from a different # project should not cause any troubles self.post_project("randomid") self.login("randomid") - self.app.post("/randomid/members/add", data={'name': 'fred'}) + self.client.post("/randomid/members/add", data={'name': 'fred'}) self.assertEqual( - len(models.Project.query.get("randomid").active_members), 1) + len(models.Project.query.get("randomid").active_members), 1) def test_person_model(self): self.post_project("raclette") self.login("raclette") # adds a member to this project - self.app.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) alexis = models.Project.query.get("raclette").members[-1] # should not have any bills self.assertFalse(alexis.has_bills()) # bound him to a bill - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': alexis.id, @@ -315,36 +294,36 @@ class BudgetTestCase(TestCase): self.login("raclette") # adds a member to this project - self.app.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) # try to remove the member using GET method - response = self.app.get("/raclette/members/1/delete") + response = self.client.get("/raclette/members/1/delete") self.assertEqual(response.status_code, 405) - #delete user using POST method - self.app.post("/raclette/members/1/delete") + # delete user using POST method + self.client.post("/raclette/members/1/delete") self.assertEqual( - len(models.Project.query.get("raclette").active_members), 0) - #try to delete an user already deleted - self.app.post("/raclette/members/1/delete") + len(models.Project.query.get("raclette").active_members), 0) + # try to delete an user already deleted + self.client.post("/raclette/members/1/delete") def test_demo(self): # test that a demo project is created if none is defined self.assertEqual([], models.Project.query.all()) - self.app.get("/demo") + self.client.get("/demo") self.assertTrue(models.Project.query.get("demo") is not None) def test_deactivated_demo(self): - run.app.config['ACTIVATE_DEMO_PROJECT'] = False + self.app.config['ACTIVATE_DEMO_PROJECT'] = False # test redirection to the create project form when demo is deactivated - resp = self.app.get("/demo") + resp = self.client.get("/demo") self.assertIn('', resp.data.decode('utf-8')) def test_authentication(self): # try to authenticate without credentials should redirect # to the authentication page - resp = self.app.post("/authenticate") + resp = self.client.post("/authenticate") self.assertIn("Authentication", resp.data.decode('utf-8')) # raclette that the login / logout process works @@ -352,21 +331,21 @@ class BudgetTestCase(TestCase): # try to see the project while not being authenticated should redirect # to the authentication page - resp = self.app.get("/raclette", follow_redirects=True) + resp = self.client.get("/raclette", follow_redirects=True) self.assertIn("Authentication", resp.data.decode('utf-8')) # try to connect with wrong credentials should not work - with run.app.test_client() as c: + with self.app.test_client() as c: resp = c.post("/authenticate", - data={'id': 'raclette', 'password': 'nope'}) + data={'id': 'raclette', 'password': 'nope'}) self.assertIn("Authentication", resp.data.decode('utf-8')) self.assertNotIn('raclette', session) # try to connect with the right credentials should work - with run.app.test_client() as c: + with self.app.test_client() as c: resp = c.post("/authenticate", - data={'id': 'raclette', 'password': 'raclette'}) + data={'id': 'raclette', 'password': 'raclette'}) self.assertNotIn("Authentication", resp.data.decode('utf-8')) self.assertIn('raclette', session) @@ -377,36 +356,36 @@ class BudgetTestCase(TestCase): self.assertNotIn('raclette', session) def test_admin_authentication(self): - run.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass") + self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass") # test the redirection to the authentication page when trying to access admin endpoints - resp = self.app.get("/create") + resp = self.client.get("/create") self.assertIn('', resp.data.decode('utf-8')) # test right password - resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'}) + resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'}) self.assertIn('/create', resp.data.decode('utf-8')) # test wrong password - resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'}) + resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'}) self.assertNotIn('/create', resp.data.decode('utf-8')) # test empty password - resp = self.app.post("/admin?goto=%2Fcreate", data={'admin_password': ''}) + resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': ''}) self.assertNotIn('/create', resp.data.decode('utf-8')) def test_manage_bills(self): self.post_project("raclette") # add two persons - self.app.post("/raclette/members/add", data={'name': 'alexis'}) - self.app.post("/raclette/members/add", data={'name': 'fred'}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'fred'}) members_ids = [m.id for m in models.Project.query.get("raclette").members] # create a bill - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': members_ids[0], @@ -418,7 +397,7 @@ class BudgetTestCase(TestCase): self.assertEqual(bill.amount, 25) # edit the bill - self.app.post("/raclette/edit/%s" % bill.id, data={ + self.client.post("/raclette/edit/%s" % bill.id, data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': members_ids[0], @@ -430,11 +409,11 @@ class BudgetTestCase(TestCase): self.assertEqual(bill.amount, 10, "bill edition") # delete the bill - self.app.get("/raclette/delete/%s" % bill.id) + self.client.get("/raclette/delete/%s" % bill.id) self.assertEqual(0, len(models.Bill.query.all()), "bill deletion") # test balance - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': members_ids[0], @@ -442,7 +421,7 @@ class BudgetTestCase(TestCase): 'amount': '19', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': members_ids[1], @@ -450,7 +429,7 @@ class BudgetTestCase(TestCase): 'amount': '20', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': members_ids[1], @@ -461,8 +440,8 @@ class BudgetTestCase(TestCase): balance = models.Project.query.get("raclette").balance self.assertEqual(set(balance.values()), set([19.0, -19.0])) - #Bill with negative amount - self.app.post("/raclette/add", data={ + # Bill with negative amount + self.client.post("/raclette/add", data={ 'date': '2011-08-12', 'what': 'fromage à raclette', 'payer': members_ids[0], @@ -472,8 +451,8 @@ class BudgetTestCase(TestCase): bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0] self.assertEqual(bill.amount, -25) - #add a bill with a comma - self.app.post("/raclette/add", data={ + # add a bill with a comma + self.client.post("/raclette/add", data={ 'date': '2011-08-01', 'what': 'fromage à raclette', 'payer': members_ids[0], @@ -487,14 +466,14 @@ class BudgetTestCase(TestCase): self.post_project("raclette") # add two persons - self.app.post("/raclette/members/add", data={'name': 'alexis'}) - self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4}) members_ids = [m.id for m in models.Project.query.get("raclette").members] # test balance - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': members_ids[0], @@ -502,7 +481,7 @@ class BudgetTestCase(TestCase): 'amount': '10', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'pommes de terre', 'payer': members_ids[1], @@ -517,28 +496,27 @@ class BudgetTestCase(TestCase): self.post_project("raclette") # add two persons - self.app.post("/raclette/members/add", data={'name': 'alexis'}) - self.app.post("/raclette/members/add", data={'name': 'tata', 'weight': 1}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'tata', 'weight': 1}) - resp = self.app.get("/raclette/") + resp = self.client.get("/raclette/") self.assertIn('extra-info', resp.data.decode('utf-8')) - self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4}) + self.client.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4}) - resp = self.app.get("/raclette/") + resp = self.client.get("/raclette/") self.assertNotIn('extra-info', resp.data.decode('utf-8')) - def test_rounding(self): self.post_project("raclette") # add members - self.app.post("/raclette/members/add", data={'name': 'alexis'}) - self.app.post("/raclette/members/add", data={'name': 'fred'}) - self.app.post("/raclette/members/add", data={'name': 'tata'}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'fred'}) + self.client.post("/raclette/members/add", data={'name': 'tata'}) # create bills - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': 1, @@ -546,7 +524,7 @@ class BudgetTestCase(TestCase): 'amount': '24.36', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'red wine', 'payer': 2, @@ -554,7 +532,7 @@ class BudgetTestCase(TestCase): 'amount': '19.12', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'delicatessen', 'payer': 1, @@ -567,8 +545,10 @@ class BudgetTestCase(TestCase): result[models.Project.query.get("raclette").members[0].id] = 8.12 result[models.Project.query.get("raclette").members[1].id] = 0.0 result[models.Project.query.get("raclette").members[2].id] = -8.12 - # Since we're using floating point to store currency, we can have some rounding issues that prevent test from working. - # However, we should obtain the same values as the theorical ones if we round to 2 decimals, like in the UI. + # Since we're using floating point to store currency, we can have some + # rounding issues that prevent test from working. + # However, we should obtain the same values as the theorical ones if we + # round to 2 decimals, like in the UI. for key, value in six.iteritems(balance): self.assertEqual(round(value, 2), result[key]) @@ -582,8 +562,8 @@ class BudgetTestCase(TestCase): 'password': 'didoudida' } - resp = self.app.post("/raclette/edit", data=new_data, - follow_redirects=True) + resp = self.client.post("/raclette/edit", data=new_data, + follow_redirects=True) self.assertEqual(resp.status_code, 200) project = models.Project.query.get("raclette") @@ -593,31 +573,31 @@ class BudgetTestCase(TestCase): # Editing a project with a wrong email address should fail new_data['contact_email'] = 'wrong_email' - resp = self.app.post("/raclette/edit", data=new_data, - follow_redirects=True) + resp = self.client.post("/raclette/edit", data=new_data, + follow_redirects=True) self.assertIn("Invalid email address", resp.data.decode('utf-8')) def test_dashboard(self): - response = self.app.get("/dashboard") + response = self.client.get("/dashboard") self.assertEqual(response.status_code, 200) def test_settle_page(self): self.post_project("raclette") - response = self.app.get("/raclette/settle_bills") + response = self.client.get("/raclette/settle_bills") self.assertEqual(response.status_code, 200) def test_settle(self): self.post_project("raclette") # add members - self.app.post("/raclette/members/add", data={'name': 'alexis'}) - self.app.post("/raclette/members/add", data={'name': 'fred'}) - self.app.post("/raclette/members/add", data={'name': 'tata'}) - #Add a member with a balance=0 : - self.app.post("/raclette/members/add", data={'name': 'toto'}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'fred'}) + self.client.post("/raclette/members/add", data={'name': 'tata'}) + # Add a member with a balance=0 : + self.client.post("/raclette/members/add", data={'name': 'toto'}) # create bills - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'fromage à raclette', 'payer': 1, @@ -625,7 +605,7 @@ class BudgetTestCase(TestCase): 'amount': '10.0', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'red wine', 'payer': 2, @@ -633,20 +613,20 @@ class BudgetTestCase(TestCase): 'amount': '20', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2011-08-10', 'what': 'delicatessen', 'payer': 1, 'payed_for': [1, 2], 'amount': '10', }) - project = models.Project.query.get('raclette') + project = models.Project.query.get('raclette') transactions = project.get_transactions_to_settle_bill() members = defaultdict(int) - #We should have the same values between transactions and project balances + # We should have the same values between transactions and project balances for t in transactions: - members[t['ower']]-=t['amount'] - members[t['receiver']]+=t['amount'] + members[t['ower']] -= t['amount'] + members[t['receiver']] += t['amount'] balance = models.Project.query.get("raclette").balance for m, a in members.items(): self.assertEqual(a, balance[m.id]) @@ -656,12 +636,12 @@ class BudgetTestCase(TestCase): self.post_project("raclette") # add members - self.app.post("/raclette/members/add", data={'name': 'alexis'}) - self.app.post("/raclette/members/add", data={'name': 'fred'}) - self.app.post("/raclette/members/add", data={'name': 'tata'}) + self.client.post("/raclette/members/add", data={'name': 'alexis'}) + self.client.post("/raclette/members/add", data={'name': 'fred'}) + self.client.post("/raclette/members/add", data={'name': 'tata'}) # create bills - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2016-12-31', 'what': 'fromage à raclette', 'payer': 1, @@ -669,7 +649,7 @@ class BudgetTestCase(TestCase): 'amount': '10.0', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2016-12-31', 'what': 'red wine', 'payer': 2, @@ -677,16 +657,16 @@ class BudgetTestCase(TestCase): 'amount': '20', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2017-01-01', 'what': 'refund', 'payer': 3, 'payed_for': [2], 'amount': '13.33', }) - project = models.Project.query.get('raclette') + project = models.Project.query.get('raclette') transactions = project.get_transactions_to_settle_bill() - members = defaultdict(int) + # There should not be any zero-amount transfer after rounding for t in transactions: rounded_amount = round(t['amount'], 2) @@ -697,13 +677,13 @@ class BudgetTestCase(TestCase): self.post_project("raclette") # add members - self.app.post("/raclette/members/add", data={'name': 'alexis', 'weight': 2}) - self.app.post("/raclette/members/add", data={'name': 'fred'}) - self.app.post("/raclette/members/add", data={'name': 'tata'}) - self.app.post("/raclette/members/add", data={'name': 'pépé'}) + self.client.post("/raclette/members/add", data={'name': 'alexis', 'weight': 2}) + self.client.post("/raclette/members/add", data={'name': 'fred'}) + self.client.post("/raclette/members/add", data={'name': 'tata'}) + self.client.post("/raclette/members/add", data={'name': 'pépé'}) # create bills - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2016-12-31', 'what': 'fromage à raclette', 'payer': 1, @@ -711,7 +691,7 @@ class BudgetTestCase(TestCase): 'amount': '10.0', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2016-12-31', 'what': 'red wine', 'payer': 2, @@ -719,7 +699,7 @@ class BudgetTestCase(TestCase): 'amount': '200', }) - self.app.post("/raclette/add", data={ + self.client.post("/raclette/add", data={ 'date': '2017-01-01', 'what': 'refund', 'payer': 3, @@ -728,27 +708,44 @@ class BudgetTestCase(TestCase): }) # generate json export of bills - resp = self.app.post("/raclette/edit", data={ + resp = self.client.post("/raclette/edit", data={ 'export_format': 'json', 'export_type': 'bills' }) - expected = [{'date': '2017-01-01', 'what': 'refund', - 'amount': 13.33, 'payer_name': 'tata', 'payer_weight': 1.0, 'owers': ['fred']}, - {'date': '2016-12-31', 'what': 'red wine', - 'amount': 200.0, 'payer_name': 'fred', 'payer_weight': 1.0, 'owers': ['alexis', 'tata']}, - {'date': '2016-12-31', 'what': 'fromage \xe0 raclette', - 'amount': 10.0, 'payer_name': 'alexis', 'payer_weight': 2.0, 'owers': ['alexis', 'fred', 'tata', 'p\xe9p\xe9']}] + expected = [{ + 'date': '2017-01-01', + 'what': 'refund', + 'amount': 13.33, + 'payer_name': 'tata', + 'payer_weight': 1.0, + 'owers': ['fred'] + }, { + 'date': '2016-12-31', + 'what': 'red wine', + 'amount': 200.0, + 'payer_name': 'fred', + 'payer_weight': 1.0, + 'owers': ['alexis', 'tata'] + }, { + 'date': '2016-12-31', + 'what': 'fromage \xe0 raclette', + 'amount': 10.0, + 'payer_name': 'alexis', + 'payer_weight': 2.0, + 'owers': ['alexis', 'fred', 'tata', 'p\xe9p\xe9'] + }] self.assertEqual(json.loads(resp.data.decode('utf-8')), expected) # generate csv export of bills - resp = self.app.post("/raclette/edit", data={ + resp = self.client.post("/raclette/edit", data={ 'export_format': 'csv', 'export_type': 'bills' }) - expected = ["date,what,amount,payer_name,payer_weight,owers", - "2017-01-01,refund,13.33,tata,1.0,fred", - "2016-12-31,red wine,200.0,fred,1.0,\"alexis, tata\"", - "2016-12-31,fromage à raclette,10.0,alexis,2.0,\"alexis, fred, tata, pépé\""] + expected = [ + "date,what,amount,payer_name,payer_weight,owers", + "2017-01-01,refund,13.33,tata,1.0,fred", + "2016-12-31,red wine,200.0,fred,1.0,\"alexis, tata\"", + "2016-12-31,fromage à raclette,10.0,alexis,2.0,\"alexis, fred, tata, pépé\""] received_lines = resp.data.decode('utf-8').split("\n") for i, line in enumerate(expected): @@ -758,7 +755,7 @@ class BudgetTestCase(TestCase): ) # generate json export of transactions - resp = self.app.post("/raclette/edit", data={ + resp = self.client.post("/raclette/edit", data={ 'export_format': 'json', 'export_type': 'transactions' }) @@ -768,7 +765,7 @@ class BudgetTestCase(TestCase): self.assertEqual(json.loads(resp.data.decode('utf-8')), expected) # generate csv export of transactions - resp = self.app.post("/raclette/edit", data={ + resp = self.client.post("/raclette/edit", data={ 'export_format': 'csv', 'export_type': 'transactions' }) @@ -786,7 +783,7 @@ class BudgetTestCase(TestCase): ) # wrong export_format should return a 200 and export form - resp = self.app.post("/raclette/edit", data={ + resp = self.client.post("/raclette/edit", data={ 'export_format': 'wrong_export_format', 'export_type': 'transactions' }) @@ -795,7 +792,7 @@ class BudgetTestCase(TestCase): self.assertIn('id="export_format" name="export_format"', resp.data.decode('utf-8')) # wrong export_type should return a 200 and export form - resp = self.app.post("/raclette/edit", data={ + resp = self.client.post("/raclette/edit", data={ 'export_format': 'json', 'export_type': 'wrong_export_type' }) @@ -804,7 +801,8 @@ class BudgetTestCase(TestCase): self.assertIn('id="export_format" name="export_format"', resp.data.decode('utf-8')) -class APITestCase(TestCase): +class APITestCase(IhatemoneyTestCase): + """Tests the API""" def api_create(self, name, id=None, password=None, contact=None): @@ -812,7 +810,7 @@ class APITestCase(TestCase): password = password or name contact = contact or "%s@notmyidea.org" % name - return self.app.post("/api/projects", data={ + return self.client.post("/api/projects", data={ 'name': name, 'id': id, 'password': password, @@ -820,9 +818,9 @@ class APITestCase(TestCase): }) def api_add_member(self, project, name, weight=1): - self.app.post("/api/projects/%s/members" % project, - data={"name": name, "weight": weight}, - headers=self.get_auth(project)) + self.client.post("/api/projects/%s/members" % project, + data={"name": name, "weight": weight}, + headers=self.get_auth(project)) def get_auth(self, username, password=None): password = password or username @@ -833,7 +831,7 @@ class APITestCase(TestCase): def assertStatus(self, expected, resp, url=""): return self.assertEqual(expected, resp.status_code, - "%s expected %s, got %s" % (url, expected, resp.status_code)) + "%s expected %s, got %s" % (url, expected, resp.status_code)) def test_basic_auth(self): # create a project @@ -841,7 +839,7 @@ class APITestCase(TestCase): self.assertStatus(201, resp) # try to do something on it being unauth should return a 401 - resp = self.app.get("/api/projects/raclette") + resp = self.client.get("/api/projects/raclette") self.assertStatus(401, resp) # PUT / POST / DELETE / GET on the different resources @@ -849,20 +847,20 @@ class APITestCase(TestCase): for verb in ('post',): for resource in ("/raclette/members", "/raclette/bills"): url = "/api/projects" + resource - self.assertStatus(401, getattr(self.app, verb)(url), - verb + resource) + self.assertStatus(401, getattr(self.client, verb)(url), + verb + resource) for verb in ('get', 'delete', 'put'): for resource in ("/raclette", "/raclette/members/1", - "/raclette/bills/1"): + "/raclette/bills/1"): url = "/api/projects" + resource - self.assertStatus(401, getattr(self.app, verb)(url), - verb + resource) + self.assertStatus(401, getattr(self.client, verb)(url), + verb + resource) def test_project(self): # wrong email should return an error - resp = self.app.post("/api/projects", data={ + resp = self.client.post("/api/projects", data={ 'name': "raclette", 'id': "raclette", 'password': "raclette", @@ -884,8 +882,8 @@ class APITestCase(TestCase): self.assertIn('id', json.loads(resp.data.decode('utf-8'))) # get information about it - resp = self.app.get("/api/projects/raclette", - headers=self.get_auth("raclette")) + resp = self.client.get("/api/projects/raclette", + headers=self.get_auth("raclette")) self.assertTrue(200, resp.status_code) expected = { @@ -900,16 +898,16 @@ class APITestCase(TestCase): self.assertDictEqual(json.loads(resp.data.decode('utf-8')), expected) # edit should work - resp = self.app.put("/api/projects/raclette", data={ + resp = self.client.put("/api/projects/raclette", data={ "contact_email": "yeah@notmyidea.org", "password": "raclette", "name": "The raclette party", - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) self.assertEqual(200, resp.status_code) - resp = self.app.get("/api/projects/raclette", - headers=self.get_auth("raclette")) + resp = self.client.get("/api/projects/raclette", + headers=self.get_auth("raclette")) self.assertEqual(200, resp.status_code) expected = { @@ -924,14 +922,14 @@ class APITestCase(TestCase): self.assertDictEqual(json.loads(resp.data.decode('utf-8')), expected) # delete should work - resp = self.app.delete("/api/projects/raclette", - headers=self.get_auth("raclette")) + resp = self.client.delete("/api/projects/raclette", + headers=self.get_auth("raclette")) self.assertEqual(200, resp.status_code) # get should return a 401 on an unknown resource - resp = self.app.get("/api/projects/raclette", - headers=self.get_auth("raclette")) + resp = self.client.get("/api/projects/raclette", + headers=self.get_auth("raclette")) self.assertEqual(401, resp.status_code) def test_member(self): @@ -939,53 +937,53 @@ class APITestCase(TestCase): self.api_create("raclette") # get the list of members (should be empty) - req = self.app.get("/api/projects/raclette/members", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/members", + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual('[]', req.data.decode('utf-8')) # add a member - req = self.app.post("/api/projects/raclette/members", data={ - "name": "Alexis" - }, headers=self.get_auth("raclette")) + req = self.client.post("/api/projects/raclette/members", data={ + "name": "Alexis" + }, headers=self.get_auth("raclette")) # the id of the new member should be returned self.assertStatus(201, req) self.assertEqual("1", req.data.decode('utf-8')) # the list of members should contain one member - req = self.app.get("/api/projects/raclette/members", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/members", + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual(len(json.loads(req.data.decode('utf-8'))), 1) # edit this member - req = self.app.put("/api/projects/raclette/members/1", data={ - "name": "Fred" - }, headers=self.get_auth("raclette")) + req = self.client.put("/api/projects/raclette/members/1", data={ + "name": "Fred" + }, headers=self.get_auth("raclette")) self.assertStatus(200, req) # get should return the new name - req = self.app.get("/api/projects/raclette/members/1", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/members/1", + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual("Fred", json.loads(req.data.decode('utf-8'))["name"]) # delete a member - req = self.app.delete("/api/projects/raclette/members/1", - headers=self.get_auth("raclette")) + req = self.client.delete("/api/projects/raclette/members/1", + headers=self.get_auth("raclette")) self.assertStatus(200, req) # the list of members should be empty # get the list of members (should be empty) - req = self.app.get("/api/projects/raclette/members", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/members", + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual('[]', req.data.decode('utf-8')) @@ -1000,28 +998,28 @@ class APITestCase(TestCase): self.api_add_member("raclette", "arnaud") # get the list of bills (should be empty) - req = self.app.get("/api/projects/raclette/bills", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/bills", + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual("[]", req.data.decode('utf-8')) # add a bill - req = self.app.post("/api/projects/raclette/bills", data={ + req = self.client.post("/api/projects/raclette/bills", data={ 'date': '2011-08-10', 'what': 'fromage', 'payer': "1", 'payed_for': ["1", "2"], 'amount': '25', - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) # should return the id self.assertStatus(201, req) self.assertEqual(req.data.decode('utf-8'), "1") # get this bill details - req = self.app.get("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/bills/1", + headers=self.get_auth("raclette")) # compare with the added info self.assertStatus(200, req) @@ -1038,35 +1036,35 @@ class APITestCase(TestCase): self.assertDictEqual(expected, json.loads(req.data.decode('utf-8'))) # the list of bills should lenght 1 - req = self.app.get("/api/projects/raclette/bills", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/bills", + headers=self.get_auth("raclette")) self.assertStatus(200, req) self.assertEqual(1, len(json.loads(req.data.decode('utf-8')))) # edit with errors should return an error - req = self.app.put("/api/projects/raclette/bills/1", data={ + req = self.client.put("/api/projects/raclette/bills/1", data={ 'date': '201111111-08-10', # not a date 'what': 'fromage', 'payer': "1", 'payed_for': ["1", "2"], 'amount': '25', - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) self.assertStatus(400, req) self.assertEqual('{"date": ["This field is required."]}', req.data.decode('utf-8')) # edit a bill - req = self.app.put("/api/projects/raclette/bills/1", data={ + req = self.client.put("/api/projects/raclette/bills/1", data={ 'date': '2011-09-10', 'what': 'beer', 'payer': "2", 'payed_for': ["1", "2"], 'amount': '25', - }, headers=self.get_auth("raclette")) + }, headers=self.get_auth("raclette")) # check its fields - req = self.app.get("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/bills/1", + headers=self.get_auth("raclette")) expected = { "what": "beer", @@ -1081,25 +1079,25 @@ class APITestCase(TestCase): self.assertDictEqual(expected, json.loads(req.data.decode('utf-8'))) # delete a bill - req = self.app.delete("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + req = self.client.delete("/api/projects/raclette/bills/1", + headers=self.get_auth("raclette")) self.assertStatus(200, req) # getting it should return a 404 - req = self.app.get("/api/projects/raclette/bills/1", - headers=self.get_auth("raclette")) + req = self.client.get("/api/projects/raclette/bills/1", + headers=self.get_auth("raclette")) self.assertStatus(404, req) def test_username_xss(self): # create a project - #self.api_create("raclette") + # self.api_create("raclette") self.post_project("raclette") self.login("raclette") # add members self.api_add_member("raclette", "