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", "