Simplify tests (#685)

Fix #501
This commit is contained in:
Miguel Victoria Villaquiran 2021-01-05 22:17:26 +01:00 committed by GitHub
parent e0bc285c92
commit 18068d76ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 3139 additions and 3107 deletions

View file

@ -0,0 +1,748 @@
import base64
import datetime
import json
import unittest
from ihatemoney.tests.common.help_functions import em_surround
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
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 f"{name}@notmyidea.org"
return self.client.post(
"/api/projects",
data={
"name": name,
"id": id,
"password": password,
"contact_email": contact,
"default_currency": "USD",
},
)
def api_add_member(self, project, name, weight=1):
self.client.post(
f"/api/projects/{project}/members",
data={"name": name, "weight": weight},
headers=self.get_auth(project),
)
def get_auth(self, username, password=None):
password = password or username
base64string = (
base64.encodebytes(f"{username}:{password}".encode("utf-8"))
.decode("utf-8")
.replace("\n", "")
)
return {"Authorization": f"Basic {base64string}"}
def test_cors_requests(self):
# Create a project and test that CORS headers are present if requested.
resp = self.api_create("raclette")
self.assertStatus(201, resp)
# Try to do an OPTIONS requests and see if the headers are correct.
resp = self.client.options(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertEqual(resp.headers["Access-Control-Allow-Origin"], "*")
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",
"default_currency": "USD",
},
)
self.assertTrue(400, resp.status_code)
self.assertEqual(
'{"contact_email": ["Invalid email address."]}\n', 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 = {
"members": [],
"name": "raclette",
"contact_email": "raclette@notmyidea.org",
"default_currency": "USD",
"id": "raclette",
"logging_preference": 1,
}
decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
# edit should work
resp = self.client.put(
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "USD",
"password": "raclette",
"name": "The raclette party",
"project_history": "y",
},
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 = {
"name": "The raclette party",
"contact_email": "yeah@notmyidea.org",
"default_currency": "USD",
"members": [],
"id": "raclette",
"logging_preference": 1,
}
decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
# password change is possible via API
resp = self.client.put(
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "USD",
"password": "tartiflette",
"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", "tartiflette")
)
self.assertEqual(200, resp.status_code)
# delete should work
resp = self.client.delete(
"/api/projects/raclette", headers=self.get_auth("raclette", "tartiflette")
)
# 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_token_creation(self):
"""Test that token of project is generated"""
# Create project
resp = self.api_create("raclette")
self.assertTrue(201, resp.status_code)
# Get token
resp = self.client.get(
"/api/projects/raclette/token", headers=self.get_auth("raclette")
)
self.assertEqual(200, resp.status_code)
decoded_resp = json.loads(resp.data.decode("utf-8"))
# Access with token
resp = self.client.get(
"/api/projects/raclette/token",
headers={"Authorization": f"Basic {decoded_resp['token']}"},
)
self.assertEqual(200, resp.status_code)
def test_token_login(self):
resp = self.api_create("raclette")
# Get token
resp = self.client.get(
"/api/projects/raclette/token", headers=self.get_auth("raclette")
)
decoded_resp = json.loads(resp.data.decode("utf-8"))
resp = self.client.get("/authenticate?token={}".format(decoded_resp["token"]))
# Test that we are redirected.
self.assertEqual(302, 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("[]\n", req.data.decode("utf-8"))
# add a member
req = self.client.post(
"/api/projects/raclette/members",
data={"name": "Zorglub"},
headers=self.get_auth("raclette"),
)
# the id of the new member should be returned
self.assertStatus(201, req)
self.assertEqual("1\n", 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)
# Try to add another member with the same name.
req = self.client.post(
"/api/projects/raclette/members",
data={"name": "Zorglub"},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
# edit the member
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "weight": 2},
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"])
self.assertEqual(2, json.loads(req.data.decode("utf-8"))["weight"])
# edit this member with same information
# (test PUT idemopotence)
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred"},
headers=self.get_auth("raclette"),
)
self.assertStatus(200, req)
# de-activate the user
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "activated": False},
headers=self.get_auth("raclette"),
)
self.assertStatus(200, req)
req = self.client.get(
"/api/projects/raclette/members/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(False, json.loads(req.data.decode("utf-8"))["activated"])
# re-activate the user
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "activated": True},
headers=self.get_auth("raclette"),
)
req = self.client.get(
"/api/projects/raclette/members/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(True, json.loads(req.data.decode("utf-8"))["activated"])
# 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
req = self.client.get(
"/api/projects/raclette/members", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8"))
def test_bills(self):
# create a project
self.api_create("raclette")
# add members
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
self.api_add_member("raclette", "quentin")
# 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("[]\n", 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",
"external_link": "https://raclette.fr",
},
headers=self.get_auth("raclette"),
)
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode("utf-8"), "1\n")
# 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": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
],
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
"converted_amount": 25.0,
"original_currency": "USD",
"external_link": "https://raclette.fr",
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# the list of bills should length 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",
"external_link": "https://raclette.fr",
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
self.assertEqual(
'{"date": ["This field is required."]}\n', 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",
"external_link": "https://raclette.fr",
},
headers=self.get_auth("raclette"),
)
# check its fields
req = self.client.get(
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
)
creation_date = datetime.datetime.strptime(
json.loads(req.data.decode("utf-8"))["creation_date"], "%Y-%m-%d"
).date()
expected = {
"what": "beer",
"payer_id": 2,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
],
"amount": 25.0,
"date": "2011-09-10",
"external_link": "https://raclette.fr",
"converted_amount": 25.0,
"original_currency": "USD",
"id": 1,
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
creation_date,
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# 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_bills_with_calculation(self):
# create a project
self.api_create("raclette")
# add members
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
# valid amounts
input_expected = [
("((100 + 200.25) * 2 - 100) / 2", 250.25),
("3/2", 1.5),
("2 + 1 * 5 - 2 / 1", 5),
]
for i, pair in enumerate(input_expected):
input_amount, expected_amount = pair
id = i + 1
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"amount": input_amount,
},
headers=self.get_auth("raclette"),
)
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode("utf-8"), "{}\n".format(id))
# get this bill's details
req = self.client.get(
"/api/projects/raclette/bills/{}".format(id),
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": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
],
"amount": expected_amount,
"date": "2011-08-10",
"id": id,
"external_link": "",
"original_currency": "USD",
"converted_amount": expected_amount,
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# should raise errors
erroneous_amounts = [
"lambda ", # letters
"(20 + 2", # invalid expression
"20/0", # invalid calc
"9999**99999999999999999", # exponents
"2" * 201, # greater than 200 chars,
]
for amount in erroneous_amounts:
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"amount": amount,
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
def test_statistics(self):
# create a project
self.api_create("raclette")
# add members
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
# 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 the list of bills (should be empty)
req = self.client.get(
"/api/projects/raclette/statistics", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(
[
{
"balance": 12.5,
"member": {
"activated": True,
"id": 1,
"name": "zorglub",
"weight": 1.0,
},
"paid": 25.0,
"spent": 12.5,
},
{
"balance": -12.5,
"member": {
"activated": True,
"id": 2,
"name": "fred",
"weight": 1.0,
},
"paid": 0,
"spent": 12.5,
},
],
json.loads(req.data.decode("utf-8")),
)
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", "zorglub")
self.api_add_member("raclette", "freddy familly", 4)
self.api_add_member("raclette", "quentin")
# 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")
)
creation_date = datetime.datetime.strptime(
json.loads(req.data.decode("utf-8"))["creation_date"], "%Y-%m-%d"
).date()
# compare with the added info
self.assertStatus(200, req)
expected = {
"what": "fromage",
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "freddy familly", "weight": 4},
],
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
"external_link": "",
"converted_amount": 25.0,
"original_currency": "USD",
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
creation_date,
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# getting it should return a 404
req = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
expected = {
"members": [
{
"activated": True,
"id": 1,
"name": "zorglub",
"weight": 1.0,
"balance": 20.0,
},
{
"activated": True,
"id": 2,
"name": "freddy familly",
"weight": 4.0,
"balance": -20.0,
},
{
"activated": True,
"id": 3,
"name": "quentin",
"weight": 1.0,
"balance": 0,
},
],
"contact_email": "raclette@notmyidea.org",
"id": "raclette",
"name": "raclette",
"logging_preference": 1,
"default_currency": "USD",
}
self.assertStatus(200, req)
decoded_req = json.loads(req.data.decode("utf-8"))
self.assertDictEqual(decoded_req, expected)
def test_log_created_from_api_call(self):
# create a project
self.api_create("raclette")
self.login("raclette")
# add members
self.api_add_member("raclette", "zorglub")
resp = self.client.get("/raclette/history", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('zorglub')} added", resp.data.decode("utf-8")
)
self.assertIn(
f"Project {em_surround('raclette')} added", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
def em_surround(string, regex_escape=False):
if regex_escape:
return r'<em class="font-italic">%s<\/em>' % string
else:
return '<em class="font-italic">%s</em>' % string

View file

@ -0,0 +1,70 @@
from flask_testing import TestCase
from werkzeug.security import generate_password_hash
from ihatemoney import models
from ihatemoney.run import create_app, db
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, follow_redirects=True):
"""Create a fake project"""
# create the project
return self.client.post(
"/create",
data={
"name": name,
"id": name,
"password": name,
"contact_email": f"{name}@notmyidea.org",
"default_currency": "USD",
},
follow_redirects=follow_redirects,
)
def create_project(self, name):
project = models.Project(
id=name,
name=str(name),
password=generate_password_hash(name),
contact_email=f"{name}@notmyidea.org",
default_currency="USD",
)
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.
def assertStatus(self, expected, resp, url=""):
return self.assertEqual(
expected,
resp.status_code,
f"{url} expected {expected}, got {resp.status_code}",
)

View file

@ -0,0 +1,599 @@
import unittest
from ihatemoney import history, models
from ihatemoney.tests.common.help_functions import em_surround
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
from ihatemoney.versioning import LoggingMode
class HistoryTestCase(IhatemoneyTestCase):
def setUp(self):
super().setUp()
self.post_project("demo")
self.login("demo")
def test_simple_create_logentry_no_ip(self):
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(f"Project {em_surround('demo')} added", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
def change_privacy_to(self, logging_preference):
# Change only logging_preferences
new_data = {
"name": "demo",
"contact_email": "demo@notmyidea.org",
"password": "demo",
"default_currency": "USD",
}
if logging_preference != LoggingMode.DISABLED:
new_data["project_history"] = "y"
if logging_preference == LoggingMode.RECORD_IP:
new_data["ip_recording"] = "y"
# Disable History
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assertNotIn("danger", resp.data.decode("utf-8"))
resp = self.client.get("/demo/edit")
self.assertEqual(resp.status_code, 200)
if logging_preference == LoggingMode.DISABLED:
self.assertIn('<input id="project_history"', resp.data.decode("utf-8"))
else:
self.assertIn(
'<input checked id="project_history"', resp.data.decode("utf-8")
)
if logging_preference == LoggingMode.RECORD_IP:
self.assertIn('<input checked id="ip_recording"', resp.data.decode("utf-8"))
else:
self.assertIn('<input id="ip_recording"', resp.data.decode("utf-8"))
def assert_empty_history_logging_disabled(self):
resp = self.client.get("/demo/history")
self.assertIn(
"This project has history disabled. New actions won't appear below. ",
resp.data.decode("utf-8"),
)
self.assertIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
)
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertNotIn("<td> -- </td>", resp.data.decode("utf-8"))
self.assertNotIn(
f"Project {em_surround('demo')} added", resp.data.decode("utf-8")
)
def test_project_edit(self):
new_data = {
"name": "demo2",
"contact_email": "demo2@notmyidea.org",
"password": "123456",
"project_history": "y",
"default_currency": "USD",
}
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(f"Project {em_surround('demo')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Project contact email changed to {em_surround('demo2@notmyidea.org')}",
resp.data.decode("utf-8"),
)
self.assertIn("Project private code changed", resp.data.decode("utf-8"))
self.assertIn(
f"Project renamed to {em_surround('demo2')}", resp.data.decode("utf-8")
)
self.assertLess(
resp.data.decode("utf-8").index("Project renamed "),
resp.data.decode("utf-8").index("Project contact email changed to "),
)
self.assertLess(
resp.data.decode("utf-8").index("Project renamed "),
resp.data.decode("utf-8").index("Project private code changed"),
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 4)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
def test_project_privacy_edit(self):
resp = self.client.get("/demo/edit")
self.assertEqual(resp.status_code, 200)
self.assertIn(
'<input checked id="project_history" name="project_history" type="checkbox" value="y">',
resp.data.decode("utf-8"),
)
self.change_privacy_to(LoggingMode.DISABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Disabled Project History\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.change_privacy_to(LoggingMode.RECORD_IP)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Enabled Project History & IP Address Recording", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 1)
self.change_privacy_to(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Disabled IP Address Recording\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
def test_project_privacy_edit2(self):
self.change_privacy_to(LoggingMode.RECORD_IP)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Enabled IP Address Recording\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 1)
self.change_privacy_to(LoggingMode.DISABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Disabled Project History & IP Address Recording", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
self.change_privacy_to(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Enabled Project History\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
def do_misc_database_operations(self, logging_mode):
new_data = {
"name": "demo2",
"contact_email": "demo2@notmyidea.org",
"password": "123456",
"default_currency": "USD",
}
# Keep privacy settings where they were
if logging_mode != LoggingMode.DISABLED:
new_data["project_history"] = "y"
if logging_mode == LoggingMode.RECORD_IP:
new_data["ip_recording"] = "y"
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
# adds a member to this project
resp = self.client.post(
"/demo/members/add", data={"name": "zorglub"}, follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
user_id = models.Person.query.one().id
# create a bill
resp = self.client.post(
"/demo/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
"amount": "25",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
bill_id = models.Bill.query.one().id
# edit the bill
resp = self.client.post(
f"/demo/edit/{bill_id}",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
"amount": "10",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
# delete the bill
resp = self.client.get(f"/demo/delete/{bill_id}", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
# delete user using POST method
resp = self.client.post(
f"/demo/members/{user_id}/delete", follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
def test_disable_clear_no_new_records(self):
# Disable logging
self.change_privacy_to(LoggingMode.DISABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"This project has history disabled. New actions won't appear below. ",
resp.data.decode("utf-8"),
)
self.assertIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
# Clear Existing Entries
resp = self.client.post("/demo/erase_history", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assert_empty_history_logging_disabled()
# Do lots of database operations & check that there's still no history
self.do_misc_database_operations(LoggingMode.DISABLED)
self.assert_empty_history_logging_disabled()
def test_clear_ip_records(self):
# Enable IP Recording
self.change_privacy_to(LoggingMode.RECORD_IP)
# Do lots of database operations to generate IP address entries
self.do_misc_database_operations(LoggingMode.RECORD_IP)
# Disable IP Recording
self.change_privacy_to(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertNotIn(
"This project has history disabled. New actions won't appear below. ",
resp.data.decode("utf-8"),
)
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 10)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
# Generate more operations to confirm additional IP info isn't recorded
self.do_misc_database_operations(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 10)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6)
# Clear IP Data
resp = self.client.post("/demo/strip_ip_addresses", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assertNotIn(
"This project has history disabled. New actions won't appear below. ",
resp.data.decode("utf-8"),
)
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 0)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 16)
def test_logs_for_common_actions(self):
# adds a member to this project
resp = self.client.post(
"/demo/members/add", data={"name": "zorglub"}, follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('zorglub')} added", resp.data.decode("utf-8")
)
# create a bill
resp = self.client.post(
"/demo/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"amount": "25",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('fromage à raclette')} added",
resp.data.decode("utf-8"),
)
# edit the bill
resp = self.client.post(
"/demo/edit/1",
data={
"date": "2011-08-10",
"what": "new thing",
"payer": 1,
"payed_for": [1],
"amount": "10",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('fromage à raclette')} added",
resp.data.decode("utf-8"),
)
self.assertRegex(
resp.data.decode("utf-8"),
r"Bill %s:\s* Amount changed\s* from %s\s* to %s"
% (
em_surround("fromage à raclette", regex_escape=True),
em_surround("25.0", regex_escape=True),
em_surround("10.0", regex_escape=True),
),
)
self.assertIn(
"Bill %s renamed to %s"
% (em_surround("fromage à raclette"), em_surround("new thing")),
resp.data.decode("utf-8"),
)
self.assertLess(
resp.data.decode("utf-8").index(
f"Bill {em_surround('fromage à raclette')} renamed to"
),
resp.data.decode("utf-8").index("Amount changed"),
)
# delete the bill
resp = self.client.get("/demo/delete/1", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('new thing')} removed", resp.data.decode("utf-8")
)
# edit user
resp = self.client.post(
"/demo/members/1/edit",
data={"weight": 2, "name": "new name"},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertRegex(
resp.data.decode("utf-8"),
r"Participant %s:\s* Weight changed\s* from %s\s* to %s"
% (
em_surround("zorglub", regex_escape=True),
em_surround("1.0", regex_escape=True),
em_surround("2.0", regex_escape=True),
),
)
self.assertIn(
"Participant %s renamed to %s"
% (em_surround("zorglub"), em_surround("new name")),
resp.data.decode("utf-8"),
)
self.assertLess(
resp.data.decode("utf-8").index(
f"Participant {em_surround('zorglub')} renamed"
),
resp.data.decode("utf-8").index("Weight changed"),
)
# delete user using POST method
resp = self.client.post("/demo/members/1/delete", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('new name')} removed", resp.data.decode("utf-8")
)
def test_double_bill_double_person_edit_second(self):
# add two members
self.client.post("/demo/members/add", data={"name": "User 1"})
self.client.post("/demo/members/add", data={"name": "User 2"})
# add two bills
self.client.post(
"/demo/add",
data={
"date": "2020-04-13",
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"amount": "25",
},
)
self.client.post(
"/demo/add",
data={
"date": "2020-04-13",
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
"amount": "20",
},
)
# Should be 5 history entries at this point
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
# Edit ONLY the amount on the first bill
self.client.post(
"/demo/edit/1",
data={
"date": "2020-04-13",
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"amount": "88",
},
)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertRegex(
resp.data.decode("utf-8"),
r"Bill {}:\s* Amount changed\s* from {}\s* to {}".format(
em_surround("Bill 1", regex_escape=True),
em_surround("25.0", regex_escape=True),
em_surround("88.0", regex_escape=True),
),
)
self.assertNotRegex(
resp.data.decode("utf-8"),
r"Removed\s* {}\s* and\s* {}\s* from\s* owers list".format(
em_surround("User 1", regex_escape=True),
em_surround("User 2", regex_escape=True),
),
resp.data.decode("utf-8"),
)
# Should be 6 history entries at this point
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
def test_bill_add_remove_add(self):
# add two members
self.client.post("/demo/members/add", data={"name": "User 1"})
self.client.post("/demo/members/add", data={"name": "User 2"})
# add 1 bill
self.client.post(
"/demo/add",
data={
"date": "2020-04-13",
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"amount": "25",
},
)
# delete the bill
self.client.get("/demo/delete/1", follow_redirects=True)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertIn(f"Bill {em_surround('Bill 1')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Bill {em_surround('Bill 1')} removed", resp.data.decode("utf-8")
)
# Add a new bill
self.client.post(
"/demo/add",
data={
"date": "2020-04-13",
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
"amount": "20",
},
)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertIn(f"Bill {em_surround('Bill 1')} added", resp.data.decode("utf-8"))
self.assertEqual(
resp.data.decode("utf-8").count(f"Bill {em_surround('Bill 1')} added"), 1
)
self.assertIn(f"Bill {em_surround('Bill 2')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Bill {em_surround('Bill 1')} removed", resp.data.decode("utf-8")
)
def test_double_bill_double_person_edit_second_no_web(self):
u1 = models.Person(project_id="demo", name="User 1")
u2 = models.Person(project_id="demo", name="User 1")
models.db.session.add(u1)
models.db.session.add(u2)
models.db.session.commit()
b1 = models.Bill(what="Bill 1", payer_id=u1.id, owers=[u2], amount=10)
b2 = models.Bill(what="Bill 2", payer_id=u2.id, owers=[u2], amount=11)
# This db commit exposes the "spurious owers edit" bug
models.db.session.add(b1)
models.db.session.commit()
models.db.session.add(b2)
models.db.session.commit()
history_list = history.get_history(models.Project.query.get("demo"))
self.assertEqual(len(history_list), 5)
# Change just the amount
b1.amount = 5
models.db.session.commit()
history_list = history.get_history(models.Project.query.get("demo"))
for entry in history_list:
if "prop_changed" in entry:
self.assertNotIn("owers", entry["prop_changed"])
self.assertEqual(len(history_list), 6)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,265 @@
import io
import os
import smtplib
import socket
import unittest
from unittest.mock import MagicMock, patch
from sqlalchemy import orm
from ihatemoney import models
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.manage import DeleteProject, GenerateConfig, GeneratePasswordHash
from ihatemoney.run import load_configuration
from ihatemoney.tests.common.ihatemoney_testcase import BaseTestCase, IhatemoneyTestCase
# Unset configuration file env var if previously set
os.environ.pop("IHATEMONEY_SETTINGS_FILE_PATH", None)
__HERE__ = os.path.dirname(os.path.abspath(__file__))
class ConfigurationTestCase(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:////tmp/ihatemoney.db"
)
self.assertFalse(self.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"])
self.assertEqual(
self.app.config["MAIL_DEFAULT_SENDER"],
("Budget manager", "budget@notmyidea.org"),
)
def test_env_var_configuration_file(self):
"""Test that settings are loaded from the specified configuration file"""
os.environ["IHATEMONEY_SETTINGS_FILE_PATH"] = os.path.join(
__HERE__, "ihatemoney_envvar.cfg"
)
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "lalatra")
# Test that the specified configuration file is loaded
# even if the default configuration file ihatemoney.cfg exists
os.environ["IHATEMONEY_SETTINGS_FILE_PATH"] = os.path.join(
__HERE__, "ihatemoney_envvar.cfg"
)
self.app.config.root_path = __HERE__
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "lalatra")
os.environ.pop("IHATEMONEY_SETTINGS_FILE_PATH", None)
def test_default_configuration_file(self):
"""Test that settings are loaded from the default configuration file"""
self.app.config.root_path = __HERE__
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "supersecret")
class ServerTestCase(IhatemoneyTestCase):
def test_homepage(self):
# See https://github.com/spiral-project/ihatemoney/pull/358
self.app.config["APPLICATION_ROOT"] = "/"
req = self.client.get("/")
self.assertStatus(200, req)
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)
class CommandTestCase(BaseTestCase):
def test_generate_config(self):
"""Simply checks that all config file generation
- raise no exception
- produce something non-empty
"""
cmd = GenerateConfig()
for config_file in cmd.get_options()[0].kwargs["choices"]:
with patch("sys.stdout", new=io.StringIO()) as stdout:
cmd.run(config_file)
print(stdout.getvalue())
self.assertNotEqual(len(stdout.getvalue().strip()), 0)
def test_generate_password_hash(self):
cmd = GeneratePasswordHash()
with patch("sys.stdout", new=io.StringIO()) as stdout, patch(
"getpass.getpass", new=lambda prompt: "secret"
): # NOQA
cmd.run()
print(stdout.getvalue())
self.assertEqual(len(stdout.getvalue().strip()), 189)
def test_demo_project_deletion(self):
self.create_project("demo")
self.assertEquals(models.Project.query.get("demo").name, "demo")
cmd = DeleteProject()
cmd.run("demo")
self.assertEqual(len(models.Project.query.all()), 0)
class ModelsTestCase(IhatemoneyTestCase):
def test_bill_pay_each(self):
self.post_project("raclette")
# add members
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 member with a balance=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 = models.Project.query.get_by_name(name="raclette")
zorglub = models.Person.query.get_by_name(name="zorglub", project=project)
zorglub_bills = models.Bill.query.options(
orm.subqueryload(models.Bill.owers)
).filter(models.Bill.owers.contains(zorglub))
for bill in zorglub_bills.all():
if bill.what == "red wine":
pay_each_expected = 20 / 2
self.assertEqual(bill.pay_each(), pay_each_expected)
if bill.what == "fromage à raclette":
pay_each_expected = 10 / 4
self.assertEqual(bill.pay_each(), pay_each_expected)
if bill.what == "delicatessen":
pay_each_expected = 10 / 3
self.assertEqual(bill.pay_each(), pay_each_expected)
class EmailFailureTestCase(IhatemoneyTestCase):
def test_creation_email_failure_smtp(self):
self.login("raclette")
with patch.object(
self.app.mail, "send", MagicMock(side_effect=smtplib.SMTPException)
):
resp = self.post_project("raclette")
# Check that an error message is displayed
self.assertIn(
"We tried to send you an reminder email, but there was an error",
resp.data.decode("utf-8"),
)
# Check that we were redirected to the home page anyway
self.assertIn(
'You probably want to <a href="/raclette/members/add"',
resp.data.decode("utf-8"),
)
def test_creation_email_failure_socket(self):
self.login("raclette")
with patch.object(self.app.mail, "send", MagicMock(side_effect=socket.error)):
resp = self.post_project("raclette")
# Check that an error message is displayed
self.assertIn(
"We tried to send you an reminder email, but there was an error",
resp.data.decode("utf-8"),
)
# Check that we were redirected to the home page anyway
self.assertIn(
'You probably want to <a href="/raclette/members/add"',
resp.data.decode("utf-8"),
)
def test_password_reset_email_failure(self):
self.create_project("raclette")
for exception in (smtplib.SMTPException, socket.error):
with patch.object(self.app.mail, "send", MagicMock(side_effect=exception)):
resp = self.client.post(
"/password-reminder", data={"id": "raclette"}, follow_redirects=True
)
# Check that an error message is displayed
self.assertIn(
"there was an error while sending you an email",
resp.data.decode("utf-8"),
)
# Check that we were not redirected to the success page
self.assertNotIn(
"A link to reset your password has been sent to you",
resp.data.decode("utf-8"),
)
def test_invitation_email_failure(self):
self.login("raclette")
self.post_project("raclette")
for exception in (smtplib.SMTPException, socket.error):
with patch.object(self.app.mail, "send", MagicMock(side_effect=exception)):
resp = self.client.post(
"/raclette/invite",
data={"emails": "toto@notmyidea.org"},
follow_redirects=True,
)
# Check that an error message is displayed
self.assertIn(
"there was an error while trying to send the invitation emails",
resp.data.decode("utf-8"),
)
# Check that we are still on the same page (no redirection)
self.assertIn(
"Invite people to join this project", resp.data.decode("utf-8")
)
class TestCurrencyConverter(unittest.TestCase):
converter = CurrencyConverter()
mock_data = {"USD": 1, "EUR": 0.8115}
converter.get_rates = MagicMock(return_value=mock_data)
def test_only_one_instance(self):
one = id(CurrencyConverter())
two = id(CurrencyConverter())
self.assertEqual(one, two)
def test_get_currencies(self):
self.assertCountEqual(self.converter.get_currencies(), ["USD", "EUR"])
def test_exchange_currency(self):
result = self.converter.exchange_currency(100, "USD", "EUR")
self.assertEqual(result, 81.15)
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ skip_missing_interpreters = True
commands =
python --version
py.test --pyargs ihatemoney.tests.tests
py.test --pyargs ihatemoney.tests
deps =
-e.[dev]