aboutsummaryrefslogtreecommitdiff
path: root/ihatemoney/tests/tests.py
diff options
context:
space:
mode:
authorAlexis Metaireau <alexis@notmyidea.org>2017-07-07 00:06:56 +0200
committerGitHub <noreply@github.com>2017-07-07 00:06:56 +0200
commit3a4282fd75e3b3317b2b08b4aa2e6ac154310e73 (patch)
tree9470c907ba1f884246af87d26d55c3aaac6d6dc5 /ihatemoney/tests/tests.py
parent0e374cd5e0ef5a9be67084365f91de2ab84f636c (diff)
downloadihatemoney-mirror-3a4282fd75e3b3317b2b08b4aa2e6ac154310e73.zip
ihatemoney-mirror-3a4282fd75e3b3317b2b08b4aa2e6ac154310e73.tar.gz
ihatemoney-mirror-3a4282fd75e3b3317b2b08b4aa2e6ac154310e73.tar.bz2
Absolute imports & some other improvements (#243)
* Use absolute imports and rename package to ihatemoney * Add a ihatemoney command * Factorize application creation logic * Refactor the tests * Update the wsgi.py module with the new create_app() function * Fix some styling thanks to Flake8. * Automate Flake8 check in the CI.
Diffstat (limited to 'ihatemoney/tests/tests.py')
-rw-r--r--ihatemoney/tests/tests.py1179
1 files changed, 1179 insertions, 0 deletions
diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py
new file mode 100644
index 0000000..271477a
--- /dev/null
+++ b/ihatemoney/tests/tests.py
@@ -0,0 +1,1179 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest # NOQA
+
+import os
+import json
+from collections import defaultdict
+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']
+
+__HERE__ = os.path.dirname(os.path.abspath(__file__))
+
+
+class BaseTestCase(TestCase):
+
+ SECRET_KEY = "TEST SESSION"
+
+ def create_app(self):
+ # Pass the test object as a configuration.
+ return create_app(self)
+
+ def setUp(self):
+ db.create_all()
+
+ def tearDown(self):
+ # clean after testing
+ db.session.remove()
+ db.drop_all()
+
+ def login(self, project, password=None, test_client=None):
+ password = password or project
+
+ 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.client.post("/create", data={
+ 'name': name,
+ 'id': name,
+ 'password': name,
+ 'contact_email': '%s@notmyidea.org' % name
+ })
+
+ def create_project(self, 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 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"""
+ 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['MAIL_DEFAULT_SENDER'],
+ ("Budget manager", "budget@notmyidea.org"))
+
+
+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 self.app.mail.record_messages() as outbox:
+
+ # create a project
+ self.login("raclette")
+
+ self.post_project("raclette")
+ 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 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"])
+
+ # mail address checking
+ 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 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)
+
+ def test_password_reminder(self):
+ # test that it is possible to have an email cotaining the password of a
+ # project in case people forget it (and it happens!)
+
+ self.create_project("raclette")
+
+ with self.app.mail.record_messages() as outbox:
+ # a nonexisting project should not send an email
+ self.client.post("/password-reminder", data={"id": "unexisting"})
+ self.assertEqual(len(outbox), 0)
+
+ # a mail should be sent when a project exists
+ 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 self.app.test_client() as c:
+
+ # add a valid project
+ c.post("/create", data={
+ 'name': 'The fabulous raclette party',
+ 'id': 'raclette',
+ 'password': 'party',
+ 'contact_email': 'raclette@notmyidea.org'
+ })
+
+ # session is updated
+ self.assertEqual(session['raclette'], 'party')
+
+ # project is created
+ self.assertEqual(len(models.Project.query.all()), 1)
+
+ # Add a second project with the same id
+ models.Project.query.get('raclette')
+
+ c.post("/create", data={
+ 'name': 'Another raclette party',
+ 'id': 'raclette', # already used !
+ 'password': 'party',
+ 'contact_email': 'raclette@notmyidea.org'
+ })
+
+ # no new project added
+ self.assertEqual(len(models.Project.query.all()), 1)
+
+ def test_project_deletion(self):
+
+ with self.app.test_client() as c:
+ c.post("/create", data={
+ 'name': 'raclette party',
+ 'id': 'raclette',
+ 'password': 'party',
+ 'contact_email': 'raclette@notmyidea.org'
+ })
+
+ # project added
+ self.assertEqual(len(models.Project.query.all()), 1)
+
+ c.get('/raclette/delete')
+
+ # project removed
+ self.assertEqual(len(models.Project.query.all()), 0)
+
+ def test_membership(self):
+ self.post_project("raclette")
+ self.login("raclette")
+
+ # adds a member to this project
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+ self.assertEqual(len(models.Project.query.get("raclette").members), 1)
+
+ # adds him twice
+ 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.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.client.get("/raclette/")
+ self.assertIn("fred", result.data.decode('utf-8'))
+
+ # remove fred
+ 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.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.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': fred_id,
+ 'payed_for': [fred_id, ],
+ 'amount': '25',
+ })
+
+ # remove fred
+ 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)
+
+ # as fred is now deactivated, check that he is not listed when adding
+ # a bill or displaying the balance
+ result = self.client.get("/raclette/")
+ self.assertNotIn(("/raclette/members/%s/delete" % fred_id), result.data.decode('utf-8'))
+
+ result = self.client.get("/raclette/add")
+ self.assertNotIn("fred", result.data.decode('utf-8'))
+
+ # adding him again should reactivate him
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
+ self.assertEqual(
+ 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.client.post("/randomid/members/add", data={'name': 'fred'})
+ self.assertEqual(
+ 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.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.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': alexis.id,
+ 'payed_for': [alexis.id, ],
+ 'amount': '25',
+ })
+
+ # should have a bill now
+ alexis = models.Project.query.get("raclette").members[-1]
+ self.assertTrue(alexis.has_bills())
+
+ def test_member_delete_method(self):
+ self.post_project("raclette")
+ self.login("raclette")
+
+ # adds a member to this project
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+
+ # try to remove the member using GET method
+ response = self.client.get("/raclette/members/1/delete")
+ self.assertEqual(response.status_code, 405)
+
+ # 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.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.client.get("/demo")
+ self.assertTrue(models.Project.query.get("demo") is not None)
+
+ def test_deactivated_demo(self):
+ self.app.config['ACTIVATE_DEMO_PROJECT'] = False
+
+ # test redirection to the create project form when demo is deactivated
+ resp = self.client.get("/demo")
+ self.assertIn('<a href="/create?project_id=demo">', resp.data.decode('utf-8'))
+
+ def test_authentication(self):
+ # try to authenticate without credentials should redirect
+ # to the authentication page
+ resp = self.client.post("/authenticate")
+ self.assertIn("Authentication", resp.data.decode('utf-8'))
+
+ # raclette that the login / logout process works
+ self.create_project("raclette")
+
+ # try to see the project while not being authenticated should redirect
+ # to the authentication page
+ 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 self.app.test_client() as c:
+ resp = c.post("/authenticate",
+ 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 self.app.test_client() as c:
+ resp = c.post("/authenticate",
+ data={'id': 'raclette', 'password': 'raclette'})
+
+ self.assertNotIn("Authentication", resp.data.decode('utf-8'))
+ self.assertIn('raclette', session)
+ self.assertEqual(session['raclette'], 'raclette')
+
+ # logout should wipe the session out
+ c.get("/exit")
+ self.assertNotIn('raclette', session)
+
+ def test_admin_authentication(self):
+ self.app.config['ADMIN_PASSWORD'] = generate_password_hash("pass")
+
+ # test the redirection to the authentication page when trying to access admin endpoints
+ resp = self.client.get("/create")
+ self.assertIn('<a href="/admin?goto=%2Fcreate">', resp.data.decode('utf-8'))
+
+ # test right password
+ resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'pass'})
+ self.assertIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
+
+ # test wrong password
+ resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': 'wrong'})
+ self.assertNotIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
+
+ # test empty password
+ resp = self.client.post("/admin?goto=%2Fcreate", data={'admin_password': ''})
+ self.assertNotIn('<a href="/create">/create</a>', resp.data.decode('utf-8'))
+
+ def test_manage_bills(self):
+ self.post_project("raclette")
+
+ # add two persons
+ 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.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': members_ids[0],
+ 'payed_for': members_ids,
+ 'amount': '25',
+ })
+ models.Project.query.get("raclette")
+ bill = models.Bill.query.one()
+ self.assertEqual(bill.amount, 25)
+
+ # edit the bill
+ self.client.post("/raclette/edit/%s" % bill.id, data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': members_ids[0],
+ 'payed_for': members_ids,
+ 'amount': '10',
+ })
+
+ bill = models.Bill.query.one()
+ self.assertEqual(bill.amount, 10, "bill edition")
+
+ # delete the bill
+ self.client.get("/raclette/delete/%s" % bill.id)
+ self.assertEqual(0, len(models.Bill.query.all()), "bill deletion")
+
+ # test balance
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': members_ids[0],
+ 'payed_for': members_ids,
+ 'amount': '19',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': members_ids[1],
+ 'payed_for': members_ids[0],
+ 'amount': '20',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': members_ids[1],
+ 'payed_for': members_ids,
+ 'amount': '17',
+ })
+
+ balance = models.Project.query.get("raclette").balance
+ self.assertEqual(set(balance.values()), set([19.0, -19.0]))
+
+ # Bill with negative amount
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-12',
+ 'what': 'fromage à raclette',
+ 'payer': members_ids[0],
+ 'payed_for': members_ids,
+ 'amount': '-25'
+ })
+ bill = models.Bill.query.filter(models.Bill.date == '2011-08-12')[0]
+ self.assertEqual(bill.amount, -25)
+
+ # add a bill with a comma
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-01',
+ 'what': 'fromage à raclette',
+ 'payer': members_ids[0],
+ 'payed_for': members_ids,
+ 'amount': '25,02',
+ })
+ bill = models.Bill.query.filter(models.Bill.date == '2011-08-01')[0]
+ self.assertEqual(bill.amount, 25.02)
+
+ def test_weighted_balance(self):
+ self.post_project("raclette")
+
+ # add two persons
+ 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.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': members_ids[0],
+ 'payed_for': members_ids,
+ 'amount': '10',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'pommes de terre',
+ 'payer': members_ids[1],
+ 'payed_for': members_ids,
+ 'amount': '10',
+ })
+
+ balance = models.Project.query.get("raclette").balance
+ self.assertEqual(set(balance.values()), set([6, -6]))
+
+ def test_weighted_members_list(self):
+ self.post_project("raclette")
+
+ # add two persons
+ self.client.post("/raclette/members/add", data={'name': 'alexis'})
+ self.client.post("/raclette/members/add", data={'name': 'tata', 'weight': 1})
+
+ resp = self.client.get("/raclette/")
+ self.assertIn('extra-info', resp.data.decode('utf-8'))
+
+ self.client.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4})
+
+ 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.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.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': 1,
+ 'payed_for': [1, 2, 3],
+ 'amount': '24.36',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'red wine',
+ 'payer': 2,
+ 'payed_for': [1],
+ 'amount': '19.12',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'delicatessen',
+ 'payer': 1,
+ 'payed_for': [1, 2],
+ 'amount': '22',
+ })
+
+ balance = models.Project.query.get("raclette").balance
+ result = {}
+ 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.
+ for key, value in six.iteritems(balance):
+ self.assertEqual(round(value, 2), result[key])
+
+ def test_edit_project(self):
+ # A project should be editable
+
+ self.post_project("raclette")
+ new_data = {
+ 'name': 'Super raclette party!',
+ 'contact_email': 'alexis@notmyidea.org',
+ 'password': 'didoudida'
+ }
+
+ resp = self.client.post("/raclette/edit", data=new_data,
+ follow_redirects=True)
+ self.assertEqual(resp.status_code, 200)
+ project = models.Project.query.get("raclette")
+
+ for key, value in new_data.items():
+ self.assertEqual(getattr(project, key), value, key)
+
+ # Editing a project with a wrong email address should fail
+ new_data['contact_email'] = 'wrong_email'
+
+ 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.client.get("/dashboard")
+ self.assertEqual(response.status_code, 200)
+
+ def test_settle_page(self):
+ self.post_project("raclette")
+ response = self.client.get("/raclette/settle_bills")
+ self.assertEqual(response.status_code, 200)
+
+ def test_settle(self):
+ self.post_project("raclette")
+
+ # add members
+ 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.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': 1,
+ 'payed_for': [1, 2, 3],
+ 'amount': '10.0',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'red wine',
+ 'payer': 2,
+ 'payed_for': [1],
+ 'amount': '20',
+ })
+
+ 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')
+ transactions = project.get_transactions_to_settle_bill()
+ members = defaultdict(int)
+ # 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']
+ balance = models.Project.query.get("raclette").balance
+ for m, a in members.items():
+ self.assertEqual(a, balance[m.id])
+ return
+
+ def test_settle_zero(self):
+ self.post_project("raclette")
+
+ # add members
+ 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.client.post("/raclette/add", data={
+ 'date': '2016-12-31',
+ 'what': 'fromage à raclette',
+ 'payer': 1,
+ 'payed_for': [1, 2, 3],
+ 'amount': '10.0',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2016-12-31',
+ 'what': 'red wine',
+ 'payer': 2,
+ 'payed_for': [1, 3],
+ 'amount': '20',
+ })
+
+ 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')
+ transactions = project.get_transactions_to_settle_bill()
+
+ # There should not be any zero-amount transfer after rounding
+ for t in transactions:
+ rounded_amount = round(t['amount'], 2)
+ self.assertNotEqual(0.0, rounded_amount,
+ msg='%f is equal to zero after rounding' % t['amount'])
+
+ def test_export(self):
+ self.post_project("raclette")
+
+ # add members
+ 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.client.post("/raclette/add", data={
+ 'date': '2016-12-31',
+ 'what': 'fromage à raclette',
+ 'payer': 1,
+ 'payed_for': [1, 2, 3, 4],
+ 'amount': '10.0',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2016-12-31',
+ 'what': 'red wine',
+ 'payer': 2,
+ 'payed_for': [1, 3],
+ 'amount': '200',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2017-01-01',
+ 'what': 'refund',
+ 'payer': 3,
+ 'payed_for': [2],
+ 'amount': '13.33',
+ })
+
+ # generate json export of bills
+ 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']
+ }]
+ self.assertEqual(json.loads(resp.data.decode('utf-8')), expected)
+
+ # generate csv export of bills
+ 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é\""]
+ received_lines = resp.data.decode('utf-8').split("\n")
+
+ for i, line in enumerate(expected):
+ self.assertEqual(
+ set(line.split(",")),
+ set(received_lines[i].strip("\r").split(","))
+ )
+
+ # generate json export of transactions
+ resp = self.client.post("/raclette/edit", data={
+ 'export_format': 'json',
+ 'export_type': 'transactions'
+ })
+ expected = [{"amount": 127.33, "receiver": "fred", "ower": "alexis"},
+ {"amount": 55.34, "receiver": "fred", "ower": "tata"},
+ {"amount": 2.00, "receiver": "fred", "ower": "p\xe9p\xe9"}]
+ self.assertEqual(json.loads(resp.data.decode('utf-8')), expected)
+
+ # generate csv export of transactions
+ resp = self.client.post("/raclette/edit", data={
+ 'export_format': 'csv',
+ 'export_type': 'transactions'
+ })
+
+ expected = ["amount,receiver,ower",
+ "127.33,fred,alexis",
+ "55.34,fred,tata",
+ "2.0,fred,pépé"]
+ received_lines = resp.data.decode('utf-8').split("\n")
+
+ for i, line in enumerate(expected):
+ self.assertEqual(
+ set(line.split(",")),
+ set(received_lines[i].strip("\r").split(","))
+ )
+
+ # wrong export_format should return a 200 and export form
+ resp = self.client.post("/raclette/edit", data={
+ 'export_format': 'wrong_export_format',
+ 'export_type': 'transactions'
+ })
+
+ self.assertEqual(resp.status_code, 200)
+ 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.client.post("/raclette/edit", data={
+ 'export_format': 'json',
+ 'export_type': 'wrong_export_type'
+ })
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertIn('id="export_format" name="export_format"', resp.data.decode('utf-8'))
+
+
+class APITestCase(IhatemoneyTestCase):
+
+ """Tests the API"""
+
+ def api_create(self, name, id=None, password=None, contact=None):
+ id = id or name
+ password = password or name
+ contact = contact or "%s@notmyidea.org" % name
+
+ return self.client.post("/api/projects", data={
+ 'name': name,
+ 'id': id,
+ 'password': password,
+ 'contact_email': contact
+ })
+
+ def api_add_member(self, project, name, weight=1):
+ 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
+ base64string = utils.base64_encode(
+ ('%s:%s' % (username, password)).encode('utf-8')).decode('utf-8').replace('\n', '')
+ return {"Authorization": "Basic %s" % base64string}
+
+ def assertStatus(self, expected, resp, url=""):
+
+ return self.assertEqual(expected, resp.status_code,
+ "%s expected %s, got %s" % (url, expected, resp.status_code))
+
+ def test_basic_auth(self):
+ # create a project
+ resp = self.api_create("raclette")
+ self.assertStatus(201, resp)
+
+ # try to do something on it being unauth should return a 401
+ resp = self.client.get("/api/projects/raclette")
+ self.assertStatus(401, resp)
+
+ # PUT / POST / DELETE / GET on the different resources
+ # should also return a 401
+ for verb in ('post',):
+ for resource in ("/raclette/members", "/raclette/bills"):
+ url = "/api/projects" + 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"):
+ url = "/api/projects" + resource
+
+ self.assertStatus(401, getattr(self.client, verb)(url),
+ verb + resource)
+
+ def test_project(self):
+ # wrong email should return an error
+ resp = self.client.post("/api/projects", data={
+ 'name': "raclette",
+ 'id': "raclette",
+ 'password': "raclette",
+ 'contact_email': "not-an-email"
+ })
+
+ self.assertTrue(400, resp.status_code)
+ self.assertEqual('{"contact_email": ["Invalid email address."]}',
+ resp.data.decode('utf-8'))
+
+ # create it
+ resp = self.api_create("raclette")
+ self.assertTrue(201, resp.status_code)
+
+ # create it twice should return a 400
+ resp = self.api_create("raclette")
+
+ self.assertTrue(400, resp.status_code)
+ self.assertIn('id', json.loads(resp.data.decode('utf-8')))
+
+ # get information about it
+ resp = self.client.get("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
+
+ self.assertTrue(200, resp.status_code)
+ expected = {
+ "active_members": [],
+ "name": "raclette",
+ "contact_email": "raclette@notmyidea.org",
+ "members": [],
+ "password": "raclette",
+ "id": "raclette",
+ "balance": {},
+ }
+ self.assertDictEqual(json.loads(resp.data.decode('utf-8')), expected)
+
+ # edit should work
+ resp = self.client.put("/api/projects/raclette", data={
+ "contact_email": "yeah@notmyidea.org",
+ "password": "raclette",
+ "name": "The raclette party",
+ }, headers=self.get_auth("raclette"))
+
+ self.assertEqual(200, resp.status_code)
+
+ resp = self.client.get("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
+
+ self.assertEqual(200, resp.status_code)
+ expected = {
+ "active_members": [],
+ "name": "The raclette party",
+ "contact_email": "yeah@notmyidea.org",
+ "members": [],
+ "password": "raclette",
+ "id": "raclette",
+ "balance": {},
+ }
+ self.assertDictEqual(json.loads(resp.data.decode('utf-8')), expected)
+
+ # delete should work
+ 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.client.get("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
+ self.assertEqual(401, resp.status_code)
+
+ def test_member(self):
+ # create a project
+ self.api_create("raclette")
+
+ # get the list of members (should be empty)
+ 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.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.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.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.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.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.client.get("/api/projects/raclette/members",
+ headers=self.get_auth("raclette"))
+
+ self.assertStatus(200, req)
+ self.assertEqual('[]', req.data.decode('utf-8'))
+
+ def test_bills(self):
+ # create a project
+ self.api_create("raclette")
+
+ # add members
+ self.api_add_member("raclette", "alexis")
+ self.api_add_member("raclette", "fred")
+ self.api_add_member("raclette", "arnaud")
+
+ # get the list of bills (should be empty)
+ 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.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"))
+
+ # should return the id
+ self.assertStatus(201, req)
+ self.assertEqual(req.data.decode('utf-8'), "1")
+
+ # get this bill details
+ req = self.client.get("/api/projects/raclette/bills/1",
+ headers=self.get_auth("raclette"))
+
+ # compare with the added info
+ self.assertStatus(200, req)
+ expected = {
+ "what": "fromage",
+ "payer_id": 1,
+ "owers": [
+ {"activated": True, "id": 1, "name": "alexis", "weight": 1},
+ {"activated": True, "id": 2, "name": "fred", "weight": 1}],
+ "amount": 25.0,
+ "date": "2011-08-10",
+ "id": 1}
+
+ self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
+
+ # the list of bills should lenght 1
+ 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.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"))
+
+ self.assertStatus(400, req)
+ self.assertEqual('{"date": ["This field is required."]}', req.data.decode('utf-8'))
+
+ # edit a bill
+ 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"))
+
+ # check its fields
+ req = self.client.get("/api/projects/raclette/bills/1",
+ headers=self.get_auth("raclette"))
+
+ expected = {
+ "what": "beer",
+ "payer_id": 2,
+ "owers": [
+ {"activated": True, "id": 1, "name": "alexis", "weight": 1},
+ {"activated": True, "id": 2, "name": "fred", "weight": 1}],
+ "amount": 25.0,
+ "date": "2011-09-10",
+ "id": 1}
+
+ self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
+
+ # delete a bill
+ 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.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.post_project("raclette")
+ self.login("raclette")
+
+ # add members
+ self.api_add_member("raclette", "<script>")
+
+ result = self.client.get('/raclette/')
+ self.assertNotIn("<script>", result.data.decode('utf-8'))
+
+ def test_weighted_bills(self):
+ # create a project
+ self.api_create("raclette")
+
+ # add members
+ self.api_add_member("raclette", "alexis")
+ self.api_add_member("raclette", "freddy familly", 4)
+ self.api_add_member("raclette", "arnaud")
+
+ # add a bill
+ 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"))
+
+ # get this bill details
+ req = self.client.get("/api/projects/raclette/bills/1",
+ headers=self.get_auth("raclette"))
+
+ # compare with the added info
+ self.assertStatus(200, req)
+ expected = {
+ "what": "fromage",
+ "payer_id": 1,
+ "owers": [
+ {"activated": True, "id": 1, "name": "alexis", "weight": 1},
+ {"activated": True, "id": 2, "name": "freddy familly", "weight": 4}],
+ "amount": 25.0,
+ "date": "2011-08-10",
+ "id": 1}
+ self.assertDictEqual(expected, json.loads(req.data.decode('utf-8')))
+
+ # getting it should return a 404
+ req = self.client.get("/api/projects/raclette",
+ headers=self.get_auth("raclette"))
+
+ expected = {
+ "active_members": [
+ {"activated": True, "id": 1, "name": "alexis", "weight": 1.0},
+ {"activated": True, "id": 2, "name": "freddy familly", "weight": 4.0},
+ {"activated": True, "id": 3, "name": "arnaud", "weight": 1.0}
+ ],
+ "balance": {"1": 20.0, "2": -20.0, "3": 0},
+ "contact_email": "raclette@notmyidea.org",
+ "id": "raclette",
+
+ "members": [
+ {"activated": True, "id": 1, "name": "alexis", "weight": 1.0},
+ {"activated": True, "id": 2, "name": "freddy familly", "weight": 4.0},
+ {"activated": True, "id": 3, "name": "arnaud", "weight": 1.0}
+ ],
+ "name": "raclette",
+ "password": "raclette"}
+
+ self.assertStatus(200, req)
+ self.assertEqual(expected, json.loads(req.data.decode('utf-8')))
+
+
+class ServerTestCase(APITestCase):
+
+ def test_unprefixed(self):
+ self.app.config['APPLICATION_ROOT'] = '/'
+ req = self.client.get("/foo/")
+ self.assertStatus(303, req)
+
+ def test_prefixed(self):
+ self.app.config['APPLICATION_ROOT'] = '/foo'
+ req = self.client.get("/foo/")
+ self.assertStatus(200, req)
+
+
+if __name__ == "__main__":
+ unittest.main()