diff options
Diffstat (limited to 'ihatemoney/tests')
| -rw-r--r-- | ihatemoney/tests/tests.py | 648 |
1 files changed, 647 insertions, 1 deletions
diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py index c4b1585..5dff64d 100644 --- a/ihatemoney/tests/tests.py +++ b/ihatemoney/tests/tests.py @@ -17,7 +17,8 @@ from flask_testing import TestCase from ihatemoney.run import create_app, db, load_configuration from ihatemoney.manage import GenerateConfig, GeneratePasswordHash, DeleteProject -from ihatemoney import models +from ihatemoney import models, history +from ihatemoney.versioning import LoggingMode from ihatemoney import utils from sqlalchemy import orm @@ -843,6 +844,7 @@ class BudgetTestCase(IhatemoneyTestCase): "name": "Super raclette party!", "contact_email": "alexis@notmyidea.org", "password": "didoudida", + "logging_preference": LoggingMode.ENABLED.value, } resp = self.client.post("/raclette/edit", data=new_data, follow_redirects=True) @@ -1517,6 +1519,7 @@ class APITestCase(IhatemoneyTestCase): "name": "raclette", "contact_email": "raclette@notmyidea.org", "id": "raclette", + "logging_preference": 1, } decoded_resp = json.loads(resp.data.decode("utf-8")) self.assertDictEqual(decoded_resp, expected) @@ -1528,6 +1531,7 @@ class APITestCase(IhatemoneyTestCase): "contact_email": "yeah@notmyidea.org", "password": "raclette", "name": "The raclette party", + "project_history": "y", }, headers=self.get_auth("raclette"), ) @@ -1544,6 +1548,7 @@ class APITestCase(IhatemoneyTestCase): "contact_email": "yeah@notmyidea.org", "members": [], "id": "raclette", + "logging_preference": 1, } decoded_resp = json.loads(resp.data.decode("utf-8")) self.assertDictEqual(decoded_resp, expected) @@ -2104,12 +2109,32 @@ class APITestCase(IhatemoneyTestCase): "contact_email": "raclette@notmyidea.org", "id": "raclette", "name": "raclette", + "logging_preference": 1, } 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", "alexis") + + resp = self.client.get("/raclette/history", follow_redirects=True) + self.assertEqual(resp.status_code, 200) + self.assertIn( + "Person %s added" % em_surround("alexis"), resp.data.decode("utf-8") + ) + self.assertIn( + "Project %s added" % em_surround("raclette"), 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")) + class ServerTestCase(IhatemoneyTestCase): def test_homepage(self): @@ -2224,5 +2249,626 @@ class ModelsTestCase(IhatemoneyTestCase): self.assertEqual(bill.pay_each(), pay_each_expected) +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 + + +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( + "Project %s added" % em_surround("demo"), 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", + } + + 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( + "Project %s added" % em_surround("demo"), resp.data.decode("utf-8") + ) + + def test_project_edit(self): + new_data = { + "name": "demo2", + "contact_email": "demo2@notmyidea.org", + "password": "123456", + "project_history": "y", + } + + 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( + "Project %s added" % em_surround("demo"), resp.data.decode("utf-8") + ) + self.assertIn( + "Project contact email changed to %s" % em_surround("demo2@notmyidea.org"), + resp.data.decode("utf-8"), + ) + self.assertIn( + "Project private code changed", resp.data.decode("utf-8"), + ) + self.assertIn( + "Project renamed to %s" % 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", + } + + # 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": "alexis"}, 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( + "/demo/edit/%i" % 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("/demo/delete/%i" % bill_id, follow_redirects=True) + self.assertEqual(resp.status_code, 200) + + # delete user using POST method + resp = self.client.post( + "/demo/members/%i/delete" % user_id, 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": "alexis"}, follow_redirects=True + ) + self.assertEqual(resp.status_code, 200) + + resp = self.client.get("/demo/history") + self.assertEqual(resp.status_code, 200) + self.assertIn( + "Person %s added" % em_surround("alexis"), 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( + "Bill %s added" % em_surround("25.0 for fromage à raclette"), + 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( + "Bill %s added" % em_surround("25.0 for fromage à raclette"), + 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("25.0 for 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("25.0 for fromage à raclette"), em_surround("new thing"),), + resp.data.decode("utf-8"), + ) + self.assertLess( + resp.data.decode("utf-8").index( + "Bill %s renamed to" % em_surround("25.0 for fromage à raclette") + ), + 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( + "Bill %s removed" % em_surround("10.0 for new thing"), + 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"Person %s:\s* Weight changed\s* from %s\s* to %s" + % ( + em_surround("alexis", regex_escape=True), + em_surround("1.0", regex_escape=True), + em_surround("2.0", regex_escape=True), + ), + ) + self.assertIn( + "Person %s renamed to %s" + % (em_surround("alexis"), em_surround("new name"),), + resp.data.decode("utf-8"), + ) + self.assertLess( + resp.data.decode("utf-8").index( + "Person %s renamed" % em_surround("alexis") + ), + 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( + "Person %s removed" % em_surround("new name"), 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:\s* Amount changed\s* from %s\s* to %s" + % ( + em_surround("25.0 for 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\s* and\s* %s\s* from\s* owers list" + % ( + 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( + "Bill %s added" % em_surround("25.0 for Bill 1"), resp.data.decode("utf-8") + ) + self.assertIn( + "Bill %s removed" % em_surround("25.0 for Bill 1"), + 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( + "Bill %s added" % em_surround("25.0 for Bill 1"), resp.data.decode("utf-8") + ) + self.assertEqual( + resp.data.decode("utf-8").count( + "Bill %s added" % em_surround("25.0 for Bill 1") + ), + 1, + ) + self.assertIn( + "Bill %s added" % em_surround("20.0 for Bill 2"), resp.data.decode("utf-8") + ) + self.assertIn( + "Bill %s removed" % em_surround("25.0 for Bill 1"), + 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() |
