from collections import defaultdict import datetime import re from time import sleep import unittest from urllib.parse import urlparse, urlunparse from flask import session import pytest from werkzeug.security import check_password_hash, generate_password_hash from ihatemoney import models from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.tests.common.help_functions import extract_link from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase from ihatemoney.versioning import LoggingMode class BudgetTestCase(IhatemoneyTestCase): def test_notifications(self): """Test that the notifications are sent, and that email addresses 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") resp = self.client.post( "/raclette/invite", data={"emails": "zorglub@notmyidea.org"}, follow_redirects=True, ) # success notification self.assertIn("Your invitations have been sent", resp.data.decode("utf-8")) self.assertEqual(len(outbox), 2) self.assertEqual(outbox[0].recipients, ["raclette@notmyidea.org"]) self.assertEqual(outbox[1].recipients, ["zorglub@notmyidea.org"]) # sending a message to multiple participants with self.app.mail.record_messages() as outbox: self.client.post( "/raclette/invite", data={"emails": "zorglub@notmyidea.org, toto@notmyidea.org"}, ) # only one message is sent to multiple participants self.assertEqual(len(outbox), 1) self.assertEqual( outbox[0].recipients, ["zorglub@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 addresses shouldn't send any messages with self.app.mail.record_messages() as outbox: self.client.post( "/raclette/invite", data={"emails": "zorglub@notmyidea.org, zorglub"} ) # not valid # only one message is sent to multiple participants self.assertEqual(len(outbox), 0) def test_invite(self): """Test that invitation e-mails are sent properly""" self.login("raclette") self.post_project("raclette") with self.app.mail.record_messages() as outbox: self.client.post("/raclette/invite", data={"emails": "toto@notmyidea.org"}) self.assertEqual(len(outbox), 1) url_start = outbox[0].body.find("You can log in using this link: ") + 32 url_end = outbox[0].body.find(".\n", url_start) url = outbox[0].body[url_start:url_end] self.client.get("/exit") # Test that we got a valid token resp = self.client.get(url, follow_redirects=True) self.assertIn( 'You probably want to ", resp.data.decode("utf-8")) # Test that password can be changed self.client.post( url, data={"password": "pass", "password_confirmation": "pass"} ) resp = self.login("raclette", password="pass") self.assertIn( "Account manager - raclette", resp.data.decode("utf-8") ) # Test empty and null tokens resp = self.client.get("/reset-password") self.assertIn("No token provided", resp.data.decode("utf-8")) resp = self.client.get("/reset-password?token=token") self.assertIn("Invalid token", resp.data.decode("utf-8")) def test_project_creation(self): with self.client as c: with self.app.mail.record_messages() as outbox: # add a valid project resp = c.post( "/create", data={ "name": "The fabulous raclette party", "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", "default_currency": "USD", }, follow_redirects=True, ) # An email is sent to the owner with a reminder of the password. self.assertEqual(len(outbox), 1) self.assertEqual(outbox[0].recipients, ["raclette@notmyidea.org"]) self.assertIn( "A reminder email has just been sent to you", resp.data.decode("utf-8"), ) # session is updated self.assertTrue(session["raclette"]) # project is created self.assertEqual(len(models.Project.query.all()), 1) # Add a second project with the same id self.get_project("raclette") c.post( "/create", data={ "name": "Another raclette party", "id": "raclette", # already used ! "password": "party", "contact_email": "raclette@notmyidea.org", "default_currency": "USD", }, ) # no new project added self.assertEqual(len(models.Project.query.all()), 1) def test_project_creation_without_public_permissions(self): self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"] = False with self.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", "default_currency": "USD", }, ) # session is not updated self.assertNotIn("raclette", session) # project is not created self.assertEqual(len(models.Project.query.all()), 0) def test_project_creation_with_public_permissions(self): self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"] = True with self.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", "default_currency": "USD", }, ) # session is updated self.assertTrue(session["raclette"]) # project is created self.assertEqual(len(models.Project.query.all()), 1) def test_project_deletion(self): with self.client as c: c.post( "/create", data={ "name": "raclette party", "id": "raclette", "password": "party", "contact_email": "raclette@notmyidea.org", "default_currency": "USD", }, ) # project added self.assertEqual(len(models.Project.query.all()), 1) # Check that we can't delete project with a GET or with a # password-less POST. resp = self.client.get("/raclette/delete") self.assertEqual(resp.status_code, 405) self.client.post("/raclette/delete") self.assertEqual(len(models.Project.query.all()), 1) # Delete for real c.post( "/raclette/delete", data={"password": "party"}, ) # project removed self.assertEqual(len(models.Project.query.all()), 0) def test_bill_placeholder(self): self.post_project("raclette") self.login("raclette") result = self.client.get("/raclette/") # Empty bill list and no participant, should now propose to add participants first self.assertIn( 'You probably want to ', 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.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.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.assertTrue(session["raclette"]) # logout should wipe the session out c.get("/exit") self.assertNotIn("raclette", session) # test that with admin credentials, one can access every project self.app.config["ADMIN_PASSWORD"] = generate_password_hash("pass") with self.client as c: resp = c.post("/admin?goto=%2Fraclette", data={"admin_password": "pass"}) self.assertNotIn("Authentication", resp.data.decode("utf-8")) self.assertTrue(session["is_admin"]) def test_authentication_with_upper_case(self): self.post_project("Raclette") # try to connect with the right credentials should work with self.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.assertTrue(session["raclette"]) def test_admin_authentication(self): self.app.config["ADMIN_PASSWORD"] = generate_password_hash("pass") # Disable public project creation so we have an admin endpoint to test self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"] = False # test the redirection to the authentication page when trying to access admin endpoints resp = self.client.get("/create") self.assertIn('', resp.data.decode("utf-8")) # test right password resp = self.client.post( "/admin?goto=%2Fcreate", data={"admin_password": "pass"} ) self.assertIn('/create', resp.data.decode("utf-8")) # test wrong password resp = self.client.post( "/admin?goto=%2Fcreate", data={"admin_password": "wrong"} ) self.assertNotIn('/create', resp.data.decode("utf-8")) # test empty password resp = self.client.post("/admin?goto=%2Fcreate", data={"admin_password": ""}) self.assertNotIn('/create', resp.data.decode("utf-8")) def test_login_throttler(self): self.app.config["ADMIN_PASSWORD"] = generate_password_hash("pass") # Activate admin login throttling by authenticating 4 times with a wrong passsword self.client.post("/admin?goto=%2Fcreate", data={"admin_password": "wrong"}) self.client.post("/admin?goto=%2Fcreate", data={"admin_password": "wrong"}) self.client.post("/admin?goto=%2Fcreate", data={"admin_password": "wrong"}) resp = self.client.post( "/admin?goto=%2Fcreate", data={"admin_password": "wrong"} ) self.assertIn( "Too many failed login attempts, please retry later.", resp.data.decode("utf-8"), ) # Change throttling delay from ihatemoney.web import login_throttler login_throttler._delay = 0.005 # Wait for delay to expire and retry logging in sleep(1) resp = self.client.post( "/admin?goto=%2Fcreate", data={"admin_password": "wrong"} ) self.assertNotIn( "Too many failed login attempts, please retry later.", resp.data.decode("utf-8"), ) def test_manage_bills(self): self.post_project("raclette") # add two participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) self.client.post("/raclette/members/add", data={"name": "fred"}) members_ids = [m.id for m in self.get_project("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", }, ) self.get_project("raclette") bill = models.Bill.query.one() self.assertEqual(bill.amount, 25) # edit the bill self.client.post( f"/raclette/edit/{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") # Try to delete the bill with a GET: it should fail response = self.client.get(f"/raclette/delete/{bill.id}") self.assertEqual(response.status_code, 405) self.assertEqual(1, len(models.Bill.query.all()), "bill deletion") # Really delete the bill self.client.post(f"/raclette/delete/{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 = self.get_project("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) # add a bill with a valid external link self.client.post( "/raclette/add", data={ "date": "2015-05-05", "what": "fromage à raclette", "payer": members_ids[0], "payed_for": members_ids, "amount": "42", "external_link": "https://example.com/fromage", }, ) bill = models.Bill.query.filter(models.Bill.date == "2015-05-05")[0] self.assertEqual(bill.external_link, "https://example.com/fromage") # add a bill with an invalid external link resp = self.client.post( "/raclette/add", data={ "date": "2015-05-06", "what": "mauvais fromage à raclette", "payer": members_ids[0], "payed_for": members_ids, "amount": "42000", "external_link": "javascript:alert('Tu bluffes, Martoni.')", }, ) self.assertIn("Invalid URL", resp.data.decode("utf-8")) def test_weighted_balance(self): self.post_project("raclette") # add two participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) self.client.post( "/raclette/members/add", data={"name": "freddy familly", "weight": 4} ) members_ids = [m.id for m in self.get_project("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 = self.get_project("raclette").balance self.assertEqual(set(balance.values()), set([6, -6])) def test_trimmed_members(self): self.post_project("raclette") # Add two times the same person (with a space at the end). self.client.post("/raclette/members/add", data={"name": "zorglub"}) self.client.post("/raclette/members/add", data={"name": "zorglub "}) members = self.get_project("raclette").members self.assertEqual(len(members), 1) def test_weighted_members_list(self): self.post_project("raclette") # add two participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) 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_negative_weight(self): self.post_project("raclette") # Add one user and edit it to have a negative share self.client.post("/raclette/members/add", data={"name": "zorglub"}) resp = self.client.post( "/raclette/members/1/edit", data={"name": "zorglub", "weight": -1} ) # An error should be generated, and its weight should still be 1. self.assertIn('

', resp.data.decode("utf-8")) self.assertEqual(len(self.get_project("raclette").members), 1) self.assertEqual(self.get_project("raclette").members[0].weight, 1) def test_rounding(self): self.post_project("raclette") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) 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 = self.get_project("raclette").balance result = {} result[self.get_project("raclette").members[0].id] = 8.12 result[self.get_project("raclette").members[1].id] = 0.0 result[self.get_project("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 theoretical ones if we # round to 2 decimals, like in the UI. for key, value in balance.items(): 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": "zorglub@notmyidea.org", "password": "didoudida", "logging_preference": LoggingMode.ENABLED.value, "default_currency": "USD", } resp = self.client.post("/raclette/edit", data=new_data, follow_redirects=True) self.assertEqual(resp.status_code, 200) project = self.get_project("raclette") self.assertEqual(project.name, new_data["name"]) self.assertEqual(project.contact_email, new_data["contact_email"]) self.assertEqual(project.default_currency, new_data["default_currency"]) self.assertTrue(check_password_hash(project.password, new_data["password"])) # 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): # test that the dashboard is deactivated by default resp = self.client.post( "/admin?goto=%2Fdashboard", data={"admin_password": "adminpass"}, follow_redirects=True, ) self.assertIn('

', resp.data.decode("utf-8")) # test access to the dashboard when it is activated self.app.config["ACTIVATE_ADMIN_DASHBOARD"] = True self.app.config["ADMIN_PASSWORD"] = generate_password_hash("adminpass") resp = self.client.post( "/admin?goto=%2Fdashboard", data={"admin_password": "adminpass"}, follow_redirects=True, ) self.assertIn( "ProjectNumber of participants", resp.data.decode("utf-8"), ) def test_statistics_page(self): self.post_project("raclette") response = self.client.get("/raclette/statistics") self.assertEqual(response.status_code, 200) def test_statistics(self): # Output is checked with the USD sign self.post_project("raclette", default_currency="USD") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2}) self.client.post("/raclette/members/add", data={"name": "fred"}) self.client.post("/raclette/members/add", data={"name": "tata"}) # Add a participant with a balance at 0 : self.client.post("/raclette/members/add", data={"name": "pépé"}) # Check that there are no monthly statistics and no active months project = self.get_project("raclette") self.assertEqual(len(project.active_months_range()), 0) self.assertEqual(len(project.monthly_stats), 0) # Check that the "monthly expenses" table is empty response = self.client.get("/raclette/statistics") regex = ( r"\s*\s*\s*\s*" r"\s*\s*\s*\s*\s*
PeriodSpent
" ) self.assertRegex(response.data.decode("utf-8"), regex) # 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", }, ) response = self.client.get("/raclette/statistics") regex = r"{}\s*{}\s*{}" self.assertRegex( response.data.decode("utf-8"), regex.format("zorglub", r"\$20\.00", r"\$31\.67"), ) self.assertRegex( response.data.decode("utf-8"), regex.format("fred", r"\$20\.00", r"\$5\.83"), ) self.assertRegex( response.data.decode("utf-8"), regex.format("tata", r"\$0\.00", r"\$2\.50") ) self.assertRegex( response.data.decode("utf-8"), regex.format("pépé", r"\$0\.00", r"\$0\.00") ) # Check that the order of participants in the sidebar table is the # same as in the main table. order = ["fred", "pépé", "tata", "zorglub"] regex1 = r".*".join( r"{}".format(name) for name in order ) regex2 = r".*".join( r"{}".format(name) for name in order ) # Build the regexp ourselves to be able to pass the DOTALL flag # (so that ".*" matches newlines) self.assertRegex(response.data.decode("utf-8"), re.compile(regex1, re.DOTALL)) self.assertRegex(response.data.decode("utf-8"), re.compile(regex2, re.DOTALL)) # Check monthly expenses again: it should have a single month and the correct amount august = datetime.date(year=2011, month=8, day=1) self.assertEqual(project.active_months_range(), [august]) self.assertEqual(dict(project.monthly_stats[2011]), {8: 40.0}) # Add bills for other months and check monthly expenses again self.client.post( "/raclette/add", data={ "date": "2011-12-20", "what": "fromage à raclette", "payer": 2, "payed_for": [1, 2], "amount": "30", }, ) months = [ datetime.date(year=2011, month=12, day=1), datetime.date(year=2011, month=11, day=1), datetime.date(year=2011, month=10, day=1), datetime.date(year=2011, month=9, day=1), datetime.date(year=2011, month=8, day=1), ] amounts_2011 = { 12: 30.0, 8: 40.0, } self.assertEqual(project.active_months_range(), months) self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011) # Test more corner cases: first day of month as oldest bill self.client.post( "/raclette/add", data={ "date": "2011-08-01", "what": "ice cream", "payer": 2, "payed_for": [1, 2], "amount": "10", }, ) amounts_2011[8] += 10.0 self.assertEqual(project.active_months_range(), months) self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011) # Last day of month as newest bill self.client.post( "/raclette/add", data={ "date": "2011-12-31", "what": "champomy", "payer": 1, "payed_for": [1, 2], "amount": "10", }, ) amounts_2011[12] += 10.0 self.assertEqual(project.active_months_range(), months) self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011) # Last day of month as oldest bill self.client.post( "/raclette/add", data={ "date": "2011-07-31", "what": "smoothie", "payer": 1, "payed_for": [1, 2], "amount": "20", }, ) months.append(datetime.date(year=2011, month=7, day=1)) amounts_2011[7] = 20.0 self.assertEqual(project.active_months_range(), months) self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011) # First day of month as newest bill self.client.post( "/raclette/add", data={ "date": "2012-01-01", "what": "more champomy", "payer": 2, "payed_for": [1, 2], "amount": "30", }, ) months.insert(0, datetime.date(year=2012, month=1, day=1)) amounts_2012 = {1: 30.0} self.assertEqual(project.active_months_range(), months) self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011) self.assertEqual(dict(project.monthly_stats[2012]), amounts_2012) 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 participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) self.client.post("/raclette/members/add", data={"name": "fred"}) self.client.post("/raclette/members/add", data={"name": "tata"}) # Add a participant with a balance at 0 : self.client.post("/raclette/members/add", data={"name": "pépé"}) # 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 = self.get_project("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 = self.get_project("raclette").balance for m, a in members.items(): assert abs(a - balance[m.id]) < 0.01 return def test_settle_zero(self): self.post_project("raclette") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) 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 = self.get_project("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"{t['amount']} is equal to zero after rounding", ) def test_access_other_projects(self): """Test that accessing or editing bills and participants from another project fails""" # Create project self.post_project("raclette") # Add participants self.client.post("/raclette/members/add", data={"name": "zorglub", "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 bill self.client.post( "/raclette/add", data={ "date": "2016-12-31", "what": "fromage à raclette", "payer": 1, "payed_for": [1, 2, 3, 4], "amount": "10.0", }, ) # Ensure it has been created raclette = self.get_project("raclette") self.assertEqual(raclette.get_bills().count(), 1) # Log out self.client.get("/exit") # Create and log in as another project self.post_project("tartiflette") modified_bill = { "date": "2018-12-31", "what": "roblochon", "payer": 2, "payed_for": [1, 3, 4], "amount": "100.0", } # Try to access bill of another project resp = self.client.get("/raclette/edit/1") self.assertStatus(303, resp) # Try to access bill of another project by ID resp = self.client.get("/tartiflette/edit/1") self.assertStatus(404, resp) # Try to edit bill resp = self.client.post("/raclette/edit/1", data=modified_bill) self.assertStatus(303, resp) # Try to edit bill by ID resp = self.client.post("/tartiflette/edit/1", data=modified_bill) self.assertStatus(404, resp) # Try to delete bill resp = self.client.post("/raclette/delete/1") self.assertStatus(303, resp) # Try to delete bill by ID resp = self.client.post("/tartiflette/delete/1") self.assertStatus(302, resp) # Additional check that the bill was indeed not modified or deleted bill = models.Bill.query.filter(models.Bill.id == 1).one() self.assertEqual(bill.what, "fromage à raclette") # Use the correct credentials to modify and delete the bill. # This ensures that modifying and deleting the bill can actually work self.client.get("/exit") self.client.post( "/authenticate", data={"id": "raclette", "password": "raclette"} ) self.client.post("/raclette/edit/1", data=modified_bill) bill = models.Bill.query.filter(models.Bill.id == 1).one_or_none() self.assertNotEqual(bill, None, "bill not found") self.assertEqual(bill.what, "roblochon") self.client.post("/raclette/delete/1") bill = models.Bill.query.filter(models.Bill.id == 1).one_or_none() self.assertEqual(bill, None) # Switch back to the second project self.client.get("/exit") self.client.post( "/authenticate", data={"id": "tartiflette", "password": "tartiflette"} ) modified_member = { "name": "bulgroz", "weight": 42, } # Try to access member from another project resp = self.client.get("/raclette/members/1/edit") self.assertStatus(303, resp) # Try to access member by ID resp = self.client.get("/tartiflette/members/1/edit") self.assertStatus(404, resp) # Try to edit member resp = self.client.post("/raclette/members/1/edit", data=modified_member) self.assertStatus(303, resp) # Try to edit member by ID resp = self.client.post("/tartiflette/members/1/edit", data=modified_member) self.assertStatus(404, resp) # Try to delete member resp = self.client.post("/raclette/members/1/delete") self.assertStatus(303, resp) # Try to delete member by ID resp = self.client.post("/tartiflette/members/1/delete") self.assertStatus(302, resp) # Additional check that the member was indeed not modified or deleted member = models.Person.query.filter(models.Person.id == 1).one_or_none() self.assertNotEqual(member, None, "member not found") self.assertEqual(member.name, "zorglub") self.assertTrue(member.activated) # Use the correct credentials to modify and delete the member. # This ensures that modifying and deleting the member can actually work self.client.get("/exit") self.client.post( "/authenticate", data={"id": "raclette", "password": "raclette"} ) self.client.post("/raclette/members/1/edit", data=modified_member) member = models.Person.query.filter(models.Person.id == 1).one() self.assertEqual(member.name, "bulgroz") self.client.post("/raclette/members/1/delete") member = models.Person.query.filter(models.Person.id == 1).one_or_none() self.assertEqual(member, None) def test_currency_switch(self): # A project should be editable self.post_project("raclette") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) 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 = self.get_project("raclette") # First all converted_amount should be the same as amount, with no currency for bill in project.get_bills(): assert bill.original_currency == CurrencyConverter.no_currency assert bill.amount == bill.converted_amount # Then, switch to EUR, all bills must have been changed to this currency project.switch_currency("EUR") for bill in project.get_bills(): assert bill.original_currency == "EUR" assert bill.amount == bill.converted_amount # Add a bill in EUR, the current default currency self.client.post( "/raclette/add", data={ "date": "2017-01-01", "what": "refund from EUR", "payer": 3, "payed_for": [2], "amount": "20", "original_currency": "EUR", }, ) last_bill = project.get_bills().first() assert last_bill.converted_amount == last_bill.amount # Erase all currencies project.switch_currency(CurrencyConverter.no_currency) for bill in project.get_bills(): assert bill.original_currency == CurrencyConverter.no_currency assert bill.amount == bill.converted_amount # Let's go back to EUR to test conversion project.switch_currency("EUR") # This is a bill in CAD self.client.post( "/raclette/add", data={ "date": "2017-01-01", "what": "Poutine", "payer": 3, "payed_for": [2], "amount": "18", "original_currency": "CAD", }, ) last_bill = project.get_bills().first() expected_amount = self.converter.exchange_currency( last_bill.amount, "CAD", "EUR" ) assert last_bill.converted_amount == expected_amount # Switch to USD. Now, NO bill should be in USD, since they already had a currency project.switch_currency("USD") for bill in project.get_bills(): assert bill.original_currency != "USD" expected_amount = self.converter.exchange_currency( bill.amount, bill.original_currency, "USD" ) assert bill.converted_amount == expected_amount # Switching back to no currency must fail with pytest.raises(ValueError): project.switch_currency(CurrencyConverter.no_currency) # It also must fails with a nice error using the form resp = self.client.post( "/raclette/edit", data={ "name": "demonstration", "password": "demo", "contact_email": "demo@notmyidea.org", "project_history": "y", "default_currency": CurrencyConverter.no_currency, }, ) # A user displayed error should be generated, and its currency should be the same. self.assertStatus(200, resp) self.assertIn('

', resp.data.decode("utf-8")) self.assertEqual(self.get_project("raclette").default_currency, "USD") def test_currency_switch_to_bill_currency(self): # Default currency is 'XXX', but we should start from a project with a currency self.post_project("raclette", default_currency="USD") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) self.client.post("/raclette/members/add", data={"name": "fred"}) # Bill with a different currency than project's default self.client.post( "/raclette/add", data={ "date": "2016-12-31", "what": "fromage à raclette", "payer": 1, "payed_for": [1, 2], "amount": "10.0", "original_currency": "EUR", }, ) project = self.get_project("raclette") bill = project.get_bills().first() assert bill.converted_amount == self.converter.exchange_currency( bill.amount, "EUR", "USD" ) # And switch project to the currency from the bill we created project.switch_currency("EUR") bill = project.get_bills().first() assert bill.converted_amount == bill.amount def test_currency_switch_to_no_currency(self): # Default currency is 'XXX', but we should start from a project with a currency self.post_project("raclette", default_currency="USD") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) self.client.post("/raclette/members/add", data={"name": "fred"}) # Bills with a different currency than project's default self.client.post( "/raclette/add", data={ "date": "2016-12-31", "what": "fromage à raclette", "payer": 1, "payed_for": [1, 2], "amount": "10.0", "original_currency": "EUR", }, ) self.client.post( "/raclette/add", data={ "date": "2017-01-01", "what": "aspirine", "payer": 2, "payed_for": [1, 2], "amount": "5.0", "original_currency": "EUR", }, ) project = self.get_project("raclette") for bill in project.get_bills_unordered(): assert bill.converted_amount == self.converter.exchange_currency( bill.amount, "EUR", "USD" ) # And switch project to no currency: amount should be equal to what was submitted project.switch_currency(CurrencyConverter.no_currency) no_currency_bills = [ (bill.amount, bill.converted_amount) for bill in project.get_bills() ] assert no_currency_bills == [(5.0, 5.0), (10.0, 10.0)] def test_amount_is_null(self): self.post_project("raclette") # add participants self.client.post("/raclette/members/add", data={"name": "zorglub"}) # null amount resp = self.client.post( "/raclette/add", data={ "date": "2016-12-31", "what": "fromage à raclette", "payer": 1, "payed_for": [1], "amount": "0", "original_currency": "EUR", }, ) assert '

' in resp.data.decode("utf-8") resp = self.client.get("/raclette/") # No bills, the previous one was not added assert "No bills" in resp.data.decode("utf-8") def test_decimals_on_weighted_members_list(self): self.post_project("raclette") # add three users with different weights self.client.post( "/raclette/members/add", data={"name": "zorglub", "weight": 1.0} ) self.client.post("/raclette/members/add", data={"name": "tata", "weight": 1.10}) self.client.post("/raclette/members/add", data={"name": "fred", "weight": 1.15}) # check if weights of the users are 1, 1.1, 1.15 respectively resp = self.client.get("/raclette/") self.assertIn( 'zorglub(x1)', resp.data.decode("utf-8") ) self.assertIn( 'tata(x1.1)', resp.data.decode("utf-8") ) self.assertIn( 'fred(x1.15)', resp.data.decode("utf-8") ) if __name__ == "__main__": unittest.main()