From 6f4312cb4f8d051150c93a4cd9e850de67883988 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Sun, 7 Feb 2021 09:22:54 +0100 Subject: [PATCH 01/10] Bugfix state with zero balance Signed-off-by: Julien Riou --- main.py | 2 +- state.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 8ab3ec3..11bf80f 100644 --- a/main.py +++ b/main.py @@ -79,7 +79,7 @@ def main(): last_balance, last_transaction = handler.watch_miner(address=config['miner'], last_balance=pool_state.get('balance'), last_transaction=pool_state.get('payment')) - if last_balance: + if last_balance is not None: logger.debug(f'saving {pool} miner balance to state file') state.write(pool_name=pool, miner_balance=last_balance) if last_transaction: diff --git a/state.py b/state.py index e1a9537..423fbe8 100644 --- a/state.py +++ b/state.py @@ -22,7 +22,7 @@ class State: content[pool_name] = {} if block_number: content[pool_name]['block'] = block_number - if miner_balance: + if miner_balance is not None: content[pool_name]['balance'] = miner_balance if miner_payment: content[pool_name]['payment'] = miner_payment From 14aa8a6d59b8204c534d3cd9045a2831a79390fd Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Fri, 12 Feb 2021 09:43:38 +0100 Subject: [PATCH 02/10] Add "state" tests Signed-off-by: Julien Riou --- .gitignore | 2 + README.md | 6 +- companion/__init__.py | 0 coingecko.py => companion/coingecko.py | 0 config.py => companion/config.py | 0 .../config.schema.json | 0 main.py => companion/main.py | 0 {pools => companion/pools}/__init__.py | 0 {pools => companion/pools}/ethermine.py | 0 {pools => companion/pools}/flexpool.py | 0 state.py => companion/state.py | 2 +- telegram.py => companion/telegram.py | 0 .../templates}/balance.md.j2 | 0 .../templates}/block.md.j2 | 0 .../templates}/payment.md.j2 | 0 utils.py => companion/utils.py | 0 pytest.ini | 2 + requirements.txt | 1 + requirements/tests.txt | 2 + tests/__init__.py | 4 + tests/test_state.py | 100 ++++++++++++++++++ 21 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 companion/__init__.py rename coingecko.py => companion/coingecko.py (100%) rename config.py => companion/config.py (100%) rename config.schema.json => companion/config.schema.json (100%) rename main.py => companion/main.py (100%) rename {pools => companion/pools}/__init__.py (100%) rename {pools => companion/pools}/ethermine.py (100%) rename {pools => companion/pools}/flexpool.py (100%) rename state.py => companion/state.py (96%) rename telegram.py => companion/telegram.py (100%) rename {templates => companion/templates}/balance.md.j2 (100%) rename {templates => companion/templates}/block.md.j2 (100%) rename {templates => companion/templates}/payment.md.j2 (100%) rename utils.py => companion/utils.py (100%) create mode 100644 pytest.ini create mode 100644 requirements/tests.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_state.py diff --git a/.gitignore b/.gitignore index 70e53ea..e14759b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ venv __pycache__ config.json state.json +.coverage +test_state.json diff --git a/README.md b/README.md index d5cb7f2..535066e 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ All options are optional (but the companion would do nothing). ## Usage ``` -python3 main.py --help +python3 companion/main.py --help ``` @@ -69,12 +69,14 @@ python3 main.py --help Contributions are welcomed! Feel free to update the code and create a pull-request. -Be sure to lint the code before: +Be sure to lint the code and run tests before: ``` docker build -t pre-commit . docker run -it -v $(pwd):/mnt/ --rm pre-commit bash # cd /mnt/ +# pip install -r requirements.txt # pre-commit run --all-files +# pytest # exit ``` diff --git a/companion/__init__.py b/companion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coingecko.py b/companion/coingecko.py similarity index 100% rename from coingecko.py rename to companion/coingecko.py diff --git a/config.py b/companion/config.py similarity index 100% rename from config.py rename to companion/config.py diff --git a/config.schema.json b/companion/config.schema.json similarity index 100% rename from config.schema.json rename to companion/config.schema.json diff --git a/main.py b/companion/main.py similarity index 100% rename from main.py rename to companion/main.py diff --git a/pools/__init__.py b/companion/pools/__init__.py similarity index 100% rename from pools/__init__.py rename to companion/pools/__init__.py diff --git a/pools/ethermine.py b/companion/pools/ethermine.py similarity index 100% rename from pools/ethermine.py rename to companion/pools/ethermine.py diff --git a/pools/flexpool.py b/companion/pools/flexpool.py similarity index 100% rename from pools/flexpool.py rename to companion/pools/flexpool.py diff --git a/state.py b/companion/state.py similarity index 96% rename from state.py rename to companion/state.py index 423fbe8..491d033 100644 --- a/state.py +++ b/companion/state.py @@ -20,7 +20,7 @@ class State: content = self.read() if pool_name not in content: content[pool_name] = {} - if block_number: + if block_number is not None: content[pool_name]['block'] = block_number if miner_balance is not None: content[pool_name]['balance'] = miner_balance diff --git a/telegram.py b/companion/telegram.py similarity index 100% rename from telegram.py rename to companion/telegram.py diff --git a/templates/balance.md.j2 b/companion/templates/balance.md.j2 similarity index 100% rename from templates/balance.md.j2 rename to companion/templates/balance.md.j2 diff --git a/templates/block.md.j2 b/companion/templates/block.md.j2 similarity index 100% rename from templates/block.md.j2 rename to companion/templates/block.md.j2 diff --git a/templates/payment.md.j2 b/companion/templates/payment.md.j2 similarity index 100% rename from templates/payment.md.j2 rename to companion/templates/payment.md.j2 diff --git a/utils.py b/companion/utils.py similarity index 100% rename from utils.py rename to companion/utils.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..f5c75eb --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov=companion tests/ diff --git a/requirements.txt b/requirements.txt index 1ad3230..165da09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -r requirements/base.txt -r requirements/ethermine.txt -r requirements/flexpool.txt +-r requirements/tests.txt diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 0000000..e972258 --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,2 @@ +pytest==6.2.2 +pytest-cov==2.11.1 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..f1487c8 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +import os +import sys + +sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'companion')) diff --git a/tests/test_state.py b/tests/test_state.py new file mode 100644 index 0000000..f623d5a --- /dev/null +++ b/tests/test_state.py @@ -0,0 +1,100 @@ +import json +import os + +import pytest +from companion.state import State + + +class TestState: + FILENAME = 'test_state.json' + POOL_NAME = 'testpool' + CONTENT = { + 'testpool': { + 'block': 1234, + 'balance': 1234, + 'payment': '0x0000000' + } + } + + @pytest.fixture(scope='function') + def state(self): + return State(self.FILENAME) + + @pytest.fixture(scope='function') + def create_state(self): + with open(self.FILENAME, 'w') as fd: + json.dump(self.CONTENT, fd, indent=2) + yield + if os.path.isfile(self.FILENAME): + os.unlink(self.FILENAME) + + @pytest.fixture(scope='function') + def remove_state(self): + yield + if os.path.isfile(self.FILENAME): + os.unlink(self.FILENAME) + + def test_init(self, state, remove_state): + assert os.path.isfile(self.FILENAME) + with open(self.FILENAME, 'r') as fd: + assert json.load(fd) == {} + + def test_read(self, state, create_state): + content = state.read() + for pool in self.CONTENT: + assert pool in content + for key in self.CONTENT[pool]: + assert key in content[pool] and content[pool][key] == self.CONTENT[pool][key] + + def test_write(self, state): + state.write(pool_name=self.POOL_NAME) + content = state.read() + assert content[self.POOL_NAME] == {} + + def test_write_block(self, create_state, state): + state.write(pool_name=self.POOL_NAME, block_number=5678) + content = state.read() + assert content[self.POOL_NAME]['block'] == 5678 + + def test_write_empty_block(self, create_state, state): + state.write(pool_name=self.POOL_NAME, block_number=None) + content = state.read() + assert content[self.POOL_NAME]['block'] == self.CONTENT[self.POOL_NAME]['block'] # not changed + + def test_write_zero_block(self, create_state, state): + state.write(pool_name=self.POOL_NAME, block_number=0) + content = state.read() + assert content[self.POOL_NAME]['block'] == 0 + + def test_write_balance(self, create_state, state): + state.write(pool_name=self.POOL_NAME, miner_balance=5678) + content = state.read() + assert content[self.POOL_NAME]['balance'] == 5678 + + def test_write_empty_balance(self, create_state, state): + state.write(pool_name=self.POOL_NAME, miner_balance=None) + content = state.read() + assert content[self.POOL_NAME]['balance'] == self.CONTENT[self.POOL_NAME]['balance'] # not changed + + def test_write_zero_balance(self, create_state, state): + state.write(pool_name=self.POOL_NAME, miner_balance=0) + content = state.read() + assert content[self.POOL_NAME]['balance'] == 0 + + def test_write_payment(self, create_state, state): + state.write(pool_name=self.POOL_NAME, miner_payment='0x1111111') + content = state.read() + assert content[self.POOL_NAME]['payment'] == '0x1111111' + + def test_write_empty_payment(self, create_state, state): + state.write(pool_name=self.POOL_NAME, miner_payment=None) + content = state.read() + assert content[self.POOL_NAME]['payment'] == self.CONTENT[self.POOL_NAME]['payment'] # not changed + + def test_get(self, create_state): + state = State(filename=self.FILENAME) + assert state.get(self.POOL_NAME) == self.CONTENT[self.POOL_NAME] + + def test_get_missing_key(self, create_state): + state = State(filename=self.FILENAME) + assert state.get('UNKNOWN_POOL') == {} From 9eddc6953cd0e0393926b9a704f7816c7b14de75 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Fri, 12 Feb 2021 16:10:16 +0100 Subject: [PATCH 03/10] Add initial flexpool tests Signed-off-by: Julien Riou --- companion/pools/__init__.py | 32 +++++----- companion/pools/flexpool.py | 1 + requirements/tests.txt | 1 + tests/test_flexpool.py | 120 ++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 tests/test_flexpool.py diff --git a/companion/pools/__init__.py b/companion/pools/__init__.py index 6523eda..ddff2ee 100644 --- a/companion/pools/__init__.py +++ b/companion/pools/__init__.py @@ -30,22 +30,20 @@ class Handler: return miner.raw_balance def _watch_miner_payments(self, miner, last_transaction=None): - if miner.last_transaction and miner.last_transaction.txid != last_transaction: - logger.debug('watching miner payments') - # send notifications for recent payements only - for transaction in miner.transactions[MAX_NOTIFICATIONS_COUNT:]: - if not last_transaction or transaction.txid > last_transaction: - logger.info(f'new payment {transaction.txid}') - if self.notifier: - logger.debug('sending payment notification') - arguments = {'pool': self.pool_name, 'address': miner.address, 'txid': transaction.txid, - 'amount': transaction.amount, 'amount_fiat': transaction.amount_fiat, - 'time': transaction.time, 'duration': transaction.duration} - try: - self.notifier.notify_payment(**arguments) - logger.info('payment notification sent') - except Exception as err: - logger.error('failed to send notification') - logger.exception(err) + logger.debug('watching miner payments') + if miner.last_transaction and (not last_transaction or miner.last_transaction.txid != last_transaction): + # send notifications for last payment only + logger.info(f'new payment {miner.last_transaction.txid}') + if self.notifier: + logger.debug('sending payment notification') + arguments = {'pool': self.pool_name, 'address': miner.address, 'txid': miner.last_transaction.txid, + 'amount': miner.last_transaction.amount, 'amount_fiat': miner.last_transaction.amount_fiat, + 'time': miner.last_transaction.time, 'duration': miner.last_transaction.duration} + try: + self.notifier.notify_payment(**arguments) + logger.info('payment notification sent') + except Exception as err: + logger.error('failed to send notification') + logger.exception(err) if miner.last_transaction and miner.last_transaction.txid: return miner.last_transaction.txid diff --git a/companion/pools/flexpool.py b/companion/pools/flexpool.py index 9f2f50c..ea94de3 100644 --- a/companion/pools/flexpool.py +++ b/companion/pools/flexpool.py @@ -40,6 +40,7 @@ class Miner: miner = flexpoolapi.miner(address) self.raw_balance = miner.balance() self.balance = convert_weis(self.raw_balance) + self.balance_fiat = None if exchange_rate and currency: self.balance_fiat = convert_fiat(amount=self.raw_balance, exchange_rate=exchange_rate, currency=currency) payout_threshold = self.get_payout_threshold(miner) diff --git a/requirements/tests.txt b/requirements/tests.txt index e972258..89b1fa6 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,2 +1,3 @@ pytest==6.2.2 pytest-cov==2.11.1 +pytest-mock==3.5.1 diff --git a/tests/test_flexpool.py b/tests/test_flexpool.py new file mode 100644 index 0000000..a214f68 --- /dev/null +++ b/tests/test_flexpool.py @@ -0,0 +1,120 @@ +from companion.pools.flexpool import FlexpoolHandler, Miner, Transaction +from datetime import datetime, timedelta +import pytest + +class TestFlexpoolHandler: + # def test_block(self, mocker): + def test_init(self): + handler = FlexpoolHandler() + assert handler.pool_name == 'flexpool' + + def test_new_balance_with_notification(self, mocker): + """Old balance is 1, new balance is 2, should send notification""" + notifier = mocker.Mock() + notifier.notify_balance = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + miner = mocker.patch('flexpoolapi.miner') + miner().balance.return_value = 2 + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') + mocker.patch('companion.pools.flexpool.Miner.get_payements') + last_balance, last_transaction = handler.watch_miner(address='addr', last_balance=1) + assert last_balance == 2 + notifier.notify_balance.assert_called_once() + + def test_new_balance_after_payment_with_notification(self, mocker): + """Old balance is 0, new balance is 1 (old > new), should send notification""" + notifier = mocker.Mock() + notifier.notify_balance = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + miner = mocker.patch('flexpoolapi.miner') + miner().balance.return_value = 0 + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') + mocker.patch('companion.pools.flexpool.Miner.get_payements') + last_balance, last_transaction = handler.watch_miner(address='addr', last_balance=1) + assert last_balance == 0 + notifier.notify_balance.assert_called_once() + + def test_very_new_balance_with_notification(self, mocker): + """Old balance doesn't exist, new balance is 1, should send notification""" + notifier = mocker.Mock() + notifier.notify_balance = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + miner = mocker.patch('flexpoolapi.miner') + miner().balance.return_value = 1 + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') + mocker.patch('companion.pools.flexpool.Miner.get_payements') + last_balance, last_transaction = handler.watch_miner(address='addr') + assert last_balance == 1 + notifier.notify_balance.assert_called_once() + + def test_same_balance_without_notification(self, mocker): + """Old balance and new balance are the same, should not send notification""" + notifier = mocker.Mock() + notifier.notify_balance = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + miner = mocker.patch('flexpoolapi.miner') + miner().balance.return_value = 1 + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') + mocker.patch('companion.pools.flexpool.Miner.get_payements') + last_balance, last_transaction = handler.watch_miner(address='addr', last_balance=1) + assert last_balance == 1 + notifier.notify_balance.assert_not_called() + + def test_new_payment_with_notification(self, mocker): + """One transaction saved (trx1), two transactions detected (trx1, trx2), should send notification""" + notifier = mocker.Mock() + notifier.notify_payment = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + mocker.patch('flexpoolapi.miner') + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') + get_payements = mocker.patch('companion.pools.flexpool.Miner.get_payements') + get_payements.return_value = [ + Transaction(txid='trx1', amount=1, time=datetime.now(), duration=timedelta(minutes=1)), + Transaction(txid='trx2', amount=1, time=datetime.now(), duration=timedelta(minutes=1)) + ] + last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction='trx1') + assert last_transaction == 'trx2' + notifier.notify_payment.assert_called_once() + + def test_very_new_payment_with_notification(self, mocker): + """No transaction saved, one transaction detected (trx1), should send notification""" + notifier = mocker.Mock() + notifier.notify_payment = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + mocker.patch('flexpoolapi.miner') + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') + get_payements = mocker.patch('companion.pools.flexpool.Miner.get_payements') + get_payements.return_value = [ + Transaction(txid='trx1', amount=1, time=datetime.now(), duration=timedelta(minutes=1)) + ] + last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction=None) + assert last_transaction == 'trx1' + notifier.notify_payment.assert_called_once() + + def test_same_payment_without_notification(self, mocker): + """One transaction saved (trx1), one transaction detected (trx1), should not send notification""" + notifier = mocker.Mock() + notifier.notify_payment = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + mocker.patch('flexpoolapi.miner') + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') + get_payements = mocker.patch('companion.pools.flexpool.Miner.get_payements') + get_payements.return_value = [ + Transaction(txid='trx1', amount=1, time=datetime.now(), duration=timedelta(minutes=1)) + ] + last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction='trx1') + assert last_transaction == 'trx1' + notifier.notify_payment.assert_not_called() + + def test_zero_payment_without_notification(self, mocker): + """Zero transaction saved, zero transaction detected, should not send notification""" + notifier = mocker.Mock() + notifier.notify_payment = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + mocker.patch('flexpoolapi.miner') + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') + get_payements = mocker.patch('companion.pools.flexpool.Miner.get_payements') + get_payements.return_value = [] + last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction=None) + assert last_transaction is None + notifier.notify_payment.assert_not_called() From 1e2ef88c6470e72643d50a2634d3574772861416 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Mon, 15 Feb 2021 16:55:22 +0100 Subject: [PATCH 04/10] Add fiat conversion to payments on Flexpool Signed-off-by: Julien Riou --- companion/pools/flexpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/companion/pools/flexpool.py b/companion/pools/flexpool.py index ea94de3..1e695fb 100644 --- a/companion/pools/flexpool.py +++ b/companion/pools/flexpool.py @@ -46,7 +46,7 @@ class Miner: payout_threshold = self.get_payout_threshold(miner) self.balance_percentage = self.format_balance_percentage(payout_threshold=payout_threshold, balance=self.raw_balance) - self.transactions = self.get_payements(miner) + self.transactions = self.get_payements(miner, exchange_rate=exchange_rate, currency=currency) @property def url(self): From 4cd278c60e8743d7f6a5f0ac14a245eb8daee919 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Mon, 15 Feb 2021 16:57:17 +0100 Subject: [PATCH 05/10] Lint Flexpool tests Signed-off-by: Julien Riou --- tests/test_flexpool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_flexpool.py b/tests/test_flexpool.py index a214f68..4dd8060 100644 --- a/tests/test_flexpool.py +++ b/tests/test_flexpool.py @@ -1,9 +1,9 @@ -from companion.pools.flexpool import FlexpoolHandler, Miner, Transaction from datetime import datetime, timedelta -import pytest + +from companion.pools.flexpool import FlexpoolHandler, Transaction + class TestFlexpoolHandler: - # def test_block(self, mocker): def test_init(self): handler = FlexpoolHandler() assert handler.pool_name == 'flexpool' From 9d756048691bbe21e6f3495778647e1b9465e70e Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Tue, 16 Feb 2021 09:29:55 +0100 Subject: [PATCH 06/10] Parametrize Flexpool tests Signed-off-by: Julien Riou --- tests/test_flexpool.py | 146 +++++++++++++---------------------------- 1 file changed, 46 insertions(+), 100 deletions(-) diff --git a/tests/test_flexpool.py b/tests/test_flexpool.py index 4dd8060..6aa4516 100644 --- a/tests/test_flexpool.py +++ b/tests/test_flexpool.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta +import pytest from companion.pools.flexpool import FlexpoolHandler, Transaction @@ -8,113 +9,58 @@ class TestFlexpoolHandler: handler = FlexpoolHandler() assert handler.pool_name == 'flexpool' - def test_new_balance_with_notification(self, mocker): - """Old balance is 1, new balance is 2, should send notification""" - notifier = mocker.Mock() - notifier.notify_balance = mocker.Mock() - handler = FlexpoolHandler(notifier=notifier) - miner = mocker.patch('flexpoolapi.miner') - miner().balance.return_value = 2 - mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') - mocker.patch('companion.pools.flexpool.Miner.get_payements') - last_balance, last_transaction = handler.watch_miner(address='addr', last_balance=1) - assert last_balance == 2 - notifier.notify_balance.assert_called_once() - - def test_new_balance_after_payment_with_notification(self, mocker): - """Old balance is 0, new balance is 1 (old > new), should send notification""" - notifier = mocker.Mock() - notifier.notify_balance = mocker.Mock() - handler = FlexpoolHandler(notifier=notifier) - miner = mocker.patch('flexpoolapi.miner') - miner().balance.return_value = 0 - mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') - mocker.patch('companion.pools.flexpool.Miner.get_payements') - last_balance, last_transaction = handler.watch_miner(address='addr', last_balance=1) - assert last_balance == 0 - notifier.notify_balance.assert_called_once() - - def test_very_new_balance_with_notification(self, mocker): - """Old balance doesn't exist, new balance is 1, should send notification""" - notifier = mocker.Mock() - notifier.notify_balance = mocker.Mock() - handler = FlexpoolHandler(notifier=notifier) - miner = mocker.patch('flexpoolapi.miner') - miner().balance.return_value = 1 - mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') - mocker.patch('companion.pools.flexpool.Miner.get_payements') - last_balance, last_transaction = handler.watch_miner(address='addr') - assert last_balance == 1 - notifier.notify_balance.assert_called_once() - - def test_same_balance_without_notification(self, mocker): - """Old balance and new balance are the same, should not send notification""" - notifier = mocker.Mock() - notifier.notify_balance = mocker.Mock() - handler = FlexpoolHandler(notifier=notifier) - miner = mocker.patch('flexpoolapi.miner') - miner().balance.return_value = 1 - mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') - mocker.patch('companion.pools.flexpool.Miner.get_payements') - last_balance, last_transaction = handler.watch_miner(address='addr', last_balance=1) - assert last_balance == 1 - notifier.notify_balance.assert_not_called() - - def test_new_payment_with_notification(self, mocker): - """One transaction saved (trx1), two transactions detected (trx1, trx2), should send notification""" - notifier = mocker.Mock() - notifier.notify_payment = mocker.Mock() - handler = FlexpoolHandler(notifier=notifier) - mocker.patch('flexpoolapi.miner') - mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') - get_payements = mocker.patch('companion.pools.flexpool.Miner.get_payements') - get_payements.return_value = [ - Transaction(txid='trx1', amount=1, time=datetime.now(), duration=timedelta(minutes=1)), - Transaction(txid='trx2', amount=1, time=datetime.now(), duration=timedelta(minutes=1)) + @pytest.mark.parametrize( + 'old_balance,new_balance,should_notify', + [ + pytest.param(1, 2, True, id='new_balance_with_notification'), + pytest.param(1, 0, True, id='new_balance_after_payment_with_notification'), + pytest.param(None, 1, True, id='very_new_balance_with_notification'), + pytest.param(1, 1, False, id='same_balance_without_notification'), ] - last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction='trx1') - assert last_transaction == 'trx2' - notifier.notify_payment.assert_called_once() - - def test_very_new_payment_with_notification(self, mocker): - """No transaction saved, one transaction detected (trx1), should send notification""" + ) + def test_balance(self, mocker, old_balance, new_balance, should_notify): notifier = mocker.Mock() - notifier.notify_payment = mocker.Mock() + notifier.notify_balance = mocker.Mock() handler = FlexpoolHandler(notifier=notifier) - mocker.patch('flexpoolapi.miner') - mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') - get_payements = mocker.patch('companion.pools.flexpool.Miner.get_payements') - get_payements.return_value = [ - Transaction(txid='trx1', amount=1, time=datetime.now(), duration=timedelta(minutes=1)) + miner = mocker.patch('flexpoolapi.miner') + miner().balance.return_value = new_balance + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') + mocker.patch('companion.pools.flexpool.Miner.get_payements') + last_balance, last_transaction = handler.watch_miner(address='addr', last_balance=old_balance) + assert last_balance == new_balance + if should_notify: + notifier.notify_balance.assert_called_once() + else: + notifier.notify_balance.assert_not_called() + + @staticmethod + def _create_transactions(names): + if names: + return [Transaction(txid=n, amount=1, time=datetime.now(), duration=timedelta(minutes=1)) for n in names] + + @pytest.mark.parametrize( + 'old_transaction,new_transactions,should_notify', + [ + pytest.param('trx1', ['trx1', 'trx2'], True, id='new_payment_with_notification'), + pytest.param(None, ['trx1'], True, id='very_new_payment_with_notification'), + pytest.param('trx1', ['trx1'], False, id='same_payment_without_notification'), + pytest.param(None, None, False, id='zero_payment_without_notification'), ] - last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction=None) - assert last_transaction == 'trx1' - notifier.notify_payment.assert_called_once() - - def test_same_payment_without_notification(self, mocker): - """One transaction saved (trx1), one transaction detected (trx1), should not send notification""" + ) + def test_payments(self, mocker, old_transaction, new_transactions, should_notify): notifier = mocker.Mock() notifier.notify_payment = mocker.Mock() handler = FlexpoolHandler(notifier=notifier) mocker.patch('flexpoolapi.miner') mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') get_payements = mocker.patch('companion.pools.flexpool.Miner.get_payements') - get_payements.return_value = [ - Transaction(txid='trx1', amount=1, time=datetime.now(), duration=timedelta(minutes=1)) - ] - last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction='trx1') - assert last_transaction == 'trx1' - notifier.notify_payment.assert_not_called() - - def test_zero_payment_without_notification(self, mocker): - """Zero transaction saved, zero transaction detected, should not send notification""" - notifier = mocker.Mock() - notifier.notify_payment = mocker.Mock() - handler = FlexpoolHandler(notifier=notifier) - mocker.patch('flexpoolapi.miner') - mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') - get_payements = mocker.patch('companion.pools.flexpool.Miner.get_payements') - get_payements.return_value = [] - last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction=None) - assert last_transaction is None - notifier.notify_payment.assert_not_called() + get_payements.return_value = self._create_transactions(new_transactions) + last_balance, last_transaction = handler.watch_miner(address='addr', last_transaction=old_transaction) + if new_transactions: + assert last_transaction == new_transactions[-1] + else: + assert last_transaction is None + if should_notify: + notifier.notify_payment.assert_called_once() + else: + notifier.notify_payment.assert_not_called() From c20ed21c2d4e216d2daa8044f35e3f2b38a7c6b3 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 17 Feb 2021 10:13:59 +0100 Subject: [PATCH 07/10] Add Flexpool tests for block notifications Signed-off-by: Julien Riou --- companion/pools/flexpool.py | 17 ++++++++++------- tests/test_flexpool.py | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/companion/pools/flexpool.py b/companion/pools/flexpool.py index 1e695fb..7c958f9 100644 --- a/companion/pools/flexpool.py +++ b/companion/pools/flexpool.py @@ -131,7 +131,9 @@ class FlexpoolHandler(Handler): blocks = self.get_blocks(exchange_rate=self.exchange_rate, currency=self.currency) if blocks: # don't spam block notification at initialization - for block in blocks[MAX_NOTIFICATIONS_COUNT:]: + notification_slice = MAX_NOTIFICATIONS_COUNT if len(blocks) > MAX_NOTIFICATIONS_COUNT else 0 + for block in blocks[notification_slice:]: + print(block) if not last_block or last_block < block.number: logger.info(f'new block {block.number}') if self.notifier: @@ -145,7 +147,7 @@ class FlexpoolHandler(Handler): except Exception as err: logger.error('failed to send notification') logger.exception(err) - last_remote_block = block + last_remote_block = block if last_remote_block and last_remote_block.number: return last_remote_block.number @@ -154,11 +156,12 @@ class FlexpoolHandler(Handler): remote_blocks = flexpoolapi.pool.last_blocks(count=MAX_BLOCKS_COUNT) # convert to blocks blocks = [] - for remote_block in remote_blocks: - block = Block(number=remote_block.number, hash=remote_block.hash, time=remote_block.time, - round_time=remote_block.round_time, reward=remote_block.total_rewards, luck=remote_block.luck, - exchange_rate=exchange_rate, currency=currency) - blocks.append(block) + if remote_blocks: + for remote_block in remote_blocks: + block = Block(number=remote_block.number, hash=remote_block.hash, time=remote_block.time, + round_time=remote_block.round_time, reward=remote_block.total_rewards, + luck=remote_block.luck, exchange_rate=exchange_rate, currency=currency) + blocks.append(block) # sort by block number return sorted(blocks) diff --git a/tests/test_flexpool.py b/tests/test_flexpool.py index 6aa4516..57c36bb 100644 --- a/tests/test_flexpool.py +++ b/tests/test_flexpool.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import pytest from companion.pools.flexpool import FlexpoolHandler, Transaction +from flexpoolapi.shared import Block as BlockApi class TestFlexpoolHandler: @@ -64,3 +65,40 @@ class TestFlexpoolHandler: notifier.notify_payment.assert_called_once() else: notifier.notify_payment.assert_not_called() + + @staticmethod + def _create_blocks(numbers): + if numbers: + blocks = [] + for number in numbers: + blocks.append(BlockApi(number=number, blockhash='h', block_type='bt', miner='m', difficulty=1, + timestamp=1, is_confirmed=True, round_time=1, luck=1.0, server_name='s', + block_reward=1, block_fees=1, uncle_inclusion_rewards=1, total_rewards=1)) + return blocks + + @pytest.mark.parametrize( + 'last_block,remote_blocks,should_notify', + [ + pytest.param(1, [1, 2], True, id='new_block_with_notification'), + pytest.param(None, [1], True, id='very_new_block_with_notification'), + pytest.param(1, [1], False, id='same_block_without_notification'), + pytest.param(9, range(1, 11), True, id='new_block_with_count_over_max_notification'), + pytest.param(10, range(1, 11), False, id='same_block_with_count_over_max_notification'), + pytest.param(None, None, False, id='zero_block_without_notification'), + ] + ) + def test_block(self, mocker, last_block, remote_blocks, should_notify): + notifier = mocker.Mock() + notifier.notify_block = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + last_blocks = mocker.patch('flexpoolapi.pool.last_blocks') + last_blocks.return_value = self._create_blocks(remote_blocks) + block = handler.watch_blocks(last_block=last_block) + if remote_blocks: + assert block == remote_blocks[-1] + else: + assert block is None + if should_notify: + notifier.notify_block.assert_called_once() + else: + notifier.notify_block.assert_not_called() From 0a2bef6192a771e359d677d776f35c247d71340d Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 17 Feb 2021 10:15:46 +0100 Subject: [PATCH 08/10] Remove leftover print statement Signed-off-by: Julien Riou --- companion/pools/flexpool.py | 1 - 1 file changed, 1 deletion(-) diff --git a/companion/pools/flexpool.py b/companion/pools/flexpool.py index 7c958f9..ab97046 100644 --- a/companion/pools/flexpool.py +++ b/companion/pools/flexpool.py @@ -133,7 +133,6 @@ class FlexpoolHandler(Handler): # don't spam block notification at initialization notification_slice = MAX_NOTIFICATIONS_COUNT if len(blocks) > MAX_NOTIFICATIONS_COUNT else 0 for block in blocks[notification_slice:]: - print(block) if not last_block or last_block < block.number: logger.info(f'new block {block.number}') if self.notifier: From 7f5bd0a1cfd4fcd99890a9524bb36d9407237a33 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Wed, 17 Feb 2021 16:21:08 +0100 Subject: [PATCH 09/10] Add Flexpool tests for API failures Signed-off-by: Julien Riou --- companion/pools/flexpool.py | 51 ++++++++++++++++++++++--------------- tests/test_flexpool.py | 38 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/companion/pools/flexpool.py b/companion/pools/flexpool.py index ab97046..f39420a 100644 --- a/companion/pools/flexpool.py +++ b/companion/pools/flexpool.py @@ -152,30 +152,39 @@ class FlexpoolHandler(Handler): @staticmethod def get_blocks(exchange_rate=None, currency=None): - remote_blocks = flexpoolapi.pool.last_blocks(count=MAX_BLOCKS_COUNT) - # convert to blocks - blocks = [] - if remote_blocks: - for remote_block in remote_blocks: - block = Block(number=remote_block.number, hash=remote_block.hash, time=remote_block.time, - round_time=remote_block.round_time, reward=remote_block.total_rewards, - luck=remote_block.luck, exchange_rate=exchange_rate, currency=currency) - blocks.append(block) - # sort by block number - return sorted(blocks) + try: + remote_blocks = flexpoolapi.pool.last_blocks(count=MAX_BLOCKS_COUNT) + # convert to blocks + blocks = [] + if remote_blocks: + for remote_block in remote_blocks: + block = Block(number=remote_block.number, hash=remote_block.hash, time=remote_block.time, + round_time=remote_block.round_time, reward=remote_block.total_rewards, + luck=remote_block.luck, exchange_rate=exchange_rate, currency=currency) + blocks.append(block) + # sort by block number + return sorted(blocks) + except flexpoolapi.exceptions.APIError as err: + logger.warning('failed to get blocks from Flexpool API') + logger.debug(err) def watch_miner(self, address, last_balance=None, last_transaction=None): logger.debug(f'watching miner {address}') try: miner = Miner(address=address, exchange_rate=self.exchange_rate, currency=self.currency) - except Exception as err: + logger.debug(miner) + + last_balance = self._watch_miner_balance(miner=miner, last_balance=last_balance) + last_transaction = self._watch_miner_payments(miner=miner, last_transaction=last_transaction) + + return last_balance, last_transaction + except flexpoolapi.exceptions.InvalidMinerAddress as err: + logger.error(f'miner address {address} is invalid') + logger.debug(err) + except flexpoolapi.exceptions.MinerDoesNotExist as err: logger.error(f'miner {address} not found') - logger.exception(err) - return - - logger.debug(miner) - - last_balance = self._watch_miner_balance(miner=miner, last_balance=last_balance) - last_transaction = self._watch_miner_payments(miner=miner, last_transaction=last_transaction) - - return last_balance, last_transaction + logger.debug(err) + except flexpoolapi.exceptions.APIError as err: + logger.warning('failed to get miner from Flexpool API') + logger.debug(err) + return None, None diff --git a/tests/test_flexpool.py b/tests/test_flexpool.py index 57c36bb..7a1324f 100644 --- a/tests/test_flexpool.py +++ b/tests/test_flexpool.py @@ -34,6 +34,20 @@ class TestFlexpoolHandler: else: notifier.notify_balance.assert_not_called() + def test_balance_with_api_failure(self, mocker): + """An API failure should not send a balance notification""" + notifier = mocker.Mock() + notifier.notify_balance = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + request_get = mocker.patch('requests.get') + request_get.return_value.status_code = 503 + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_payments') + mocker.patch('companion.pools.flexpool.Miner.get_payements') + last_balance, last_transaction = handler.watch_miner(address='0000000000000000000000000000000000000001', + last_balance=1) + assert last_balance is None + notifier.notify_balance.assert_not_called() + @staticmethod def _create_transactions(names): if names: @@ -66,6 +80,19 @@ class TestFlexpoolHandler: else: notifier.notify_payment.assert_not_called() + def test_payment_with_api_failure(self, mocker): + """An API failure should not send a payment notification""" + notifier = mocker.Mock() + notifier.notify_payment = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + request_get = mocker.patch('requests.get') + request_get.return_value.status_code = 503 + mocker.patch('companion.pools.flexpool.FlexpoolHandler._watch_miner_balance') + last_balance, last_transaction = handler.watch_miner(address='0000000000000000000000000000000000000001', + last_transaction=1) + assert last_transaction is None + notifier.notify_payment.assert_not_called() + @staticmethod def _create_blocks(numbers): if numbers: @@ -102,3 +129,14 @@ class TestFlexpoolHandler: notifier.notify_block.assert_called_once() else: notifier.notify_block.assert_not_called() + + def test_block_with_api_failure(self, mocker): + """An API failure should not send a block notification""" + notifier = mocker.Mock() + notifier.notify_block = mocker.Mock() + handler = FlexpoolHandler(notifier=notifier) + request_get = mocker.patch('requests.get') + request_get.return_value.status_code = 503 + block = handler.watch_blocks(last_block=1) + assert block is None + notifier.notify_block.assert_not_called() From b66aa8b1586a31ddad0c2454e4762661c63385a1 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 25 Mar 2021 10:41:10 +0100 Subject: [PATCH 10/10] Escape '=' to produce valid markdown Signed-off-by: Julien Riou --- companion/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/companion/telegram.py b/companion/telegram.py index a213a8d..ce6c5f2 100644 --- a/companion/telegram.py +++ b/companion/telegram.py @@ -18,7 +18,7 @@ class TelegramNotifier: @staticmethod def _markdown_escape(text): text = str(text) - for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!']: + for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!', '=']: text = text.replace(special_char, fr'\{special_char}') return text