diff --git a/tests/test_commands.py b/tests/test_commands.py index 6bafb4f..5710c11 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -124,6 +124,7 @@ def test_with_multiword_term(self, mock_commands): def test_quote(): with patch('tululbot.commands.quote_engine') as mock_engine: + mock_engine.refresh_cache_if_applicable.return_value = False mock_engine.retrieve_random.return_value = 'some random quote' rv = quote() assert rv == 'some random quote' diff --git a/tests/test_quoteengine.py b/tests/test_quoteengine.py new file mode 100644 index 0000000..a933816 --- /dev/null +++ b/tests/test_quoteengine.py @@ -0,0 +1,124 @@ +from datetime import datetime +from unittest.mock import patch +from unittest.mock import MagicMock + +import pytest + +from tululbot.utils import QuoteEngine + + +class FakeResponse: + def get_json(self): + return self.json + + pass + + +@pytest.fixture +def qe(): + return QuoteEngine() + + +def cdn_with_branch(branch): + return 'https://cdn.rawgit.com/tulul/tulul-quotes/{}/quote.yaml'.format(branch) # noqa + + +def test_refresh_time(qe): + with patch('tululbot.utils.TimeHelper') as mock_dt: + with patch('tululbot.utils.requests') as mock_requests: + response1 = FakeResponse() + response1.status_code = 200 + response1.json = { + "commit": { + "sha": "beefbeef" + } + } + + response1.ok = True + + mock_requests.get.return_value = response1 + + qe._refresh_interval = 5 + + mock_dt.now.return_value = datetime(2009, 1, 6, 15, 8, 30) + qe._last_refresh = datetime(2000, 1, 6, 15, 8, 24) + + assert qe._quote_url == cdn_with_branch("master") + + result = qe.refresh_url_if_applicable() + assert result is True + result = qe.refresh_url_if_applicable() + assert result is False + result = qe.refresh_url_if_applicable() + assert result is False + + assert qe._quote_url == cdn_with_branch("beefbeef") + + qe._last_refresh = datetime(2000, 1, 6, 15, 8, 24) + + result = qe.refresh_url_if_applicable() + assert result is True + result = qe.refresh_url_if_applicable() + assert result is False + + +def test_refresh_response(qe): + with patch('tululbot.utils.requests') as mock_requests: + + # Test: stay empty if no need to refresh + qe.refresh_url_if_applicable = MagicMock() + qe.refresh_url_if_applicable.return_value = False + + qe.refresh_cache_if_applicable() + + assert qe._cache == [] + + # Test: should refresh + qe.refresh_url_if_applicable.return_value = True + mock_requests.get.return_value = generic_quote_1 + qe.refresh_cache_if_applicable() + result = qe.retrieve_random() + assert result == 'Yuzu kok lucu banget sih - Waza, Pejuang' + + # Test: Remote content changed but no need to refresh + qe.refresh_url_if_applicable.return_value = False + mock_requests.get.return_value = generic_quote_2 + qe.refresh_cache_if_applicable() + result = qe.retrieve_random() + assert result == 'Yuzu kok lucu banget sih - Waza, Pejuang' + + # Test: Remote content changed and it does refresh + qe.refresh_url_if_applicable.return_value = True + mock_requests.get.return_value = generic_quote_2 + qe.refresh_cache_if_applicable() + result = qe.retrieve_random() + assert result == 'Miki Hoshii is the best girl - Waza, Rocket Builder' + + +generic_quote_1 = FakeResponse() +generic_quote_1.text = ''' +--- +quotes: \n + - q_no: 1 + quote: "Yuzu kok lucu banget sih" + author: Waza + author_bio: Pejuang + tags: + - cinta + - sahur + - ramadhan +''' + +generic_quote_2 = FakeResponse() +generic_quote_2.text = ''' +--- +quotes: + - q_no: 3 + quote: "Miki Hoshii is the best girl" + author: Waza + author_bio: Rocket Builder + tags: + - cinta + - geek + - matematika +''' diff --git a/tululbot/commands.py b/tululbot/commands.py index aacd376..30d8651 100644 --- a/tululbot/commands.py +++ b/tululbot/commands.py @@ -24,7 +24,6 @@ dispatcher = CommandDispatcher() quote_engine = QuoteEngine() -quote_engine.refresh_cache() @dispatcher.command(r'^/leli (?P.+)$') @@ -95,6 +94,7 @@ def search_on_google(): @dispatcher.command(r'^/quote$') def quote(): + quote_engine.refresh_cache_if_applicable() return quote_engine.retrieve_random() diff --git a/tululbot/utils.py b/tululbot/utils.py index 57bcfc8..4f15347 100644 --- a/tululbot/utils.py +++ b/tululbot/utils.py @@ -1,18 +1,38 @@ +from datetime import datetime import random import requests import yaml +class TimeHelper: + + @classmethod + def now(): + return datetime.datetime.now() + + class QuoteEngine: def __init__(self): - self._quote_url = 'https://cdn.rawgit.com/tulul/tulul-quotes/master/quote.yaml' # noqa # Note: rawgit does not have 100% uptime, but at # least they're not throttling us. + self._quote_url = 'https://cdn.rawgit.com/tulul/tulul-quotes/master/quote.yaml' # noqa + + # Why not using rawgit's development endpoint? Because + # they can't promise 100% uptime on the development endpoint. + # Meanwhile, production endpoint's uptime is better because + # it is served by MaxCDN + self._git_branch_check_url = 'https://api.github.com/repos/tulul/tulul-quotes/branches/master' # noqa self._cache = [] + # URI refresh per interval in seconds + self._refresh_interval = 5 * 60 + # Dummy date. Must be old enough (just to trigger) + # the uri must be refreshed on the first call + self._last_refresh = datetime(2009, 1, 6, 15, 8, 24, 78915) + def retrieve_random(self): cache = self._cache return self.format_quote(random.choice(cache)) @@ -20,9 +40,35 @@ def retrieve_random(self): def format_quote(self, q): return '{q[quote]} - {q[author]}, {q[author_bio]}'.format(q=q) - def refresh_cache(self): + def refresh_cache_if_applicable(self): + if (not self.refresh_url_if_applicable()): + return False + body = requests.get(self._quote_url).text - # What if previosuly we have the cache, but this time + # What if previously we have the cache, but this time # when we try to get new cache, the network occurs error? # We will think about "don't refresh if error" later. self._cache = yaml.load(body)['quotes'] + + return True + + def refresh_url_if_applicable(self): + now = TimeHelper.now() + delta = now - self._last_refresh + + if (delta.seconds < self._refresh_interval): + return False + + res = requests.get(self._git_branch_check_url) + + # Don't care broken request + if (not res.ok): + return False + + json = res.get_json() + sha = json['commit']['sha'] + self._quote_url = 'https://cdn.rawgit.com/tulul/tulul-quotes/{}/quote.yaml'.format(sha) + + self._last_refresh = now + + return True