#!/usr/bin/env python3 from datetime import datetime, date, timedelta import json import os from shutil import copyfile from string import Template import subprocess import unittest alice, bob, charlie = "alice", "bob", "charlie" num = {alice: "+49123456", bob: "+49654321", charlie: "+49615243"} os.environ["GSB_GROUP_ID"] = "test" os.environ["GSB_STATE_FILE"] = "test/state.json" os.environ["GSB_SEND_CMD"] = "cat" os.environ["GSB_SEND_GROUP"] = "cat" os.environ["GSB_SEND_USER"] = "cat" os.environ["GSB_MODULES"] = "geldschiebing.py" 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": "+49123456", "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", "./geldschieberbot.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): res = _run_bot(sender, cmd) test.assertEqual(res.returncode, 0) test.assertEqual(res.stderr, "") return res def save_state(dest): copyfile(os.environ["GSB_STATE_FILE"], dest) def reset_state(state=None): 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): with open(os.environ["GSB_STATE_FILE"], "w", encoding='utf-8') as f: json.dump(json.loads(string), f) def compare_state(comp_state): with open(comp_state, "r", encoding='utf-8') as csf, \ open(os.environ["GSB_STATE_FILE"], "r", encoding='utf-8') as sf: cs = csf.read() s = sf.read() return cs == s # 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") def test_alice_pays_exact(self): run_bot(self, num[bob], "!zieh foo 20") run_bot(self, num[charlie], "!zieh foo 10") i = "!cars pay foo 30" res = run_bot(self, num[alice], i) 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") i = "!cars pay foo 40" res = run_bot(self, num[alice], i) 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 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) #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 charlie <- 5.00 bob """ 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_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.json_schedule_weekly") 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.json_schedule_weekly") 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.json_schedule_weekly") 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.json_schedule_monthly") 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 = date(now.year, now.month - 1, now.day) 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.json_schedule_monthly") res = run_bot(self, num[alice], "!fuck") self.assertEqual(res.stdout, 'Nothing to rewind') # Last exec two month ago if now.month > 2: two_months_ago = date(now.year, now.month - 2, now.day) 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.json_schedule_monthly") 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." ) if __name__ == '__main__': unittest.main()