mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
parent
e0bc285c92
commit
18068d76ca
8 changed files with 3139 additions and 3107 deletions
748
ihatemoney/tests/api_test.py
Normal file
748
ihatemoney/tests/api_test.py
Normal 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()
|
1451
ihatemoney/tests/budget_test.py
Normal file
1451
ihatemoney/tests/budget_test.py
Normal file
File diff suppressed because it is too large
Load diff
5
ihatemoney/tests/common/help_functions.py
Normal file
5
ihatemoney/tests/common/help_functions.py
Normal 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
|
70
ihatemoney/tests/common/ihatemoney_testcase.py
Normal file
70
ihatemoney/tests/common/ihatemoney_testcase.py
Normal 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}",
|
||||
)
|
599
ihatemoney/tests/history_test.py
Normal file
599
ihatemoney/tests/history_test.py
Normal 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()
|
265
ihatemoney/tests/main_test.py
Normal file
265
ihatemoney/tests/main_test.py
Normal 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
2
tox.ini
2
tox.ini
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue