def __init__(self, cardinal, config): self.logger = logging.getLogger(__name__) self.admins = [] if config is None or not config.get('admins', False): self.logger.warning("No admins configured for admin plugin -- " "copy config.example.json to config.json and " "add your information.") return for admin in config['admins']: user = user_info( admin.get('nick', None), admin.get('user', None), admin.get('vhost', None), ) if user.nick is None and user.user is None and user.vhost is None: self.logger.error( "Invalid admin listed in admin plugin config -- at least " "one of nick, user, or vhost must be present.") continue self.admins.append(user)
def test_on_part(): channel1 = '#channel1' channel2 = '#channel2' user = user_info('nick', None, None) msg = 'msg' plugin = SedPlugin() cardinal = Mock() plugin.on_msg(cardinal, user, channel1, msg) plugin.on_msg(cardinal, user, channel2, msg) assert plugin.history[channel1] == { user.nick: msg } assert plugin.history[channel2] == { user.nick: msg } plugin.on_part(cardinal, user, channel1, 'message') assert plugin.history[channel1] == {} assert plugin.history[channel2] == { user.nick: msg } plugin.on_part(cardinal, user, channel2, 'message') assert plugin.history[channel2] == {}
def __init__(self, cardinal, config): self.logger = logging.getLogger(__name__) self.cardinal = cardinal self.config = config or {} self.config.setdefault('api_key', None) self.config.setdefault('channels', []) self.config.setdefault('stocks', {}) self.config.setdefault('relay_bots', []) if not self.config["channels"]: self.logger.warning("No channels for ticker defined in config --" "ticker will be disabled") if not self.config["stocks"]: self.logger.warning("No stocks for ticker defined in config -- " "ticker will be disabled") if not self.config["api_key"]: raise KeyError("Missing required api_key in ticker config") if len(self.config["stocks"]) > 5: raise ValueError("No more than 5 stocks may be present in ticker " "config") self.relay_bots = [] for relay_bot in self.config['relay_bots']: user = user_info(relay_bot['nick'], relay_bot['user'], relay_bot['vhost']) self.relay_bots.append(user) self.db = cardinal.get_db('ticker', default={ 'predictions': {}, }) self.call_id = None self.wait()
def test_substitute_escaping(message, new_message): user = user_info('user', None, None) channel = '#channel' plugin = SedPlugin() plugin.history[channel][user.nick] = 'hi/hey/hello' assert plugin.substitute(user, channel, message) == new_message
def test_not_a_substitute(): user = user_info('user', None, None) channel = '#channel' plugin = SedPlugin() plugin.history[channel][user.nick] = 'doesnt matter' assert plugin.substitute(user, channel, 'foobar') == None
def test_subsitution_no_history(): user = user_info('user', None, None) channel = '#channel' plugin = SedPlugin() # make sure this doesn't raise plugin.on_msg(Mock(), user, channel, 's/foo/bar/')
def test_substitute_modifiers(message, new_message): user = user_info('user', None, None) channel = '#channel' plugin = SedPlugin() plugin.history[channel][user.nick] = 'this is a test message' assert plugin.substitute(user, channel, message) == new_message
def test_substitution_doesnt_match(): user = user_info('user', None, None) channel = '#channel' plugin = SedPlugin() plugin.history[channel][user.nick] = 'doesnt matter' assert plugin.substitute(user, channel, 's/foo/bar/') == 'doesnt matter'
def test_predict_not_relay_bot(self, input_msg): channel = "#finance" yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert len(self.db['predictions']) == 0 assert self.mock_cardinal.sendMsg.mock_calls == []
def test_on_kick_no_history(): channel = '#channel' user = user_info('nick', None, None) plugin = SedPlugin() cardinal = Mock() # make sure this doesn't raise plugin.on_kick(cardinal, user, channel, user.nick, 'message')
def test_on_quit_no_history(): channel = '#channel' user = user_info('nick', None, None) plugin = SedPlugin() assert plugin.history[channel] == {} cardinal = Mock() # make sure this doesn't raise plugin.on_quit(cardinal, user, 'message') assert plugin.history[channel] == {}
def test_on_part_self_no_history(): cardinal = Mock() cardinal.nickname = 'Cardinal' channel = '#channel' user = user_info(cardinal.nickname, None, None) plugin = SedPlugin() # make sure this doesn't raise plugin.on_part(cardinal, user, channel, 'message')
def test_on_msg_failed_correction(): user = user_info('user', None, None) channel = '#channel' plugin = SedPlugin() cardinal = Mock() plugin.history[channel][user.nick] = 'doesnt matter' # make sure this doesn't raise plugin.on_msg(cardinal, user, channel, 's/foo/bar/') cardinal.sendMsg.assert_not_called()
def setup_method(self): self.channel = '#cah' self.player = 'player1' self.user = user_info(self.player, 'user', 'vhost') self.mock_cardinal = Mock(spec=CardinalBot) self.mock_cardinal.nickname = 'Cardinal' self.plugin = CAHPlugin(self.mock_cardinal, {'channel': self.channel}) self.plugin.game = game.Game() self.plugin.game.add_player(self.player)
def test_predict_relay_bot(self, input_msg, output_msg): symbol = 'INX' channel = "#finance" response = make_global_quote_response(symbol, previous_close=100) with mock_api(response): yield self.plugin.predict(self.mock_cardinal, user_info("relay.bot", "relay", "relay"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_once_with(channel, output_msg)
def predict_relayed(self, cardinal, user, channel, msg): """Hack to support relayed messages""" match = re.match(PREDICT_RELAY_REGEX, msg) # this regex should only match when a relay bot is relaying a message # for another user - make sure this is really a relay bot if not self.is_relay_bot(user): return user = user_info(util.strip_formatting(match.group(1)), user.user, user.vhost, ) yield self.predict(cardinal, user, channel, match.group(2))
def test_on_msg_failed_correction(): user = user_info('user', None, None) channel = '#channel' plugin = SedPlugin() cardinal = Mock() plugin.history[channel][user.nick] = 'yo, foo matters' # make sure this doesn't raise plugin.on_msg(cardinal, user, channel, 's/foo/bar/') cardinal.sendMsg.assert_called_with( channel, "{} meant: yo bar matters".format(nick), )
def test_predict(self, symbol, input_msg, output_msg, market_is_open): channel = "#finance" fake_now = get_fake_now(market_is_open=market_is_open) kwargs = {'previous_close': 100} if market_is_open else {'close': 100} response = make_global_quote_response(symbol, **kwargs) with mock_api(response, fake_now=fake_now): yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_once_with(channel, output_msg)
def test_cmd_info(self, mock_datetime): channel = '#test' msg = '.info' now = datetime.now() reloads = 123 mock_datetime.now = Mock(return_value=now) type(self.mock_cardinal).reloads = PropertyMock(return_value=123) self.mock_cardinal.booted = now - timedelta(seconds=30) self.mock_cardinal.uptime = now - timedelta(seconds=15) self.mock_cardinal.config.return_value = { 'admins': [ { 'nick': 'whoami' }, { 'nick': 'test_foo' }, ] } self.plugin.cmd_info( self.mock_cardinal, user_info(None, None, None), channel, msg, ) assert self.mock_cardinal.sendMsg.mock_calls == [ call( channel, "I have been connected without downtime for 00:00:15, and was " "initially launched 00:00:30 ago. Plugins have been reloaded " "123 times since then.".format(reloads), ), call( channel, "My admins are: test_foo, whoami. Visit " "https://github.com/JohnMaguire/Cardinal for more info about " "me. (Use .help to see my commands.)", ), ]
def test_predict_replace(self, message_pairs): channel = "#finance" symbol = 'INX' response = make_global_quote_response(symbol, previous_close=100) fake_now = get_fake_now() for input_msg, output_msg in message_pairs: with mock_api(response, fake_now): yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_with( channel, output_msg.format( fake_now.strftime('%Y-%m-%d %H:%M:%S %Z')) if '{}' in output_msg else output_msg)
def test_admins_translation(self): plugin = AdminPlugin( None, { 'admins': [ { 'nick': 'nick', 'user': '******', 'vhost': 'vhost' }, { 'nick': 'nick', 'user': '******' }, { 'nick': 'nick', 'vhost': 'vhost' }, { 'user': '******', 'vhost': 'vhost' }, { 'nick': 'nick' }, { 'user': '******' }, { 'vhost': 'vhost' }, ] }) assert plugin.admins == [ user_info('nick', 'user', 'vhost'), user_info('nick', 'user', None), user_info('nick', None, 'vhost'), user_info(None, 'user', 'vhost'), user_info('nick', None, None), user_info(None, 'user', None), user_info(None, None, 'vhost'), ]
def test_cmd_info(self, mock_datetime): channel = '#test' msg = '.info' now = datetime.now() mock_datetime.now = Mock(return_value=now) self.mock_cardinal.booted = now - timedelta(seconds=30) self.mock_cardinal.uptime = now - timedelta(seconds=15) self.mock_cardinal.config.return_value = { 'admins': [ { 'nick': 'whoami' }, { 'nick': 'test_foo' }, ] } self.plugin.cmd_info( self.mock_cardinal, user_info(None, None, None), channel, msg, ) assert self.mock_cardinal.sendMsg.mock_calls == [ call( channel, "I am a Python 3 IRC bot, online since 00:00:15. I initially " "connected 00:00:30 ago. My admins are: test_foo, whoami. " "Use .help to list commands."), call( channel, "Visit https://github.com/JohnMaguire/Cardinal to learn more." ), ]
class TestTickerPlugin: @pytest.fixture(autouse=True) def setup_method_fixture(self, request, tmpdir): self.api_key = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' self.channel = '#test' self.channels = [self.channel] self.stocks = [ ['SPY', 'S&P 500'], ['DIA', 'Dow'], ['VEU', 'Foreign'], ['AGG', 'US Bond'], ] self.relay_bots = [ {"nick": "relay.bot", "user": "******", "vhost": "relay"}, ] d = tmpdir.mkdir('storage') get_db, self.db = get_mock_db() self.mock_cardinal = Mock(spec=CardinalBot) self.mock_cardinal.network = self.network = 'irc.darkscience.net' self.mock_cardinal.storage_path = str(d.dirpath()) self.mock_cardinal.get_db.side_effect = get_db self.plugin = TickerPlugin(self.mock_cardinal, { 'api_key': self.api_key, 'channels': self.channels, 'stocks': self.stocks, 'relay_bots': self.relay_bots, }) def test_config_defaults(self): plugin = TickerPlugin(self.mock_cardinal, { 'api_key': self.api_key, }) assert plugin.config['api_key'] == self.api_key assert plugin.config['channels'] == [] assert plugin.config['stocks'] == [] assert plugin.config['relay_bots'] == [] def test_missing_api_key(self): with pytest.raises(KeyError): TickerPlugin(self.mock_cardinal, {}) def test_missing_stocks(self): with pytest.raises(ValueError): TickerPlugin(self.mock_cardinal, { 'api_key': self.api_key, 'stocks': [ ['a', 'a'], ['b', 'b'], ['c', 'c'], ['d', 'd'], ['e', 'e'], ['f', 'f'], ], }) @defer.inlineCallbacks def test_send_ticker(self): responses = [ make_iex_response('DIA', previous_close=100, price=200), make_iex_response('AGG', previous_close=100, price=150.50), make_iex_response('VEU', previous_close=100, price=105), make_iex_response('SPY', previous_close=100, price=50), ] with mock_api(responses, fake_now=get_fake_now(market_is_open=True)): yield self.plugin.send_ticker() # These should be ordered per the config self.mock_cardinal.sendMsg.assert_called_once_with( self.channel, 'S&P 500 (\x02SPY\x02): \x0304-50.00%\x03 | ' 'Dow (\x02DIA\x02): \x0309100.00%\x03 | ' 'Foreign (\x02VEU\x02): \x03095.00%\x03 | ' 'US Bond (\x02AGG\x02): \x030950.50%\x03' ) @pytest.mark.parametrize("dt,should_send_ticker,should_do_predictions", [ (datetime.datetime(2020, 3, 21, 16, 0, 0), # Saturday 4pm False, False,), (datetime.datetime(2020, 3, 22, 16, 0, 0), # Sunday 4pm False, False,), (datetime.datetime(2020, 3, 23, 15, 45, 45), # Monday 3:45pm True, False,), (datetime.datetime(2020, 3, 23, 16, 0, 30), # Monday 4pm True, True,), (datetime.datetime(2020, 3, 23, 16, 15, 0), # Monday 4:15pm False, False,), (datetime.datetime(2020, 3, 27, 9, 15, 0), # Friday 9:15am False, False,), (datetime.datetime(2020, 3, 27, 9, 30, 15), # Friday 9:30am True, True,), (datetime.datetime(2020, 3, 27, 9, 45, 15), # Friday 9:45am True, False,), ]) @patch.object(plugin.TickerPlugin, 'do_predictions') @patch.object(plugin.TickerPlugin, 'send_ticker') @patch.object(util, 'sleep') @patch.object(plugin, 'est_now') @pytest_twisted.inlineCallbacks def test_tick(self, est_now, sleep, send_ticker, do_predictions, dt, should_send_ticker, should_do_predictions): est_now.return_value = dt yield self.plugin.tick() if should_send_ticker: send_ticker.assert_called_once_with() else: assert send_ticker.mock_calls == [] if should_do_predictions: sleep.assert_called_once_with(60) do_predictions.assert_called_once_with() else: assert sleep.mock_calls == [] assert do_predictions.mock_calls == [] @pytest.mark.parametrize("market_is_open", [True, False]) @patch.object(util, 'reactor', new_callable=Clock) @pytest_twisted.inlineCallbacks def test_do_predictions(self, mock_reactor, market_is_open): symbol = 'SPY' base = 100.0 user1 = 'user1' user2 = 'user2' prediction1 = 105.0 prediction2 = 96.0 actual = 95.0 yield self.plugin.save_prediction( symbol, user1, base, prediction1, ) yield self.plugin.save_prediction( symbol, user2, base, prediction2, ) assert len(self.db['predictions']) == 1 assert len(self.db['predictions'][symbol]) == 2 response = make_iex_response(symbol, price=actual) with mock_api(response, fake_now=get_fake_now(market_is_open)): d = self.plugin.do_predictions() mock_reactor.advance(15) yield d assert len(self.mock_cardinal.sendMsg.mock_calls) == 3 self.mock_cardinal.sendMsg.assert_called_with( self.channel, '{} had the closest guess for \x02{}\x02 out of {} predictions ' 'with a prediction of {:.2f} (\x0304{:.2f}%\x03) ' 'compared to the actual {} of {:.2f} (\x0304{:.2f}%\x03).'.format( user2, symbol, 2, prediction2, -4, 'open' if market_is_open else 'close', actual, -5)) @patch.object(plugin, 'est_now') def test_send_prediction(self, mock_now): prediction = 105 actual = 110 base = 100 nick = "nick" symbol = "SPY" # Set the datetime to a known value so the message can be tested tz = pytz.timezone('America/New_York') mock_now.return_value = tz.localize( datetime.datetime(2020, 3, 20, 10, 50, 0, 0)) prediction_ = {'when': '2020-03-20 10:50:00 EDT', 'prediction': prediction, 'base': base, } self.plugin.send_prediction(nick, symbol, prediction_, actual) message = ("Prediction by nick for \x02SPY\02: 105.00 (\x03095.00%\x03). " "Actual value at open: 110.00 (\x030910.00%\x03). " "Prediction set at 2020-03-20 10:50:00 EDT.") self.mock_cardinal.sendMsg.assert_called_once_with('#test', message) @pytest.mark.skip(reason="Not written yet") def test_check(self): pass @pytest.mark.parametrize("symbol,input_msg,output_msg,market_is_open", [ ("SPY", "!predict SPY +5%", "Prediction by nick for \x02SPY\x02 at market close: 105.00 (\x03095.00%\x03) ", True, ), ("SPY", "!predict SPY -5%", "Prediction by nick for \x02SPY\x02 at market close: 95.00 (\x0304-5.00%\x03) ", True, ), ("SPY", "!predict SPY -5%", "Prediction by nick for \x02SPY\x02 at market open: 95.00 (\x0304-5.00%\x03) ", False, ), # testing a few more formats of stock symbols ("^RUT", "!predict ^RUT -5%", "Prediction by nick for \x02^RUT\x02 at market open: 95.00 (\x0304-5.00%\x03) ", False, ), ("REE.MC", "!predict REE.MC -5%", "Prediction by nick for \x02REE.MC\x02 at market open: 95.00 (\x0304-5.00%\x03) ", False, ), ("LON:HDLV", "!predict LON:HDLV -5%", "Prediction by nick for \x02LON:HDLV\x02 at market open: 95.00 (\x0304-5.00%\x03) ", False, ), ]) @pytest_twisted.inlineCallbacks def test_predict(self, symbol, input_msg, output_msg, market_is_open): channel = "#finance" fake_now = get_fake_now(market_is_open=market_is_open) kwargs = {'previous_close': 100} if market_is_open else {'price': 100} response = make_iex_response(symbol, **kwargs) with mock_api(response, fake_now=fake_now): yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_once_with( channel, output_msg) @pytest.mark.parametrize("message_pairs", [ (("!predict SPY +5%", "Prediction by nick for \x02SPY\x02 at market close: 105.00 (\x03095.00%\x03) ", ), ("!predict SPY -5%", "Prediction by nick for \x02SPY\x02 at market close: 95.00 (\x0304-5.00%\x03) " "(replaces old prediction of 105.00 (\x03095.00%\x03) set at {})" ), ) ]) @pytest_twisted.inlineCallbacks def test_predict_replace(self, message_pairs): channel = "#finance" symbol = 'SPY' response = make_iex_response(symbol, previous_close=100) fake_now = get_fake_now() for input_msg, output_msg in message_pairs: with mock_api(response, fake_now): yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_with( channel, output_msg.format(fake_now.strftime('%Y-%m-%d %H:%M:%S %Z')) if '{}' in output_msg else output_msg) @pytest.mark.parametrize("input_msg,output_msg", [ ("<nick> !predict SPY +5%", "Prediction by nick for \x02SPY\x02 at market close: 105.00 (\x03095.00%\x03) ", ), ("<nick> !predict SPY -5%", "Prediction by nick for \x02SPY\x02 at market close: 95.00 (\x0304-5.00%\x03) ", ), ]) @pytest_twisted.inlineCallbacks def test_predict_relay_bot(self, input_msg, output_msg): symbol = 'SPY' channel = "#finance" response = make_iex_response(symbol, previous_close=100) with mock_api(response): yield self.plugin.predict(self.mock_cardinal, user_info("relay.bot", "relay", "relay"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_once_with( channel, output_msg) @pytest.mark.parametrize("input_msg", [ "<whoami> !predict SPY +5%", "<whoami> !predict SPY -5%", ]) @pytest_twisted.inlineCallbacks def test_predict_not_relay_bot(self, input_msg): channel = "#finance" yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert len(self.db['predictions']) == 0 assert self.mock_cardinal.sendMsg.mock_calls == [] @pytest.mark.parametrize("user,message,value,expected", [ ( user_info("whoami", None, None), "!predict SPY 5%", 100, ("whoami", "SPY", 105, 100), ), ( user_info("whoami", None, None), "!predict SPY +5%", 100, ("whoami", "SPY", 105, 100), ), ( user_info("whoami", None, None), "!predict SPY -5%", 100, ("whoami", "SPY", 95, 100), ), ( user_info("not.a.relay.bot", None, None), "<whoami> !predict SPY -5%", 100, None, ), ( user_info("relay.bot", "relay", "relay"), "<whoami> !predict SPY -5%", 100, ("whoami", "SPY", 95, 100), ), ]) @pytest_twisted.inlineCallbacks def test_parse_prediction_open( self, user, message, value, expected, ): symbol = 'SPY' response = make_iex_response(symbol, previous_close=value) with mock_api(response): result = yield self.plugin.parse_prediction(user, message) assert result == expected @pytest.mark.parametrize("user,message,value,expected", [ ( user_info("whoami", None, None), "!predict SPY 500", 100, ("whoami", "SPY", 500, 100), ), ( user_info("whoami", None, None), "!predict SPY $100", 100, ("whoami", "SPY", 100, 100), ), ( user_info("relay.bot", "relay", "relay"), "<whoami> !predict SPY 500", 100, ("whoami", "SPY", 500, 100), ), ]) @pytest_twisted.inlineCallbacks def test_parse_prediction_open_dollar_amount( self, user, message, value, expected, ): symbol = 'SPY' response = make_iex_response(symbol, previous_close=value) with mock_api(response): result = yield self.plugin.parse_prediction(user, message) assert result == expected @pytest.mark.parametrize("user,message,value,expected", [ ( user_info("whoami", None, None), "!predict SPY 5%", 100, ("whoami", "SPY", 105, 100), ), ( user_info("whoami", None, None), "!predict SPY +5%", 100, ("whoami", "SPY", 105, 100), ), ( user_info("whoami", None, None), "!predict SPY -5%", 100, ("whoami", "SPY", 95, 100), ), ( user_info("not.a.relay.bot", None, None), "<whoami> !predict SPY -5%", 100, None, ), ( user_info("relay.bot", "relay", "relay"), "<whoami> !predict SPY -5%", 100, ("whoami", "SPY", 95, 100), ), ]) @pytest_twisted.inlineCallbacks def test_parse_prediction_close( self, user, message, value, expected, ): symbol = 'SPY' response = make_iex_response(symbol, price=value) with mock_api(response, fake_now=get_fake_now(market_is_open=False)): result = yield self.plugin.parse_prediction(user, message) assert result == expected @patch.object(plugin, 'est_now') def test_save_prediction(self, mock_now): symbol = 'SPY' nick = 'whoami' base = 100 prediction = 105 tz = pytz.timezone('America/New_York') mock_now.return_value = tz.localize(datetime.datetime( 2020, 3, 23, 12, 0, 0, )) self.plugin.save_prediction( symbol, nick, base, prediction, ) assert symbol in self.db['predictions'] assert nick in self.db['predictions'][symbol] actual = self.db['predictions'][symbol][nick] assert actual == { 'when': '2020-03-23 12:00:00 EDT', 'base': base, 'prediction': prediction, } @defer.inlineCallbacks def test_get_daily(self): symbol = 'SPY' price = 101.0 previous_close = 102.0 response = make_iex_response(symbol, price=price, previous_close=previous_close, ) expected = { 'symbol': symbol, 'price': price, 'previous close': previous_close, # this one is calculated by our mock response function so it # doesn't really test anything anymore 'change': ((price - previous_close) / previous_close) * 100, } with mock_api(response): result = yield self.plugin.get_daily(symbol) assert result == expected @patch.object(plugin, 'est_now') def test_market_is_open(self, mock_now): tz = pytz.timezone('America/New_York') # Nothing special about this time - it's a Thursday 7:49pm mock_now.return_value = tz.localize(datetime.datetime( 2020, 3, 19, 19, 49, 55, 0, )) assert plugin.market_is_open() is False # The market was open earlier though mock_now.return_value = tz.localize(datetime.datetime( 2020, 3, 19, 13, 49, 55, 0, )) assert plugin.market_is_open() is True # But not before 9:30am mock_now.return_value = tz.localize(datetime.datetime( 2020, 3, 19, 9, 29, 59, 0, )) assert plugin.market_is_open() is False # Or this weekend mock_now.return_value = tz.localize(datetime.datetime( 2020, 3, 14, 13, 49, 55, 0, )) assert plugin.market_is_open() is False
def get_user(): user = user_info('nick', 'user', 'vhost') return "{}!{}@{}".format(user.nick, user.user, user.vhost), user
class TestTickerPlugin(object): @pytest.fixture(autouse=True) def setup_method_fixture(self, request, tmpdir): self.api_key = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' self.channel = '#test' self.channels = [self.channel] self.stocks = { 'INX': 'S&P 500', 'DJI': 'Dow', 'VEU': 'Foreign', 'AGG': 'US Bond', } self.relay_bots = [ { "nick": "relay.bot", "user": "******", "vhost": "relay" }, ] d = tmpdir.mkdir('storage') get_db, self.db = get_mock_db() self.mock_cardinal = Mock(spec=CardinalBot) self.mock_cardinal.network = self.network = 'irc.darkscience.net' self.mock_cardinal.storage_path = str(d.dirpath()) self.mock_cardinal.get_db.side_effect = get_db self.plugin = TickerPlugin( self.mock_cardinal, { 'api_key': self.api_key, 'channels': self.channels, 'stocks': self.stocks, 'relay_bots': self.relay_bots, }) def test_config_defaults(self): plugin = TickerPlugin(self.mock_cardinal, { 'api_key': self.api_key, }) assert plugin.config['api_key'] == self.api_key assert plugin.config['channels'] == [] assert plugin.config['stocks'] == {} assert plugin.config['relay_bots'] == [] def test_missing_api_key(self): with pytest.raises(KeyError): TickerPlugin(self.mock_cardinal, {}) def test_missing_stocks(self): with pytest.raises(ValueError): TickerPlugin( self.mock_cardinal, { 'api_key': self.api_key, 'stocks': { 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', }, }) @defer.inlineCallbacks def test_send_ticker(self): responses = [ make_global_quote_response('DJI', previous_close=100, close=200), make_global_quote_response('AGG', previous_close=100, close=150.50), make_global_quote_response('VEU', previous_close=100, close=105), make_global_quote_response('INX', previous_close=100, close=50), ] with mock_api(responses, fake_now=get_fake_now(market_is_open=True)): yield self.plugin.send_ticker() self.mock_cardinal.sendMsg.assert_called_once_with( self.channel, 'Dow (\x02DJI\x02): \x0309100.00%\x03 | ' 'Foreign (\x02VEU\x02): \x03095.00%\x03 | ' 'S&P 500 (\x02INX\x02): \x0304-50.00%\x03 | ' 'US Bond (\x02AGG\x02): \x030950.50%\x03') @pytest.mark.parametrize( "dt,should_send_ticker,should_do_predictions", [ ( datetime.datetime(2020, 3, 21, 16, 0, 0), # Saturday 4pm False, False, ), ( datetime.datetime(2020, 3, 22, 16, 0, 0), # Sunday 4pm False, False, ), ( datetime.datetime(2020, 3, 23, 15, 45, 45), # Monday 3:45pm True, False, ), ( datetime.datetime(2020, 3, 23, 16, 0, 30), # Monday 4pm True, True, ), ( datetime.datetime(2020, 3, 23, 16, 15, 0), # Monday 4:15pm False, False, ), ( datetime.datetime(2020, 3, 27, 9, 15, 0), # Friday 9:15am False, False, ), ( datetime.datetime(2020, 3, 27, 9, 30, 15), # Friday 9:30am True, True, ), ( datetime.datetime(2020, 3, 27, 9, 45, 15), # Friday 9:45am True, False, ), ]) @patch.object(plugin.TickerPlugin, 'do_predictions') @patch.object(plugin.TickerPlugin, 'send_ticker') @patch.object(util, 'sleep') @patch.object(plugin, 'est_now') @pytest_twisted.inlineCallbacks def test_tick(self, est_now, sleep, send_ticker, do_predictions, dt, should_send_ticker, should_do_predictions): est_now.return_value = dt yield self.plugin.tick() if should_send_ticker: send_ticker.assert_called_once_with() else: assert send_ticker.mock_calls == [] if should_do_predictions: sleep.assert_called_once_with(60) do_predictions.assert_called_once_with() else: assert sleep.mock_calls == [] assert do_predictions.mock_calls == [] @pytest.mark.parametrize("market_is_open", [True, False]) @patch.object(util, 'reactor', new_callable=Clock) @pytest_twisted.inlineCallbacks def test_do_predictions(self, mock_reactor, market_is_open): symbol = 'INX' base = 100.0 user1 = 'user1' user2 = 'user2' prediction1 = 105.0 prediction2 = 96.0 actual = 95.0 yield self.plugin.save_prediction( symbol, user1, base, prediction1, ) yield self.plugin.save_prediction( symbol, user2, base, prediction2, ) assert len(self.db['predictions']) == 1 assert len(self.db['predictions'][symbol]) == 2 response = make_global_quote_response(symbol, close=actual) with mock_api(response, fake_now=get_fake_now(market_is_open)): d = self.plugin.do_predictions() mock_reactor.advance(15) yield d assert len(self.mock_cardinal.sendMsg.mock_calls) == 3 self.mock_cardinal.sendMsg.assert_called_with( self.channel, '{} had the closest guess for \x02{}\x02 out of {} predictions ' 'with a prediction of {} (\x0304{:.2f}%\x03) ' 'compared to the actual {} of {} (\x0304{:.2f}%\x03).'.format( user2, symbol, 2, prediction2, -4, 'open' if market_is_open else 'close', actual, -5)) @patch.object(plugin, 'est_now') def test_send_prediction(self, mock_now): prediction = 105 actual = 110 base = 100 nick = "nick" symbol = "INX" # Set the datetime to a known value so the message can be tested tz = pytz.timezone('America/New_York') mock_now.return_value = tz.localize( datetime.datetime(2020, 3, 20, 10, 50, 0, 0)) prediction_ = { 'when': '2020-03-20 10:50:00 EDT', 'prediction': prediction, 'base': base, } self.plugin.send_prediction(nick, symbol, prediction_, actual) message = ("Prediction by nick for \x02INX\02: 105 (\x03095.00%\x03). " "Actual value at open: 110 (\x030910.00%\x03). " "Prediction set at 2020-03-20 10:50:00 EDT.") self.mock_cardinal.sendMsg.assert_called_once_with('#test', message) @pytest.mark.skip(reason="Not written yet") def test_check(self): pass @pytest.mark.parametrize( "symbol,input_msg,output_msg,market_is_open", [ ( "INX", "!predict INX +5%", "Prediction by nick for \x02INX\x02 at market close: 105.00 (\x03095.00%\x03) ", True, ), ( "INX", "!predict INX -5%", "Prediction by nick for \x02INX\x02 at market close: 95.00 (\x0304-5.00%\x03) ", True, ), ( "INX", "!predict INX -5%", "Prediction by nick for \x02INX\x02 at market open: 95.00 (\x0304-5.00%\x03) ", False, ), # testing a few more formats of stock symbols ( "^RUT", "!predict ^RUT -5%", "Prediction by nick for \x02^RUT\x02 at market open: 95.00 (\x0304-5.00%\x03) ", False, ), ( "REE.MC", "!predict REE.MC -5%", "Prediction by nick for \x02REE.MC\x02 at market open: 95.00 (\x0304-5.00%\x03) ", False, ), ( "LON:HDLV", "!predict LON:HDLV -5%", "Prediction by nick for \x02LON:HDLV\x02 at market open: 95.00 (\x0304-5.00%\x03) ", False, ), ]) @pytest_twisted.inlineCallbacks def test_predict(self, symbol, input_msg, output_msg, market_is_open): channel = "#finance" fake_now = get_fake_now(market_is_open=market_is_open) kwargs = {'previous_close': 100} if market_is_open else {'close': 100} response = make_global_quote_response(symbol, **kwargs) with mock_api(response, fake_now=fake_now): yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_once_with(channel, output_msg) @pytest.mark.parametrize("message_pairs", [( ( "!predict INX +5%", "Prediction by nick for \x02INX\x02 at market close: 105.00 (\x03095.00%\x03) ", ), ("!predict INX -5%", "Prediction by nick for \x02INX\x02 at market close: 95.00 (\x0304-5.00%\x03) " "(replaces old prediction of 105.00 (\x03095.00%\x03) set at {})"), )]) @pytest_twisted.inlineCallbacks def test_predict_replace(self, message_pairs): channel = "#finance" symbol = 'INX' response = make_global_quote_response(symbol, previous_close=100) fake_now = get_fake_now() for input_msg, output_msg in message_pairs: with mock_api(response, fake_now): yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_with( channel, output_msg.format( fake_now.strftime('%Y-%m-%d %H:%M:%S %Z')) if '{}' in output_msg else output_msg) @pytest.mark.parametrize("input_msg,output_msg", [ ( "<nick> !predict INX +5%", "Prediction by nick for \x02INX\x02 at market close: 105.00 (\x03095.00%\x03) ", ), ( "<nick> !predict INX -5%", "Prediction by nick for \x02INX\x02 at market close: 95.00 (\x0304-5.00%\x03) ", ), ]) @pytest_twisted.inlineCallbacks def test_predict_relay_bot(self, input_msg, output_msg): symbol = 'INX' channel = "#finance" response = make_global_quote_response(symbol, previous_close=100) with mock_api(response): yield self.plugin.predict(self.mock_cardinal, user_info("relay.bot", "relay", "relay"), channel, input_msg) assert symbol in self.db['predictions'] assert len(self.db['predictions'][symbol]) == 1 self.mock_cardinal.sendMsg.assert_called_once_with(channel, output_msg) @pytest.mark.parametrize("input_msg", [ "<whoami> !predict INX +5%", "<whoami> !predict INX -5%", ]) @pytest_twisted.inlineCallbacks def test_predict_not_relay_bot(self, input_msg): channel = "#finance" yield self.plugin.predict(self.mock_cardinal, user_info("nick", "user", "vhost"), channel, input_msg) assert len(self.db['predictions']) == 0 assert self.mock_cardinal.sendMsg.mock_calls == [] @pytest.mark.parametrize("user,message,value,expected", [ ( user_info("whoami", None, None), "!predict INX 5%", 100, ("whoami", "INX", 105, 100), ), ( user_info("whoami", None, None), "!predict INX +5%", 100, ("whoami", "INX", 105, 100), ), ( user_info("whoami", None, None), "!predict INX -5%", 100, ("whoami", "INX", 95, 100), ), ( user_info("not.a.relay.bot", None, None), "<whoami> !predict INX -5%", 100, None, ), ( user_info("relay.bot", "relay", "relay"), "<whoami> !predict INX -5%", 100, ("whoami", "INX", 95, 100), ), ]) @pytest_twisted.inlineCallbacks def test_parse_prediction_open( self, user, message, value, expected, ): symbol = 'INX' response = make_global_quote_response(symbol, previous_close=value) with mock_api(response): result = yield self.plugin.parse_prediction(user, message) assert result == expected @pytest.mark.parametrize("user,message,value,expected", [ ( user_info("whoami", None, None), "!predict INX 5%", 100, ("whoami", "INX", 105, 100), ), ( user_info("whoami", None, None), "!predict INX +5%", 100, ("whoami", "INX", 105, 100), ), ( user_info("whoami", None, None), "!predict INX -5%", 100, ("whoami", "INX", 95, 100), ), ( user_info("not.a.relay.bot", None, None), "<whoami> !predict INX -5%", 100, None, ), ( user_info("relay.bot", "relay", "relay"), "<whoami> !predict INX -5%", 100, ("whoami", "INX", 95, 100), ), ]) @pytest_twisted.inlineCallbacks def test_parse_prediction_close( self, user, message, value, expected, ): symbol = 'INX' response = make_global_quote_response(symbol, close=value) with mock_api(response, fake_now=get_fake_now(market_is_open=False)): result = yield self.plugin.parse_prediction(user, message) assert result == expected @patch.object(plugin, 'est_now') def test_save_prediction(self, mock_now): symbol = 'INX' nick = 'whoami' base = 100 prediction = 105 tz = pytz.timezone('America/New_York') mock_now.return_value = tz.localize( datetime.datetime( 2020, 3, 23, 12, 0, 0, )) self.plugin.save_prediction( symbol, nick, base, prediction, ) assert symbol in self.db['predictions'] assert nick in self.db['predictions'][symbol] actual = self.db['predictions'][symbol][nick] assert actual == { 'when': '2020-03-23 12:00:00 EDT', 'base': base, 'prediction': prediction, } @defer.inlineCallbacks def test_get_quote(self): symbol = 'INX' response = make_global_quote_response(symbol) r = response["Global Quote"] expected = { 'symbol': symbol, 'open': float(r['02. open']), 'high': float(r['03. high']), 'low': float(r['04. low']), 'price': float(r['05. price']), 'volume': int(r['06. volume']), 'latest trading day': datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0), 'previous close': float(r['08. previous close']), 'change': float(r['09. change']), 'change percent': float(r['10. change percent'][:-1]), } with mock_api(response): result = yield self.plugin.get_quote(symbol) assert result == expected @defer.inlineCallbacks def test_get_daily(self): symbol = 'INX' last_open = 100.0 last_close = 101.0 previous_close = 102.0 response = make_global_quote_response( symbol, open_=last_open, close=last_close, previous_close=previous_close, ) expected = { 'symbol': symbol, 'close': last_close, 'open': last_open, 'previous close': previous_close, # this one is calculated by our make response function so it # doesn't really test anything anymore 'change': float('{:.4f}'.format(get_delta(last_close, previous_close))), } with mock_api(response): result = yield self.plugin.get_daily(symbol) assert result == expected @defer.inlineCallbacks def test_get_time_series_daily(self): symbol = 'INX' response = make_time_series_daily_response(symbol) with mock_api(response): result = yield self.plugin.get_time_series_daily(symbol) for date in response['Time Series (Daily)']: assert date in result # verify prefix is stripped and values are floats for key in ('open', 'high', 'low', 'close', 'volume'): assert key in result[date] assert isinstance(result[date][key], float) @defer.inlineCallbacks def test_get_time_series_daily_bad_format(self): symbol = 'INX' response = {} with mock_api(response): with pytest.raises(KeyError): yield self.plugin.get_time_series_daily(symbol) @defer.inlineCallbacks def test_make_av_request(self): # Verify that this returns the response unmodified, and that it # properly calculates params function = 'TIME_SERIES_DAILY' symbol = 'INX' outputsize = 'compact' response = make_time_series_daily_response(symbol) with mock_api(response) as defer_mock: result = yield self.plugin.make_av_request(function, params={ 'symbol': symbol, 'outputsize': outputsize, }) assert result == response defer_mock.assert_called_once_with(plugin.requests.get, plugin.AV_API_URL, params={ 'apikey': self.api_key, 'function': function, 'symbol': symbol, 'outputsize': outputsize, 'datatype': 'json', }) @defer.inlineCallbacks def test_make_av_request_no_params(self): # This one is mostly just for coverage function = 'TIME_SERIES_DAILY' symbol = 'INX' response = make_time_series_daily_response(symbol) with mock_api(response) as defer_mock: result = yield self.plugin.make_av_request(function) assert result == response defer_mock.assert_called_once_with(plugin.requests.get, plugin.AV_API_URL, params={ 'apikey': self.api_key, 'function': function, 'datatype': 'json', }) @patch.object(util, 'reactor', new_callable=Clock) @defer.inlineCallbacks def test_make_av_request_retry_when_throttled(self, mock_reactor): # Verify that this returns the response unmodified, and that it # properly calculates params function = 'TIME_SERIES_DAILY' symbol = 'INX' outputsize = 'compact' response = make_time_series_daily_response(symbol) throttle_times = plugin.MAX_RETRIES - 1 with mock_api(response, throttle_times=throttle_times) as defer_mock: d = self.plugin.make_av_request(function, params={ 'symbol': symbol, 'outputsize': outputsize, }) # loop through retries for _ in range(throttle_times): mock_reactor.advance(plugin.RETRY_WAIT) result = yield d assert result == response defer_mock.assert_has_calls([ call(plugin.requests.get, plugin.AV_API_URL, params={ 'apikey': self.api_key, 'function': function, 'symbol': symbol, 'outputsize': outputsize, 'datatype': 'json', }) ] * (throttle_times + 1)) @patch.object(util, 'reactor', new_callable=Clock) @defer.inlineCallbacks def test_make_av_request_retry_on_exception(self, mock_reactor): # Verify that this returns the response unmodified, and that it # properly calculates params function = 'TIME_SERIES_DAILY' symbol = 'INX' outputsize = 'compact' response = make_time_series_daily_response(symbol) raise_times = plugin.MAX_RETRIES - 1 with mock_api(response, raise_times=raise_times) as defer_mock: d = self.plugin.make_av_request(function, params={ 'symbol': symbol, 'outputsize': outputsize, }) # loop through retries for _ in range(raise_times): mock_reactor.advance(plugin.RETRY_WAIT) result = yield d assert result == response defer_mock.assert_has_calls([ call(plugin.requests.get, plugin.AV_API_URL, params={ 'apikey': self.api_key, 'function': function, 'symbol': symbol, 'outputsize': outputsize, 'datatype': 'json', }) ] * (raise_times + 1)) @patch.object(util, 'reactor', new_callable=Clock) @defer.inlineCallbacks def test_make_av_request_give_up_after_max_retries(self, mock_reactor): # Verify that this returns the response unmodified, and that it # properly calculates params function = 'TIME_SERIES_DAILY' symbol = 'INX' outputsize = 'compact' response = make_time_series_daily_response(symbol) raise_times = plugin.MAX_RETRIES with mock_api(response, raise_times=raise_times) as defer_mock: d = self.plugin.make_av_request(function, params={ 'symbol': symbol, 'outputsize': outputsize, }) # loop through retries for _ in range(raise_times): mock_reactor.advance(plugin.RETRY_WAIT) with pytest.raises(Exception): yield d defer_mock.assert_has_calls([ call(plugin.requests.get, plugin.AV_API_URL, params={ 'apikey': self.api_key, 'function': function, 'symbol': symbol, 'outputsize': outputsize, 'datatype': 'json', }) ] * (raise_times)) @patch.object(plugin, 'est_now') def test_market_is_open(self, mock_now): tz = pytz.timezone('America/New_York') # Nothing special about this time - it's a Thursday 7:49pm mock_now.return_value = tz.localize( datetime.datetime( 2020, 3, 19, 19, 49, 55, 0, )) assert plugin.market_is_open() is False # The market was open earlier though mock_now.return_value = tz.localize( datetime.datetime( 2020, 3, 19, 13, 49, 55, 0, )) assert plugin.market_is_open() is True # But not before 9:30am mock_now.return_value = tz.localize( datetime.datetime( 2020, 3, 19, 9, 29, 59, 0, )) assert plugin.market_is_open() is False # Or this weekend mock_now.return_value = tz.localize( datetime.datetime( 2020, 3, 14, 13, 49, 55, 0, )) assert plugin.market_is_open() is False
def test_is_admin(self): plugin = AdminPlugin(None, {'admins': [{'nick': 'nick'}]}) assert plugin.is_admin(user_info('nick', 'user', 'vhost')) is True assert plugin.is_admin(user_info('bad_nick', 'user', 'vhost')) is False plugin = AdminPlugin(None, {'admins': [{'user': '******'}]}) assert plugin.is_admin(user_info('nick', 'user', 'vhost')) is True assert plugin.is_admin(user_info('nick', 'bad_user', 'vhost')) is False plugin = AdminPlugin(None, {'admins': [{'vhost': 'vhost'}]}) assert plugin.is_admin(user_info('nick', 'user', 'vhost')) is True assert plugin.is_admin(user_info('nick', 'user', 'bad_vhost')) is False plugin = AdminPlugin(None, {'admins': [{ 'nick': 'nick', 'vhost': 'vhost' }]}) assert plugin.is_admin(user_info('nick', 'user', 'vhost')) is True assert plugin.is_admin(user_info('bad_nick', 'user', 'vhost')) is False assert plugin.is_admin(user_info('nick', 'user', 'bad_vhost')) is False plugin = AdminPlugin(None, {'admins': [{ 'user': '******', 'vhost': 'vhost' }]}) assert plugin.is_admin(user_info('nick', 'user', 'vhost')) is True assert plugin.is_admin(user_info('nick', 'bad_user', 'vhost')) is False assert plugin.is_admin(user_info('nick', 'user', 'bad_vhost')) is False plugin = AdminPlugin(None, {'admins': [{ 'nick': 'nick', 'user': '******' }]}) assert plugin.is_admin(user_info('nick', 'user', 'vhost')) is True assert plugin.is_admin(user_info('bad_nick', 'user', 'vhost')) is False assert plugin.is_admin(user_info('nick', 'bad_user', 'vhost')) is False plugin = AdminPlugin( None, {'admins': [{ 'nick': 'nick', 'user': '******', 'vhost': 'vhost' }]}) assert plugin.is_admin(user_info('nick', 'user', 'vhost')) is True assert plugin.is_admin(user_info('bad_nick', 'user', 'vhost')) is False assert plugin.is_admin(user_info('nick', 'bad_user', 'vhost')) is False assert plugin.is_admin(user_info('nick', 'user', 'bad_vhost')) is False