#!/usr/bin/env python3 # Copyright (c) 2020-2023 Florian Fischer. All rights reserved. # # This file is part of geldschieberbot. # # geldschieberbot is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # # geldschieberbot is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # geldschieberbot found in the LICENSE file. If not, # see . """System tests for the geldschieberbot""" from datetime import datetime, date, timedelta import json import os from shutil import copyfile from string import Template import subprocess import unittest from geldschieberbot import Geldschieberbot, GeldschieberbotJSONEncoder alice, bob, charlie = "alice", "bob", "charlie" num = {alice: "+49123456", bob: "+49654321", charlie: "+49615243"} DEFAULT_STATE_FILE = 'test/state.json' DEFAULT_GROUP_ID = 'test' os.environ["GSB_GROUP_ID"] = DEFAULT_GROUP_ID os.environ["GSB_STATE_FILE"] = DEFAULT_STATE_FILE os.environ["GSB_SEND_CMD"] = "cat" now = datetime.now().date() msg_template = Template(""" {"envelope": {"source":"$sender", "sourceDevice":2, "relay":null, "timestamp":1544101248419, "isReceipt":false, "dataMessage": {"timestamp":1544101248419, "message":"$msg", "expiresInSeconds":0, "attachments":[], "groupInfo": {"groupId":"test", "members":null, "name":null, "type":"DELIVER"} }, "syncMessage":null, "callMessage":null} }""".replace("\n", "")) scheduled_state_template = Template(""" {"balance": {"alice": {"bob": -100, "charlie": -100}, "bob": {"alice": 100, "charlie": 0}, "charlie": {"alice": 100, "bob": 0}}, "name2num": {"alice": "+49123456", "bob": "+49654321", "charlie": "+49615243"}, "num2name": {"+49123456": "alice", "+49654321": "bob", "+49615243": "charlie"}, "cars": {}, "scheduled_cmds": {"stuff": {"schedule": "$schedule", "last_time": "$last_time", "sender": "alice", "cmd": ["split", "3", "bob", "charlie"]}}, "changes": {"alice": [], "bob": [], "charlie": []}}""") def _run_bot(sender, cmd): msg = msg_template.substitute(sender=sender, msg=cmd).replace("\n", "\\n") + "\n" res = subprocess.run( ["python3", "./single_shot.py", "--no-quote"], text=True, capture_output=True, check=False, # res = subprocess.run(["python3", "./bot.py"], text=True, capture_output=True, input=msg) if res.returncode != 0: print(res.stdout) print(res.stderr) return res def run_bot(test, sender, cmd): """Run the bot and check its status and error messages""" res = _run_bot(sender, cmd) test.assertEqual(res.returncode, 0) test.assertEqual(res.stderr, "") return res def save_state(dest): """Copy the used state file to a different location""" copyfile(os.environ["GSB_STATE_FILE"], dest) def reset_state(state=None): """Reset or delete the used state file""" if state: copyfile(state, os.environ["GSB_STATE_FILE"]) else: state = os.environ["GSB_STATE_FILE"] if os.path.isfile(state): os.remove(state) def reset_state_string(string): """Reset the used state to a string""" with open(os.environ["GSB_STATE_FILE"], "w", encoding='utf-8') as state_file: json.dump(json.loads(string), state_file) def compare_state(expected_state_path, state=None, state_path=os.environ["GSB_STATE_FILE"]) -> bool: """Compare to states""" state_str = '' if state is None: with open(state_path, "r", encoding='utf-8') as state_file: state_str = state_file.read() else: state_str = json.dumps(state, cls=GeldschieberbotJSONEncoder) with open(expected_state_path, "r", encoding='utf-8') as exp_state_file: expected_state_str = exp_state_file.read() return expected_state_str == state_str # pragma pylint: disable=missing-function-docstring,missing-class-docstring,invalid-name class TestRegCmd(unittest.TestCase): def setUp(self): reset_state() def test_correct_reg(self): res = run_bot(self, num[alice], "!reg " + alice) self.assertEqual(res.stdout, f'Happy geldschiebing {alice}!') def test_double_reg(self): res = run_bot(self, num[alice], "!reg " + alice) self.assertEqual(res.stdout, f'Happy geldschiebing {alice}!') res = run_bot(self, num[alice], "!reg " + alice) self.assertEqual(res.stdout, 'ERROR: ' + alice + ' already registered') def test_invalid_reg(self): res = run_bot(self, num[alice], "!reg nase 03") self.assertEqual(res.stdout, 'ERROR: not in form "!reg name"') def test_numerical_reg(self): res = run_bot(self, num[alice], "!reg 9") self.assertEqual(res.stdout, 'ERROR: pure numerical names are not allowed') res = run_bot(self, num[alice], "!reg 9.7") self.assertEqual(res.stdout, 'ERROR: pure numerical names are not allowed') def test_additional_reg(self): res = run_bot(self, num[alice], "!reg " + alice) self.assertEqual(res.stdout, f'Happy geldschiebing {alice}!') res = run_bot(self, num[bob], "!reg " + bob) self.assertEqual(res.stdout, f'Happy geldschiebing {bob}!') res = run_bot(self, num[charlie], "!reg " + charlie) self.assertEqual(res.stdout, f'Happy geldschiebing {charlie}!') self.assertTrue(compare_state("test/state_3users.json")) class TestTransactionCmd(unittest.TestCase): @classmethod def tearDownClass(cls): reset_state() def setUp(self): reset_state("test/state_3users.json") def test_unknown_recipient(self): res = run_bot(self, num[alice], "!schieb 10 horst") self.assertEqual(res.stdout, 'ERROR: recipient not known') res = run_bot(self, num[alice], "!schieb horst 10") self.assertEqual(res.stdout, 'ERROR: recipient not known') def test_correct_schieb(self): res = run_bot(self, num[alice], "!schieb 10 " + bob) self.assertEqual(res.stdout, f'New Balance: {alice} <- 10.00 {bob}\n') res = run_bot(self, num[bob], "!schieb 10 " + alice) self.assertEqual(res.stdout, f'New Balance: {bob} <- 0.00 {alice}\n') def test_correct_gib(self): res = run_bot(self, num[alice], "!gib 10 " + bob) self.assertEqual(res.stdout, f'New Balance: {alice} <- 10.00 {bob}\n') res = run_bot(self, num[bob], "!gib 10 " + alice) self.assertEqual(res.stdout, f'New Balance: {bob} <- 0.00 {alice}\n') def test_correct_amount(self): res = run_bot(self, num[bob], "!schieb 1.1 " + alice) self.assertEqual(res.stdout, f'New Balance: {bob} <- 1.10 {alice}\n') res = run_bot(self, num[bob], "!gib 1,1 " + alice) self.assertEqual(res.stdout, f'New Balance: {bob} <- 2.20 {alice}\n') def test_invalid_amount(self): res = run_bot(self, num[bob], "!schieb 1b1 " + alice) self.assertEqual(res.stdout, 'ERROR: amount must be a positive number') res = run_bot(self, num[bob], "!schieb ä€ " + alice) self.assertEqual(res.stdout, 'ERROR: amount must be a positive number') def test_correct_schieb_name_before_amount(self): res = run_bot(self, num[alice], "!schieb " + bob + " 10") self.assertEqual(res.stdout, f'New Balance: {alice} <- 10.00 {bob}\n') res = run_bot(self, num[bob], "!schieb " + alice + " 10") self.assertEqual(res.stdout, f'New Balance: {bob} <- 0.00 {alice}\n') def test_correct_gib_name_before_amount(self): res = run_bot(self, num[alice], "!gib " + charlie + " 10") self.assertEqual(res.stdout, f'New Balance: {alice} <- 10.00 {charlie}\n') res = run_bot(self, num[charlie], "!gib " + alice + " 10") self.assertEqual(res.stdout, f'New Balance: {charlie} <- 0.00 {alice}\n') def test_correct_nimm(self): res = run_bot(self, num[alice], "!nimm 10 " + bob) self.assertEqual(res.stdout, f'New Balance: {alice} -> 10.00 {bob}\n') res = run_bot(self, num[bob], "!nimm 10 " + alice) self.assertEqual(res.stdout, f'New Balance: {bob} <- 0.00 {alice}\n') def test_correct_zieh_name_before_amount(self): res = run_bot(self, num[alice], "!zieh " + charlie + " 10") self.assertEqual(res.stdout, f'New Balance: {alice} -> 10.00 {charlie}\n') res = run_bot(self, num[charlie], "!zieh " + alice + " 10") self.assertEqual(res.stdout, f'New Balance: {charlie} <- 0.00 {alice}\n') def test_transactions_complex(self): res = run_bot(self, num[alice], "!schieb " + charlie + " 1,1") self.assertEqual(res.stdout, f'New Balance: {alice} <- 1.10 {charlie}\n') res = run_bot(self, num[alice], "!zieh " + charlie + " 2.1") self.assertEqual(res.stdout, f'New Balance: {alice} -> 1.00 {charlie}\n') res = run_bot(self, num[charlie], "!schieb " + bob + " 42") self.assertEqual(res.stdout, f'New Balance: {charlie} <- 42.00 {bob}\n') res = run_bot(self, num[alice], "!zieh " + bob + " 0.01") self.assertEqual(res.stdout, f'New Balance: {alice} -> 0.01 {bob}\n') compare_state("test/state_transactions1.json") def test_transactions_with_myself(self): res = run_bot(self, num[alice], f"!schieb {alice} 1,1") self.assertEqual( res.stdout, 'ERROR: you can not transfer money to or from yourself') res = run_bot(self, num[alice], f"!zieh {alice} 2.1") self.assertEqual( res.stdout, 'ERROR: you can not transfer money to or from yourself') def test_transactions_with_multiple_recipients(self): for cmds in [('schieb', 'zieh'), ('gib', 'nimm')]: res = run_bot(self, num[charlie], f'!{cmds[0]} 5 {alice} {bob}') self.assertEqual(res.stdout, \ """-> alice 5.00 -> bob 5.00 New Balance: charlie: \t<- alice 5.00 \t<- bob 5.00 \tBalance: 10.00""") res = run_bot(self, num[charlie], f'!{cmds[1]} 5 {alice} {bob}') self.assertEqual(res.stdout, \ """<- alice 5.00 <- bob 5.00 New Balance: charlie: \tAll fine :)""") def test_invalid_transactions_with_multiple_recipients(self): res = run_bot(self, num[charlie], f'!schieb {alice} 5 {bob}') self.assertEqual(res.stdout, 'ERROR: amount must be a positive number') res = run_bot(self, num[charlie], f'!zieh 5 5 {bob}') self.assertEqual(res.stdout, 'ERROR: recipient "5" not known') class TestSumCmd(unittest.TestCase): def test_summary_single_user(self): reset_state("test/state_transactions1.json") res = run_bot(self, num[alice], "!sum " + alice) self.assertEqual( res.stdout, 'Summary:\nalice:\n\t-> bob 0.01\n\t-> charlie 1.00\n\tBalance: -1.01\n' ) def test_summary_invalide_single_user(self): reset_state() res = run_bot(self, num[alice], "!sum " + alice) self.assertEqual(res.stdout, 'ERROR: name "alice" not registered') def test_summary_double_user(self): reset_state("test/state_transactions1.json") res = run_bot(self, num[alice], f"!sum {alice} {bob}") summary = \ """Summary: alice: \t-> bob 0.01 \t-> charlie 1.00 \tBalance: -1.01 bob: \t<- alice 0.01 \t-> charlie 42.00 \tBalance: -41.99 """ self.assertEqual(res.stdout, summary) def test_summary(self): reset_state("test/state_transactions1.json") self.assertEqual( run_bot(self, num[alice], "!sum").stdout, 'Summary:\nalice:\n\t-> bob 0.01\n\t-> charlie 1.00\n\tBalance: -1.01' ) self.assertEqual( run_bot(self, num[alice], "!summary").stdout, 'Summary:\nalice:\n\t-> bob 0.01\n\t-> charlie 1.00\n\tBalance: -1.01' ) self.assertEqual( run_bot(self, num[alice], "!balance").stdout, 'Summary:\nalice:\n\t-> bob 0.01\n\t-> charlie 1.00\n\tBalance: -1.01' ) def test_full_summary(self): reset_state("test/state_transactions1.json") res = run_bot(self, num[alice], "!full-sum") summary = \ """Summary: alice: \t-> bob 0.01 \t-> charlie 1.00 \tBalance: -1.01 bob: \t<- alice 0.01 \t-> charlie 42.00 \tBalance: -41.99 charlie: \t<- alice 1.00 \t<- bob 42.00 \tBalance: 43.00""" self.assertEqual(res.stdout, summary) def test_full_summary_with_args(self): reset_state("test/state_transactions1.json") res = run_bot(self, num[alice], "!full-sum foo") self.assertEqual(res.stdout, 'ERROR: full-sum takes no arguments') class TestMisc(unittest.TestCase): def test_unknown_command(self): res = run_bot(self, num[alice], "!foo") self.assertEqual( res.stdout, "ERROR: unknown cmd. Enter !help for a list of commands.") def test_no_command(self): res = run_bot(self, num[alice], "Hi, how are you?") self.assertEqual(res.stdout, "") class TestListCmd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_ls(self): res = run_bot(self, num[alice], "!ls") msg = f"alice: {num[alice]}\nbob: {num[bob]}\ncharlie: {num[charlie]}\n" self.assertEqual(res.stdout, msg) def test_list(self): res = run_bot(self, num[bob], "!list") msg = f"alice: {num[alice]}\nbob: {num[bob]}\ncharlie: {num[charlie]}\n" self.assertEqual(res.stdout, msg) class TestSplitCmd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_split_unregistered(self): res = run_bot(self, "+4971576357", "!split") self.assertEqual(res.stdout, 'ERROR: you must register first') def test_split_invalid_args(self): res = run_bot(self, num[alice], "!split") self.assertEqual(res.stdout, 'ERROR: not in form "!split amount [name]+"') res = run_bot(self, num[alice], "!split 10") self.assertEqual(res.stdout, 'ERROR: not in form "!split amount [name]+"') def test_split_one_unknown_user(self): res = run_bot(self, num[alice], "!split 30 " + "foo" + " " + charlie) msg = \ """Split 30.00 between 3 -> 10.00 each foo not known. Please take care manually New Balance: alice: \t<- charlie 10.00 \tBalance: 10.00""" self.assertEqual(res.stdout, msg) def test_split_with_sender_in_user_list(self): res = run_bot(self, num[alice], f"!split 30 {charlie} {alice}") msg = \ """Split 30.00 between 3 -> 10.00 each alice, you will be charged multiple times. This may not be what you want New Balance: alice: \t<- charlie 10.00 \tBalance: 10.00""" self.assertEqual(res.stdout, msg) def test_split_with_double_user(self): res = run_bot(self, num[alice], f"!split 30 {charlie} {charlie}") msg = \ """Split 30.00 between 3 -> 10.00 each New Balance: alice: \t<- charlie 20.00 \tBalance: 20.00""" self.assertEqual(res.stdout, msg) def test_split(self): res = run_bot(self, num[alice], "!split 30 " + bob + " " + charlie) msg = \ """Split 30.00 between 3 -> 10.00 each New Balance: alice: \t<- bob 10.00 \t<- charlie 10.00 \tBalance: 20.00""" self.assertEqual(res.stdout, msg) def test_split_single_user_amount_swap(self): res = run_bot(self, num[alice], f"!split {bob} 10") msg = \ """Split 10.00 between 2 -> 5.00 each New Balance: alice: \t<- bob 5.00 \tBalance: 5.00""" self.assertEqual(res.stdout, msg) def test_split_whitespace(self): res = run_bot(self, num[alice], "!split 30 " + bob + " " + charlie + " ") msg = \ """Split 30.00 between 3 -> 10.00 each New Balance: alice: \t<- bob 10.00 \t<- charlie 10.00 \tBalance: 20.00""" self.assertEqual(res.stdout, msg) class TestAliasAdd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_add_success(self): res = run_bot(self, num[alice], f"!alias foo {alice} {bob}") self.assertEqual(res.stdout, 'New alias "foo" registered') res = run_bot(self, num[alice], f"!alias bar {charlie}") self.assertEqual(res.stdout, 'New alias "bar" registered') def test_add_invalid_aliases(self): res = run_bot(self, num[alice], f"!alias foo {alice} {bob}") self.assertEqual(res.stdout, 'New alias "foo" registered') res = run_bot(self, num[alice], f"!alias foo {alice} {bob}") self.assertEqual(res.stdout, 'ERROR: Alias "foo" is already registered') res = run_bot(self, num[alice], f"!alias {alice} {alice} {bob}") self.assertEqual(res.stdout, f'ERROR: A user "{alice}" is already registered') res = run_bot(self, num[alice], f"!alias 13.23 {charlie}") self.assertEqual(res.stdout, 'ERROR: Pure numerical aliases are not allowed') res = run_bot(self, num[alice], "!alias bar ingo") self.assertEqual(res.stdout, 'ERROR: User ingo is not registered') res = run_bot(self, num[alice], f"!alias all {alice}") self.assertEqual(res.stdout, 'ERROR: Alias "all" is already registered') class TestAlias(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_list_aliases_empty_state(self): res = run_bot(self, num[alice], "!alias") self.assertEqual(res.stdout, f"""Aliases: \tall: {alice} {bob} {charlie}""") def test_list_aliases(self): run_bot(self, num[alice], f"!alias alob {alice} {bob}") res = run_bot(self, num[alice], "!alias") self.assertEqual( res.stdout, f"""Aliases: \tall: {alice} {bob} {charlie} \talob: {alice} {bob}""") def test_add_invalid_aliases(self): run_bot(self, num[alice], f"!alias foo {alice} {bob}") res = run_bot(self, num[alice], f"!alias foo {alice} {bob}") self.assertEqual(res.stdout, 'ERROR: Alias "foo" is already registered') res = run_bot(self, num[alice], f"!alias {alice} {alice} {bob}") self.assertEqual(res.stdout, f'ERROR: A user "{alice}" is already registered') res = run_bot(self, num[alice], f"!alias 13.23 {charlie}") self.assertEqual(res.stdout, 'ERROR: Pure numerical aliases are not allowed') res = run_bot(self, num[alice], "!alias bar ingo") self.assertEqual(res.stdout, 'ERROR: User ingo is not registered') res = run_bot(self, num[alice], f"!alias all {alice}") self.assertEqual(res.stdout, 'ERROR: Alias "all" is already registered') class TestAliasUsage(unittest.TestCase): def setUp(self): reset_state("test/state_3users_1alias.json") def test_schieb_alias(self): expected = \ """-> alice 5.00 -> bob 5.00 New Balance: charlie: \t<- alice 5.00 \t<- bob 5.00 \tBalance: 10.00""" for cmd in ['gib', 'schieb']: for args in ['5 alob', 'alob 5']: res = run_bot(self, num[charlie], f'!{cmd} {args}') self.assertEqual(res.stdout, expected) self.setUp() def test_zieh_alias(self): expected = \ """<- alice 5.00 <- bob 5.00 New Balance: charlie: \t-> alice 5.00 \t-> bob 5.00 \tBalance: -10.00""" for cmd in ['nimm', 'zieh']: for args in ['5 alob', 'alob 5']: res = run_bot(self, num[charlie], f'!{cmd} {args}') self.assertEqual(res.stdout, expected) self.setUp() def test_split_all(self): res = run_bot(self, num[alice], "!split 9 all") self.assertEqual(res.stdout, \ """Split 9.00 between 3 -> 3.00 each New Balance: alice: \t<- bob 3.00 \t<- charlie 3.00 \tBalance: 6.00""") def test_split_alias(self): res = run_bot(self, num[charlie], "!split 9 alob") self.assertEqual(res.stdout, \ """Split 9.00 between 3 -> 3.00 each New Balance: charlie: \t<- alice 3.00 \t<- bob 3.00 \tBalance: 6.00""") def test_tanken_all(self): res = run_bot(self, num[alice], "!tanken 10\n10 all") self.assertEqual(res.stdout, \ """alice: 10km = fuel: 3.33, service charge: 0.00 bob: 10km = fuel: 3.33, service charge: 0.00 charlie: 10km = fuel: 3.33, service charge: 0.00 New Balance: alice: \t<- bob 3.33 \t<- charlie 3.33 \tBalance: 6.66""") def test_tanken_alias(self): res = run_bot(self, num[charlie], "!tanken 8\n10 alob") self.assertEqual(res.stdout, \ """alice: 10km = fuel: 4.00, service charge: 0.00 bob: 10km = fuel: 4.00, service charge: 0.00 New Balance: charlie: \t<- alice 4.00 \t<- bob 4.00 \tBalance: 8.00""") class TestCarsAdd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_add_success(self): i = "!cars add foo 0.04" res = run_bot(self, num[alice], i) o = 'added "foo" as an available car' self.assertEqual(res.stdout, o) i = "!cars new bar 0.02" res = run_bot(self, num[alice], i) o = 'added "bar" as an available car' self.assertEqual(res.stdout, o) save_state("test/state_2cars.json") def test_add_invalid_service_charge(self): i = "!cars add foo 0.04hut" res = run_bot(self, num[alice], i) o = "ERROR: service-charge must be a positive number" self.assertEqual(res.stdout, o) i = "!cars new bar -5" res = run_bot(self, num[alice], i) o = "ERROR: service-charge must be a positive number" self.assertEqual(res.stdout, o) def test_add_name_conflict(self): i = "!cars add foo 0.04" res = run_bot(self, num[alice], i) o = 'added "foo" as an available car' self.assertEqual(res.stdout, o) i = "!cars new alice 0.02" res = run_bot(self, num[alice], i) o = 'ERROR: A user named "alice" already exists. Please use a different name for this car' self.assertEqual(res.stdout, o) class TestCarsTransactions(unittest.TestCase): def setUp(self): reset_state("test/state_2cars.json") def test_schieb_car(self): i = "!schieb foo 20" res = run_bot(self, num[alice], i) o = "New Balance: alice <- 20.00 foo\n" self.assertEqual(res.stdout, o) class TestCarsSum(unittest.TestCase): def setUp(self): reset_state("test/state_2cars.json") def test_sum_car(self): res = run_bot(self, num[alice], "!sum foo") o =\ """Summary: foo: \tAll fine :) """ self.assertEqual(res.stdout, o) class TestCarsRemove(unittest.TestCase): def setUp(self): reset_state("test/state_2cars.json") def test_schieb_car(self): i = "!cars remove foo" res = run_bot(self, num[alice], i) o = 'removed "foo" from the available cars' self.assertEqual(res.stdout, o) i = "!sum foo" res = run_bot(self, num[alice], i) o = 'ERROR: name "foo" not registered' self.assertEqual(res.stdout, o) class TestCarPayCmd(unittest.TestCase): def setUp(self): reset_state("test/state_2cars.json") self.maxDiff = None def test_alice_pays_exact(self): run_bot(self, num[bob], "!zieh foo 20") run_bot(self, num[charlie], "!zieh foo 10") res = run_bot(self, num[alice], "!cars pay foo 30") o = \ """alice payed 30.00 Transferring 100.00% of everybody's charges Transfer 20.00 from bob to alice Transfer 10.00 from charlie to alice New Balances: alice: \t<- bob 20.00 \t<- charlie 10.00 \tBalance: 30.00 foo: \tAll fine :)""" self.assertEqual(res.stdout, o) def test_alice_pays_more(self): run_bot(self, num[bob], "!zieh foo 20") run_bot(self, num[charlie], "!zieh foo 10") res = run_bot(self, num[alice], "!cars pay foo 40") o = \ """alice payed 40.00 Transferring 100.00% of everybody's charges Transfer 20.00 from bob to alice Transfer 10.00 from charlie to alice New Balances: alice: \t<- bob 20.00 \t<- charlie 10.00 \tBalance: 30.00 \tCars: \t<- foo 10.00 \tLiability: 10.00 foo: \t-> alice 10.00 \tBalance: -10.00""" self.assertEqual(res.stdout, o) def test_alice_pays_half(self): run_bot(self, num[bob], "!zieh foo 20") run_bot(self, num[charlie], "!zieh foo 10") i = "!cars pay foo 15" res = run_bot(self, num[alice], i) o = \ """alice payed 15.00 Transferring 50.00% of everybody's charges Transfer 10.00 from bob to alice Transfer 5.00 from charlie to alice New Balances: alice: \t<- bob 10.00 \t<- charlie 5.00 \tBalance: 15.00 foo: \t<- bob 10.00 \t<- charlie 5.00 \tBalance: 15.00""" self.assertEqual(res.stdout, o) class TestCarsListCmd(unittest.TestCase): def setUp(self): reset_state("test/state_2cars.json") def test_no_cars(self): reset_state() i = "!cars" res = run_bot(self, num[alice], i) o = "No cars registered yet." self.assertEqual(res.stdout, o) def test_implicit_call(self): i = "!cars" res = run_bot(self, num[alice], i) o = \ """foo - service charge 4ct/km foo: \tAll fine :) bar - service charge 2ct/km bar: \tAll fine :)""" self.assertEqual(res.stdout, o) def test_list_all(self): i = "!cars ls" res = run_bot(self, num[alice], i) o = \ """foo - service charge 4ct/km foo: \tAll fine :) bar - service charge 2ct/km bar: \tAll fine :)""" self.assertEqual(res.stdout, o) i = "!cars list" res = run_bot(self, num[alice], i) o = \ """foo - service charge 4ct/km foo: \tAll fine :) bar - service charge 2ct/km bar: \tAll fine :)""" self.assertEqual(res.stdout, o) def test_list_explicit_car(self): i = "!cars ls foo" res = run_bot(self, num[alice], i) o = \ """foo - service charge 4ct/km foo: \tAll fine :)""" self.assertEqual(res.stdout, o) def test_list_invalid_explicit_car(self): i = "!cars ls alice" res = run_bot(self, num[alice], i) o = 'ERROR: "alice" is no available car\n' self.assertEqual(res.stdout, o) class TestTankenCmd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_tanken_3users(self): i = \ """!tanken 10 alice 20 alice bob 10 charlie""" res = run_bot(self, num[alice], i) o = \ """alice: 20km = fuel: 3.33, service charge: 0.00 bob: 20km = fuel: 3.33, service charge: 0.00 charlie: 10km = fuel: 3.33, service charge: 0.00 New Balance: alice: \t<- bob 3.33 \t<- charlie 3.33 \tBalance: 6.66""" self.assertEqual(res.stdout, o) def test_tanken_unknown_user(self): i = \ """!tanken 10 alice 20 alice dieter 10 charlie""" res = run_bot(self, num[alice], i) o = \ """alice: 20km = fuel: 3.33, service charge: 0.00 dieter: 20km = fuel: 3.33, service charge: 0.00 dieter not known. Please collect fuel cost manually charlie: 10km = fuel: 3.33, service charge: 0.00 New Balance: alice: \t<- charlie 3.33 \tBalance: 3.33""" self.assertEqual(res.stdout, o) def test_tanken_3users_with_car(self): i = "!cars add foo 0.04" res = run_bot(self, num[alice], i) o = 'added "foo" as an available car' self.assertEqual(res.stdout, o) i = \ """!tanken 10 alice foo 20 alice bob 10 charlie""" res = run_bot(self, num[alice], i) o = \ """alice: 20km = fuel: 3.33, service charge: 0.40 bob: 20km = fuel: 3.33, service charge: 0.40 charlie: 10km = fuel: 3.33, service charge: 0.40 New Balance: alice: \t<- bob 3.33 \t<- charlie 3.33 \tBalance: 6.66 \tCars: \t-> foo 0.40 \tLiability: -0.40 Car foo: \t<- alice 0.40 \t<- bob 0.40 \t<- charlie 0.40 \tBalance: 1.20""" self.assertEqual(res.stdout, o) def test_tanken_unknown_user_with_car(self): i = "!cars add foo 0.04" res = run_bot(self, num[alice], i) o = 'added "foo" as an available car' self.assertEqual(res.stdout, o) i = \ """!tanken 10 alice foo 20 alice bob 10 dieter""" res = run_bot(self, num[alice], i) o = \ """alice: 20km = fuel: 3.33, service charge: 0.40 bob: 20km = fuel: 3.33, service charge: 0.40 dieter: 10km = fuel: 3.33, service charge: 0.40 dieter not known. alice held accountable for service charge. Please collect fuel cost manually New Balance: alice: \t<- bob 3.33 \tBalance: 3.33 \tCars: \t-> foo 0.80 \tLiability: -0.80 Car foo: \t<- alice 0.80 \t<- bob 0.40 \tBalance: 1.20""" self.assertEqual(res.stdout, o) class TestTransferCmd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_transfer(self): i = '!transfer 5 bob charlie' res = run_bot(self, num[alice], i) o = \ """New Balance: alice -> 5.00 bob New Balance: alice <- 5.00 charlie New Balance: bob -> 5.00 charlie """ self.assertEqual(res.stdout, o) def test_transfer_credit(self): res = run_bot(self, num[alice], "!schieb bob 5") i = '!transfer 5 bob charlie' res = run_bot(self, num[alice], i) o = \ """New Balance: alice <- 0.00 bob New Balance: alice <- 5.00 charlie New Balance: bob -> 5.00 charlie """ self.assertEqual(res.stdout, o) def test_transfer_change_dept(self): res = run_bot(self, num[alice], "!schieb bob 5") i = '!transfer 5 charlie alice' res = run_bot(self, num[bob], i) o = \ """New Balance: bob -> 5.00 charlie New Balance: bob <- 0.00 alice New Balance: charlie -> 5.00 alice """ self.assertEqual(res.stdout, o) #TODO: tanken, transfer, cars pay class TestListChangesCmd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_list_changes_unregistered(self): res = run_bot(self, "+4971576357", "!list-changes") self.assertEqual(res.stdout, 'ERROR: you must register first') def test_list_changes_single_change(self): run_bot(self, num[alice], f"!schieb {bob} 10") res = run_bot(self, num[alice], "!list-changes") msg = \ """Changes from alice 1-1 Change 1: \t!schieb bob 10 \talice <- 10.00 bob """ self.assertEqual(res.stdout, msg) def test_list_changes_single_change_invalid_amount(self): run_bot(self, num[alice], f"!schieb {bob} 10") res = run_bot(self, num[alice], "!list-changes foo") msg = \ """ERROR: the amount of changes to list must be a number""" self.assertEqual(res.stdout, msg) def test_list_changes_single_change_invalid_offset(self): run_bot(self, num[alice], f"!schieb {bob} 10") res = run_bot(self, num[alice], "!list-changes 1 foo") msg = \ """ERROR: the first change to list must be a number""" self.assertEqual(res.stdout, msg) res = run_bot(self, num[alice], "!list-changes 1 20") msg = \ """ERROR: the first change to list is bigger than there are changes""" self.assertEqual(res.stdout, msg) def test_list_changes_two_changes(self): run_bot(self, num[alice], f"!schieb {bob} 10") run_bot(self, num[alice], f"!zieh {charlie} 5") res = run_bot(self, num[alice], "!list-changes") msg = \ """Changes from alice 1-2 Change 1: \t!schieb bob 10 \talice <- 10.00 bob Change 2: \t!zieh charlie 5 \talice -> 5.00 charlie """ self.assertEqual(res.stdout, msg) def test_list_changes_one_of_two(self): run_bot(self, num[alice], f"!schieb {bob} 10") run_bot(self, num[alice], f"!zieh {charlie} 5") res = run_bot(self, num[alice], "!list-changes 1") msg = \ """Changes from alice 2-2 Change 2: \t!zieh charlie 5 \talice -> 5.00 charlie """ self.assertEqual(res.stdout, msg) def test_list_changes_only_second(self): run_bot(self, num[alice], f"!schieb {bob} 10") run_bot(self, num[alice], f"!zieh {charlie} 5") run_bot(self, num[alice], f"!split 9 {bob} {charlie}") res = run_bot(self, num[alice], "!list-changes 1 2") msg = \ """Changes from alice 2-2 Change 2: \t!zieh charlie 5 \talice -> 5.00 charlie """ self.assertEqual(res.stdout, msg) class TestFuckCmd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_fuck_unregistered(self): res = run_bot(self, "+4971576357", "!fuck") self.assertEqual(res.stdout, 'ERROR: you must register first') def test_fuck_nothing(self): res = run_bot(self, num[alice], "!fuck") self.assertEqual(res.stdout, 'Nothing to rewind') def test_fuck_invalid_change(self): run_bot(self, num[alice], f"!schieb {bob} 10") res = run_bot(self, num[alice], "!fuck foo") msg = "ERROR: change to rewind must be a number" self.assertEqual(res.stdout, msg) res = run_bot(self, num[alice], "!fuck 20") msg = "ERROR: change to rewind is bigger than there are changes" self.assertEqual(res.stdout, msg) def test_fuck_transaction(self): for cmd in ["fuck", "undo", "rewind"]: run_bot(self, num[alice], f"!schieb {bob} 10") res = run_bot(self, num[alice], f"!{cmd}") msg = \ """alice: sorry I fucked up! Rewinding: !schieb bob 10 alice <- 10.00 bob """ self.assertEqual(res.stdout, msg) compare_state("test/state_3users.json") def test_fuck_transfer(self): run_bot(self, num[alice], f"!transfer 5 {bob} {charlie}") res = run_bot(self, num[alice], "!fuck") msg = \ """alice: sorry I fucked up! Rewinding: !transfer 5 bob charlie alice -> 5.00 bob alice <- 5.00 charlie bob -> 5.00 charlie """ self.assertEqual(res.stdout, msg) compare_state("test/state_3users.json") def test_rewind_first(self): run_bot(self, num[alice], f"!split 3 {bob} {charlie}") run_bot(self, num[alice], f"!schieb {bob} 10") run_bot(self, num[alice], f"!nimm {charlie} 10") res = run_bot(self, num[alice], "!fuck 1") msg = ""\ """alice: sorry I fucked up! Rewinding: !split 3 bob charlie alice <- 1.00 bob alice <- 1.00 charlie """ self.assertEqual(res.stdout, msg) def test_rewind_second(self): run_bot(self, num[alice], f"!split 10 {bob} {charlie}") run_bot(self, num[alice], f"!schieb {bob} 10") run_bot(self, num[alice], f"!nimm {charlie} 10") res = run_bot(self, num[alice], "!fuck 2") msg = ""\ """alice: sorry I fucked up! Rewinding: !schieb bob 10 alice <- 10.00 bob """ self.assertEqual(res.stdout, msg) def test_fuck_split(self): run_bot(self, num[alice], "!split 3 bob charlie") res = run_bot(self, num[alice], "!fuck") msg = \ """alice: sorry I fucked up! Rewinding: !split 3 bob charlie alice <- 1.00 bob alice <- 1.00 charlie """ self.assertEqual(res.stdout, msg) compare_state("test/state_3users.json") class TestScheduleCmd(unittest.TestCase): def setUp(self): reset_state("test/state_3users.json") def test_schedule_twice(self): run_bot(self, num[alice], "!weekly stuff ls") res = run_bot(self, num[alice], "!weekly stuff ls") self.assertEqual( res.stdout, 'ERROR: there is already a scheduled command named "stuff"') def test_schedule_not_available_cmd(self): res = run_bot(self, num[alice], "!weekly stuff foo") self.assertEqual( res.stdout, 'ERROR: the command "stuff" can not be registered because "foo" is unknown' ) def test_schedule_errorous_cmd(self): res = run_bot(self, num[alice], "!weekly stuff gib foo 100") self.assertEqual( res.stdout, 'ERROR: the command "stuff" failed with "recipient not known" and will not be recorded' ) def test_weekly(self): res = run_bot(self, num[alice], "!weekly stuff split 3 bob charlie") msg = \ """Recorded the weekly command "split 3 bob charlie" as "stuff" Running weekly command stuff for alice initially Split 3.00 between 3 -> 1.00 each New Balance: alice: \t<- bob 1.00 \t<- charlie 1.00 \tBalance: 2.00""" self.assertEqual(res.stdout, msg) save_state("test/state_schedule_weekly.json") res = run_bot(self, num[alice], "!fuck") msg = \ """alice: sorry I fucked up! Rewinding: split 3 bob charlie alice <- 1.00 bob alice <- 1.00 charlie Cancelled the weekly cmd "stuff\"""" self.assertEqual(res.stdout, msg) # Last exec onw week ago reset_state_string( scheduled_state_template.substitute(last_time=now - timedelta(7), schedule="weekly")) res = run_bot(self, num[alice], "!fuck") # There is no new line because the real bot uses two Messages msg = \ f"""Nothing to rewindRunning weekly command stuff for alice triggered on {now.isoformat()} Split 3.00 between 3 -> 1.00 each New Balance: alice: \t<- bob 2.00 \t<- charlie 2.00 \tBalance: 4.00""" self.assertEqual(res.stdout, msg) compare_state("test/state_schedule_weekly.json") res = run_bot(self, num[alice], "!fuck") self.assertEqual(res.stdout, 'Nothing to rewind') # Last exec two week ago reset_state_string( scheduled_state_template.substitute(last_time=now - timedelta(14), schedule="weekly")) res = run_bot(self, num[alice], "!fuck") # There is no new line because the real bot uses two Messages last_week = now - timedelta(7) msg = \ f"""Nothing to rewindRunning weekly command stuff for alice triggered on {last_week.isoformat()} Split 3.00 between 3 -> 1.00 each New Balance: alice: \t<- bob 2.00 \t<- charlie 2.00 \tBalance: 4.00Running weekly command stuff for alice triggered on {now.isoformat()} Split 3.00 between 3 -> 1.00 each New Balance: alice: \t<- bob 3.00 \t<- charlie 3.00 \tBalance: 6.00""" self.assertEqual(res.stdout, msg) os.remove("test/state_schedule_weekly.json") def test_monthly(self): self.maxDiff = None res = run_bot(self, num[alice], "!monthly stuff split 3 bob charlie") msg = \ """Recorded the monthly command "split 3 bob charlie" as "stuff" Running monthly command stuff for alice initially Split 3.00 between 3 -> 1.00 each New Balance: alice: \t<- bob 1.00 \t<- charlie 1.00 \tBalance: 2.00""" self.assertEqual(res.stdout, msg) save_state("test/state_schedule_monthly.json") res = run_bot(self, num[alice], "!fuck") msg = \ """alice: sorry I fucked up! Rewinding: split 3 bob charlie alice <- 1.00 bob alice <- 1.00 charlie Cancelled the monthly cmd "stuff\"""" self.assertEqual(res.stdout, msg) # Last exec one month ago if now.month > 1: one_month_ago = None try: one_month_ago = date(now.year, now.month - 1, now.day) except ValueError: self.skipTest( f"current day {now.day} does not exist in previous month") else: one_month_ago = date(now.year - 1, 12, now.day) reset_state_string( scheduled_state_template.substitute(last_time=one_month_ago, schedule="monthly")) res = run_bot(self, num[alice], "!fuck") # There is no new line because the real bot uses two Messages msg = \ f"""Nothing to rewindRunning monthly command stuff for alice triggered on {now.isoformat()} Split 3.00 between 3 -> 1.00 each New Balance: alice: \t<- bob 2.00 \t<- charlie 2.00 \tBalance: 4.00""" self.assertEqual(res.stdout, msg) compare_state("test/state_schedule_monthly.json") res = run_bot(self, num[alice], "!fuck") self.assertEqual(res.stdout, 'Nothing to rewind') # Last exec two month ago if now.month > 2: try: two_months_ago = date(now.year, now.month - 2, now.day) except ValueError: self.skipTest( f"current day {now.day} does not exist two months ago") else: two_months_ago = date(now.year - 1, 12 - now.month + 1, now.day) reset_state_string( scheduled_state_template.substitute(last_time=two_months_ago, schedule="monthly")) res = run_bot(self, num[alice], "!fuck") # There is no new line because the real bot uses to Messages msg = \ f"""Nothing to rewindRunning monthly command stuff for alice triggered on {one_month_ago.isoformat()} Split 3.00 between 3 -> 1.00 each New Balance: alice: \t<- bob 2.00 \t<- charlie 2.00 \tBalance: 4.00Running monthly command stuff for alice triggered on {now.isoformat()} Split 3.00 between 3 -> 1.00 each New Balance: alice: \t<- bob 3.00 \t<- charlie 3.00 \tBalance: 6.00""" self.assertEqual(res.stdout, msg) os.remove("test/state_schedule_monthly.json") def test_monthly_car(self): run_bot(self, num[alice], "!cars add fiat 0.5") res = run_bot(self, num[alice], "!monthly versicherung cars pay fiat 3") msg = \ """Recorded the monthly command "cars pay fiat 3" as "versicherung" Running monthly command versicherung for alice initially alice payed 3.00 Transferring 100.00% of everybody's charges New Balances: alice: \tAll fine :) \tCars: \t<- fiat 3.00 \tLiability: 3.00 fiat: \t-> alice 3.00 \tBalance: -3.00""" self.assertEqual(res.stdout, msg) class TestThanks(unittest.TestCase): def test_thanks(self): res = run_bot(self, num[alice], "!thanks") self.assertEqual( res.stdout, f"You are welcome. It is a pleasure to work with you, {alice}.") def test_thanks_nick(self): res = run_bot(self, num[alice], "!thanks pussy") self.assertEqual( res.stdout, f"You are welcome. It is a pleasure to work with you, {alice}.\nBut don't call me pussy." ) class TestConvertState(unittest.TestCase): def setUp(self): reset_state("test/old_changes.json") def test_thanks(self): res = run_bot(self, num[alice], "!thanks") self.assertEqual( res.stdout, f"You are welcome. It is a pleasure to work with you, {num['alice']}." ) class TestStateLoadStore(unittest.TestCase): def test_init(self): sp = "test/state_3users.json" with Geldschieberbot(sp, DEFAULT_GROUP_ID) as bot: self.assertTrue(compare_state(sp, state=bot.state)) def test_explicit_load(self): sp = "test/state_3users.json" bot = Geldschieberbot(DEFAULT_STATE_FILE, DEFAULT_GROUP_ID) bot.load_state(sp) self.assertTrue(compare_state(sp, state=bot.state)) class TestMinimize(unittest.TestCase): """Test the minimize command""" def setUp(self): reset_state("test/state_minimize.json") def test_minimize(self): res = run_bot(self, num[alice], "!minimize") # The found cycle is not deterministic yet # exp = "minimize:\nbob -> charlie -> alice 10" # self.assertEqual( res.stdout, exp ) self.assertTrue("alice" in res.stdout) self.assertTrue("bob" in res.stdout) self.assertTrue("charlie" in res.stdout) self.assertTrue("10" in res.stdout) res = run_bot(self, num[alice], "!full-sum") o =\ """Summary: alice: \tAll fine :) bob: \tAll fine :) charlie: \tAll fine :)""" self.assertEqual(res.stdout, o) if __name__ == '__main__': unittest.main()