From fb84135fe5892f4321977a48abacf233d3ee0d9c Mon Sep 17 00:00:00 2001 From: 0livd <0livd@users.noreply.github.com> Date: Sat, 22 Apr 2017 22:07:24 +0100 Subject: Move tests to budget.tests (#205) * Move tests to budget.tests Update tox.ini to call the unittest dicovery module Closes #196 * Fix typo in Readme --- budget/tests/tests.py | 1114 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1114 insertions(+) create mode 100644 budget/tests/tests.py (limited to 'budget/tests/tests.py') diff --git a/budget/tests/tests.py b/budget/tests/tests.py new file mode 100644 index 0000000..854c07b --- /dev/null +++ b/budget/tests/tests.py @@ -0,0 +1,1114 @@ + # -*- 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 + +os.environ['FLASK_SETTINGS_MODULE'] = 'default_settings' + +from flask import session + +import run +import models +import utils + + +class TestCase(unittest.TestCase): + + 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() + + def tearDown(self): + # clean after testing + models.db.session.remove() + models.db.drop_all() + + def login(self, project, password=None, test_client=None): + password = password or project + + return self.app.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 + }) + + 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)) + models.db.session.commit() + + +class BudgetTestCase(TestCase): + + 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: + + # create a project + self.login("raclette") + + self.post_project("raclette") + self.app.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'}) + + # 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 run.mail.record_messages() as outbox: + response = self.app.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 + + # 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 run.mail.record_messages() as outbox: + # a nonexisting project should not send an email + self.app.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.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: + + # 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 run.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.app.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'}) + + # 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.assertEqual(len(models.Project.query.get("raclette").members), 2) + + # check fred is present in the bills page + result = self.app.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) + + # 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'}) + fred_id = models.Project.query.get("raclette").members[-1].id + + # bound him to a bill + result = self.app.post("/raclette/add", data={ + 'date': '2011-08-10', + 'what': 'fromage à raclette', + 'payer': fred_id, + 'payed_for': [fred_id, ], + 'amount': '25', + }) + + # remove fred + self.app.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.app.get("/raclette/") + self.assertNotIn(("/raclette/members/%s/delete" % fred_id), result.data.decode('utf-8')) + + result = self.app.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.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.app.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.app.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={ + '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.app.post("/raclette/members/add", data={'name': 'alexis'}) + + # try to remove the member using GET method + response = self.app.get("/raclette/members/1/delete") + self.assertEqual(response.status_code, 405) + + #delete user using POST method + self.app.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") + + 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.assertTrue(models.Project.query.get("demo") is not None) + + def test_authentication(self): + # try to authenticate without credentials should redirect + # to the authentication page + resp = self.app.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.app.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: + 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 run.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_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'}) + + members_ids = [m.id for m in + models.Project.query.get("raclette").members] + + # create a bill + self.app.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.app.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.app.get("/raclette/delete/%s" % bill.id) + self.assertEqual(0, len(models.Bill.query.all()), "bill deletion") + + # test balance + self.app.post("/raclette/add", data={ + 'date': '2011-08-10', + 'what': 'fromage à raclette', + 'payer': members_ids[0], + 'payed_for': members_ids, + 'amount': '19', + }) + + self.app.post("/raclette/add", data={ + 'date': '2011-08-10', + 'what': 'fromage à raclette', + 'payer': members_ids[1], + 'payed_for': members_ids[0], + 'amount': '20', + }) + + self.app.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.app.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.app.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.app.post("/raclette/members/add", data={'name': 'alexis'}) + self.app.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={ + 'date': '2011-08-10', + 'what': 'fromage à raclette', + 'payer': members_ids[0], + 'payed_for': members_ids, + 'amount': '10', + }) + + self.app.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.app.post("/raclette/members/add", data={'name': 'alexis'}) + self.app.post("/raclette/members/add", data={'name': 'tata', 'weight': 1}) + + resp = self.app.get("/raclette/") + self.assertIn('extra-info', resp.data.decode('utf-8')) + + self.app.post("/raclette/members/add", data={'name': 'freddy familly', 'weight': 4}) + + resp = self.app.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'}) + + # create bills + self.app.post("/raclette/add", data={ + 'date': '2011-08-10', + 'what': 'fromage à raclette', + 'payer': 1, + 'payed_for': [1, 2, 3], + 'amount': '24.36', + }) + + self.app.post("/raclette/add", data={ + 'date': '2011-08-10', + 'what': 'red wine', + 'payer': 2, + 'payed_for': [1], + 'amount': '19.12', + }) + + self.app.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.app.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.app.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") + self.assertEqual(response.status_code, 200) + + def test_settle_page(self): + self.post_project("raclette") + response = self.app.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'}) + + # create bills + self.app.post("/raclette/add", data={ + 'date': '2011-08-10', + 'what': 'fromage à raclette', + 'payer': 1, + 'payed_for': [1, 2, 3], + 'amount': '10.0', + }) + + self.app.post("/raclette/add", data={ + 'date': '2011-08-10', + 'what': 'red wine', + 'payer': 2, + 'payed_for': [1], + 'amount': '20', + }) + + self.app.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.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'}) + + # create bills + self.app.post("/raclette/add", data={ + 'date': '2016-12-31', + 'what': 'fromage à raclette', + 'payer': 1, + 'payed_for': [1, 2, 3], + 'amount': '10.0', + }) + + self.app.post("/raclette/add", data={ + 'date': '2016-12-31', + 'what': 'red wine', + 'payer': 2, + 'payed_for': [1, 3], + 'amount': '20', + }) + + self.app.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() + members = defaultdict(int) + # 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.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é'}) + + # create bills + self.app.post("/raclette/add", data={ + 'date': '2016-12-31', + 'what': 'fromage à raclette', + 'payer': 1, + 'payed_for': [1, 2, 3, 4], + 'amount': '10.0', + }) + + self.app.post("/raclette/add", data={ + 'date': '2016-12-31', + 'what': 'red wine', + 'payer': 2, + 'payed_for': [1, 3], + 'amount': '200', + }) + + self.app.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.app.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.app.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.app.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.app.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.app.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.app.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(TestCase): + """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.app.post("/api/projects", data={ + 'name': name, + 'id': id, + 'password': password, + 'contact_email': contact + }) + + 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)) + + 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.app.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.app, 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.app, verb)(url), + verb + resource) + + def test_project(self): + # wrong email should return an error + resp = self.app.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.app.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.app.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.app.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.app.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")) + 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.app.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")) + + # 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")) + + 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")) + + self.assertStatus(200, req) + + # get should return the new name + req = self.app.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")) + + 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")) + + 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.app.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={ + '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.app.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.app.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={ + '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.app.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.app.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.app.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")) + 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", "