Repository: rotki/rotki

Issue:
Add periodic premium check
## Abstract

Add a periodic check for premium status for long-running rotki instances.

## Task

Do it


Discussion:

Resolving patch:
diff --git a/rotkehlchen/rotkehlchen.py b/rotkehlchen/rotkehlchen.py
--- a/rotkehlchen/rotkehlchen.py
+++ b/rotkehlchen/rotkehlchen.py
@@ -328,6 +328,7 @@ def unlock_user(
             chain_manager=self.chain_manager,
             exchange_manager=self.exchange_manager,
             evm_tx_decoder=self.evm_tx_decoder,
+            deactivate_premium=self.deactivate_premium_status,
         )
         DataMigrationManager(self).maybe_migrate_data()
         self.greenlet_manager.spawn_and_track(
diff --git a/rotkehlchen/tasks/manager.py b/rotkehlchen/tasks/manager.py
--- a/rotkehlchen/tasks/manager.py
+++ b/rotkehlchen/tasks/manager.py
@@ -2,7 +2,7 @@
 import logging
 import random
 from collections import defaultdict
-from typing import DefaultDict, List, NamedTuple, Set, Tuple
+from typing import Callable, DefaultDict, List, NamedTuple, Set, Tuple
 
 import gevent
 
@@ -16,7 +16,7 @@
 from rotkehlchen.db.ethtx import DBEthTx
 from rotkehlchen.db.filtering import DBIgnoreValuesFilter, DBStringFilter, HistoryEventFilterQuery
 from rotkehlchen.db.history_events import DBHistoryEvents
-from rotkehlchen.errors import NoPriceForGivenTimestamp, RemoteError
+from rotkehlchen.errors import NoPriceForGivenTimestamp, PremiumAuthenticationError, RemoteError
 from rotkehlchen.exchanges.manager import ExchangeManager
 from rotkehlchen.externalapis.cryptocompare import Cryptocompare
 from rotkehlchen.fval import FVal
@@ -25,8 +25,10 @@
 from rotkehlchen.history.price import PriceHistorian
 from rotkehlchen.history.types import HistoricalPriceOracle
 from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.premium.premium import premium_create_and_verify
 from rotkehlchen.premium.sync import PremiumSyncManager
 from rotkehlchen.types import ChecksumEthAddress, Location, Optional, Timestamp
+from rotkehlchen.user_messages import MessagesAggregator
 from rotkehlchen.utils.misc import ts_now
 
 logger = logging.getLogger(__name__)
@@ -39,6 +41,7 @@
 XPUB_DERIVATION_FREQUENCY = 3600  # every hour
 ETH_TX_QUERY_FREQUENCY = 3600  # every hour
 EXCHANGE_QUERY_FREQUENCY = 3600  # every hour
+PREMIUM_STATUS_CHECK = 3600  # every hour
 
 
 def noop_exchange_succes_cb(trades, margin, asset_movements, ledger_actions, exchange_specific_data) -> None:  # type: ignore # noqa: E501
@@ -67,6 +70,7 @@ def __init__(
             chain_manager: ChainManager,
             exchange_manager: ExchangeManager,
             evm_tx_decoder: EVMTransactionDecoder,
+            deactivate_premium: Callable,
     ) -> None:
         self.max_tasks_num = max_tasks_num
         self.greenlet_manager = greenlet_manager
@@ -88,6 +92,9 @@ def __init__(
             exception_is_error=True,
             method=self._prepare_cryptocompare_queries,
         )
+        self.deactivate_premium = deactivate_premium
+        self.last_premium_status_check = ts_now()
+        self.msg_aggregator = MessagesAggregator()
 
         self.potential_tasks = [
             self._maybe_schedule_cryptocompare_query,
@@ -97,6 +104,7 @@ def __init__(
             self._maybe_schedule_ethereum_txreceipts,
             self._maybe_query_missing_prices,
             self._maybe_decode_evm_transactions,
+            self._maybe_check_premium_status,
         ]
         if premium_sync_manager is not None:
             self.potential_tasks.append(premium_sync_manager.maybe_upload_data_to_server)
@@ -380,6 +388,29 @@ def _maybe_decode_evm_transactions(self) -> None:
                 tx_hashes=hashes,
             )
 
+    def _maybe_check_premium_status(self) -> None:
+        """
+        Validates the premium status of the account and if the credentials are not valid
+        it deactivates the user's premium status.
+        """
+        now = ts_now()
+        if now - self.last_premium_status_check < PREMIUM_STATUS_CHECK:
+            return
+
+        db_credentials = self.database.get_rotkehlchen_premium()
+        if db_credentials:
+            try:
+                premium_create_and_verify(db_credentials)
+            except PremiumAuthenticationError as e:
+                message = (
+                    f'Could not authenticate with the rotkehlchen server with '
+                    f'the API keys found in the Database. Error: {str(e)}. Will '
+                    f'deactivate the premium status.'
+                )
+                self.msg_aggregator.add_error(message)
+                self.deactivate_premium()
+        self.last_premium_status_check = now
+
     def _schedule(self) -> None:
         """Schedules background tasks"""
         self.greenlet_manager.clear_finished()


Test patch:
diff --git a/rotkehlchen/tests/unit/test_tasks_manager.py b/rotkehlchen/tests/unit/test_tasks_manager.py
--- a/rotkehlchen/tests/unit/test_tasks_manager.py
+++ b/rotkehlchen/tests/unit/test_tasks_manager.py
@@ -1,5 +1,5 @@
 from typing import List
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
 
 import gevent
 import pytest
@@ -9,8 +9,10 @@
 from rotkehlchen.chain.ethereum.transactions import EthTransactions
 from rotkehlchen.db.ethtx import DBEthTx
 from rotkehlchen.exchanges.manager import ExchangeManager
+from rotkehlchen.premium.premium import Premium, PremiumCredentials, SubscriptionStatus
 from rotkehlchen.tasks.manager import TaskManager
 from rotkehlchen.tests.utils.ethereum import setup_ethereum_transactions_test
+from rotkehlchen.tests.utils.premium import VALID_PREMIUM_KEY, VALID_PREMIUM_SECRET
 from rotkehlchen.types import Location
 from rotkehlchen.utils.hexbytes import hexstring_to_bytes
 from rotkehlchen.utils.misc import ts_now
@@ -61,6 +63,7 @@ def fixture_task_manager(
         chain_manager=blockchain,
         exchange_manager=exchange_manager,
         evm_tx_decoder=evm_transaction_decoder,
+        deactivate_premium=lambda: None,
     )
     return task_manager
 
@@ -198,3 +201,37 @@ def test_maybe_schedule_ethereum_txreceipts(task_manager, ethereum_manager, data
     assert receipt1 == receipts[0]
     receipt2 = txmodule.get_or_query_transaction_receipt(tx_hash_2)
     assert receipt2 == receipts[1]
+
+
+@pytest.mark.parametrize('max_tasks_num', [7])
+@pytest.mark.parametrize('start_with_valid_premium', [True])
+def test_check_premium_status(rotkehlchen_api_server):
+    """
+    Test that the premium check tasks works correctly. The tests creates a valid subscription
+    and verifies that after the task was scheduled the users premium is deactivated.
+    """
+    rotki = rotkehlchen_api_server.rest_api.rotkehlchen
+    gevent.killall(rotki.api_task_greenlets)
+    task_manager = rotki.task_manager
+    task_manager.potential_tasks = [task_manager._maybe_check_premium_status]
+    task_manager.last_premium_status_check = ts_now() - 3601
+
+    premium_credentials = PremiumCredentials(VALID_PREMIUM_KEY, VALID_PREMIUM_SECRET)
+    premium = Premium(premium_credentials)
+    premium.status = SubscriptionStatus.ACTIVE
+
+    with patch(
+        'rotkehlchen.db.dbhandler.DBHandler.get_rotkehlchen_premium',
+        MagicMock(return_value=premium_credentials),
+    ):
+        assert premium.is_active() is True
+        assert rotki.premium is not None
+
+        timeout = 5
+        try:
+            with gevent.Timeout(timeout):
+                task_manager.schedule()
+        except gevent.Timeout as e:
+            raise AssertionError(f'Premium check query was not completed within {timeout} seconds') from e  # noqa: E501
+
+        assert rotki.premium is None