drbotzo-idlerpg/tests/test_idlerpg_ircplugin.py
Brian S. Stephan 56b554f6f1
add scaffolding to load the bot and lightly test it
better tests might be coming some time in the future, but I have the
whole IRC framework mocked out at the moment

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-06-02 00:19:06 -05:00

548 lines
24 KiB
Python

"""Test IdleRPG character operations.
SPDX-FileCopyrightText: © 2024 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
import datetime
import logging
import re
import unittest.mock as mock
from django.test import TestCase
from django.utils import timezone
from ircbot.models import IrcChannel, IrcServer
from idlerpg.ircplugin import IdleRPG
from idlerpg.models import Character, Game
class IrcPluginTest(TestCase):
"""Test the IRC integration."""
fixtures = ['tests/fixtures/simple_character.json']
def setUp(self):
"""Create common objects."""
self.mock_bot = mock.MagicMock()
self.mock_connection = mock.MagicMock()
self.mock_connection.get_nickname.return_value = 'test_bot'
self.mock_connection.server_config = IrcServer.objects.get(pk=1)
self.plugin = IdleRPG(self.mock_bot, self.mock_connection, mock.MagicMock())
self.game = Game.objects.get(pk=1)
def test_start_stop(self):
"""Test that handlers are registered, roughly, as expected."""
self.plugin.start()
self.plugin.stop()
assert self.mock_connection.add_global_handler.call_count == 8
assert (self.mock_connection.add_global_handler.call_count ==
self.mock_connection.remove_global_handler.call_count)
assert self.mock_connection.reactor.add_global_regex_handler.call_count == 5
assert (self.mock_connection.reactor.add_global_regex_handler.call_count ==
self.mock_connection.reactor.remove_global_regex_handler.call_count)
def test_kick_penalty(self):
"""Test that if a character is kicked from the game channel, they get penalized."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_kick_penalty'
mock_event.target = '#test'
mock_event.recursing = False
# make a character so as to not disturb other tests
test_char = Character.objects.register('testkickpen', self.game, 'test',
'bss!bss@test_kick_penalty', 'tester')
with mock.patch('idlerpg.models.Character.penalize', return_value=5) as mock_penalize:
with mock.patch('idlerpg.models.Character.log_out') as mock_log_out:
self.plugin.handle_kick_penalty(self.mock_connection, mock_event)
mock_penalize.assert_called_with(250, "getting kicked from the game channel")
mock_log_out.assert_called()
test_char = Character.objects.get(name='testkickpen')
assert test_char.time_penalized_kicked == 5
test_char.delete()
def test_generic_penalty_no_penalty_for_unknown(self):
"""Test that a kick from an unknown hostmask is ignored."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_kick_penalty'
mock_event.target = '#test'
mock_event.recursing = False
with mock.patch('idlerpg.models.Character.penalize', return_value=5) as mock_penalize:
with mock.patch('idlerpg.models.Character.log_out') as mock_log_out:
self.plugin.handle_kick_penalty(self.mock_connection, mock_event)
mock_penalize.assert_not_called()
mock_log_out.assert_not_called()
def test_nick_penalty(self):
"""Test that if a user changes their nick, they get penalized."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_nick_penalty'
mock_event.target = '#test'
mock_event.recursing = False
# make a character so as to not disturb other tests
test_char = Character.objects.register('testnickpen', self.game, 'test',
'bss!bss@test_nick_penalty', 'tester')
with mock.patch('idlerpg.models.Character.penalize', return_value=5) as mock_penalize:
with mock.patch('idlerpg.models.Character.log_out') as mock_log_out:
self.plugin.handle_nick_penalty(self.mock_connection, mock_event)
mock_penalize.assert_called_with(30, "changing their nick")
mock_log_out.assert_called()
test_char = Character.objects.get(name='testnickpen')
assert test_char.time_penalized_nick_change == 5
test_char.delete()
def test_part_penalty(self):
"""Test that if a character parts from the game channel, they get penalized."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_part_penalty'
mock_event.target = '#test'
mock_event.recursing = False
# make a character so as to not disturb other tests
test_char = Character.objects.register('testpartpen', self.game, 'test',
'bss!bss@test_part_penalty', 'tester')
with mock.patch('idlerpg.models.Character.penalize', return_value=5) as mock_penalize:
with mock.patch('idlerpg.models.Character.log_out') as mock_log_out:
self.plugin.handle_part_penalty(self.mock_connection, mock_event)
mock_penalize.assert_called_with(200, "parting the game channel")
mock_log_out.assert_called()
test_char = Character.objects.get(name='testpartpen')
assert test_char.time_penalized_part == 5
test_char.delete()
def test_pubmsg_penalty(self):
"""Test that if a character speaks in the game channel, they get penalized."""
mock_event = mock.MagicMock()
mock_event.arguments = ['this is a test message']
mock_event.source = 'bss!bss@test_pubmsg_penalty'
mock_event.target = '#test'
mock_event.recursing = False
# make a character so as to not disturb other tests
test_char = Character.objects.register('testpubmsgpen', self.game, 'test',
'bss!bss@test_pubmsg_penalty', 'tester')
with mock.patch('idlerpg.models.Character.penalize', return_value=5) as mock_penalize:
self.plugin.handle_message_penalty(self.mock_connection, mock_event)
# 22 is the len of the message
mock_penalize.assert_called_with(22, "sending a privmsg to the game channel")
test_char = Character.objects.get(name='testpubmsgpen')
assert test_char.time_penalized_privmsg == 5
test_char.delete()
def test_pubmsg_no_penalty_for_unknown(self):
"""Test that a speakerwith an unknown hostmask is ignored."""
mock_event = mock.MagicMock()
mock_event.arguments = ['this is a test message']
mock_event.source = 'bss!bss@some_user'
mock_event.target = '#test'
mock_event.recursing = False
with mock.patch('idlerpg.models.Character.penalize', return_value=5) as mock_penalize:
self.plugin.handle_message_penalty(self.mock_connection, mock_event)
mock_penalize.assert_not_called()
def test_join(self):
"""Test that joiners update the seen hostmasks."""
mock_event = mock.MagicMock()
mock_event.arguments = ['this is a test message']
mock_event.source = 'test!test@test_join'
mock_event.target = '#test'
mock_event.recursing = False
assert 'test!test@test_join' not in self.plugin.seen_hostmasks
self.plugin.handle_join(self.mock_connection, mock_event)
assert 'test!test@test_join' in self.plugin.seen_hostmasks
def test_quit_penalty(self):
"""Test that if a character quits from the game channel, they get penalized."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_quit_penalty'
mock_event.target = '#test'
mock_event.recursing = False
# make a character so as to not disturb other tests
test_char = Character.objects.register('testquitpen', self.game, 'test',
'bss!bss@test_quit_penalty', 'tester')
with mock.patch('idlerpg.models.Character.penalize', return_value=5) as mock_penalize:
with mock.patch('idlerpg.models.Character.log_out') as mock_log_out:
self.plugin.handle_quit_penalty(self.mock_connection, mock_event)
mock_penalize.assert_called_with(20, "quitting IRC")
mock_log_out.assert_called()
test_char = Character.objects.get(name='testquitpen')
assert test_char.time_penalized_quit == 5
test_char.delete()
def test_quit_penalty_even_if_logged_out(self):
"""Test that the penalty applies even if the character is already logged out."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_quit_penalty'
mock_event.target = '#test'
mock_event.recursing = False
# make a character so as to not disturb other tests
test_char = Character.objects.register('testquitpen', self.game, 'test',
'bss!bss@test_quit_penalty', 'tester')
with mock.patch('idlerpg.models.Character.penalize', return_value=5) as mock_penalize:
with mock.patch('idlerpg.models.Character.log_out', side_effect=ValueError) as mock_log_out:
self.plugin.handle_quit_penalty(self.mock_connection, mock_event)
mock_penalize.assert_called_with(20, "quitting IRC")
mock_log_out.assert_called()
test_char = Character.objects.get(name='testquitpen')
assert test_char.time_penalized_quit == 5
test_char.delete()
def test_login_character_not_found(self):
"""Test the LOGIN command sent to the bot."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_login'
mock_event.target = '#test'
mock_event.recursing = False
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'LOGIN nope bss')
self.plugin.handle_login(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_once_with(
mock_event,
"The requested character does not exist, or has been disabled, or your password does not match."
)
def test_login_bad_password(self):
"""Test the LOGIN command sent to the bot."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_login'
mock_event.target = '#test'
mock_event.recursing = False
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'LOGIN bss nope')
self.plugin.handle_login(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_once_with(
mock_event,
"The requested character does not exist, or has been disabled, or your password does not match."
)
def test_login_not_seen(self):
"""Test the LOGIN command sent to the bot."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_login'
mock_event.target = '#test'
mock_event.recursing = False
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'LOGIN bss bss')
self.plugin.handle_login(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_once_with(mock_event, "Please join #test before logging in.")
def test_login_not_logged_off(self):
"""Test the LOGIN command sent to the bot."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_login'
mock_event.target = '#test'
mock_event.recursing = False
self.plugin.seen_hostmasks.add('bss!bss@test_login')
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'LOGIN bss bss')
self.plugin.handle_login(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_once_with(
mock_event,
"Cannot log in bss, either they already are, or they are in a state that cannot be modified."
)
self.plugin.seen_hostmasks.remove('bss!bss@test_login')
def test_login(self):
"""Check that the LOGIN command actually sets the character as logged in."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@bss_login'
mock_event.recursing = False
game = Game.objects.get(pk=1)
test_char = Character.objects.register('test_login', game, 'test', 'bss!bss@bss_login', 'tester')
test_char.log_out()
test_char.next_level = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
test_char.last_login = test_char.next_level
test_char.save()
self.plugin.seen_hostmasks.add('bss!bss@bss_login')
# manipulate login so it doesn't add time to next_level and so we have a consistent
# value to test against below
with mock.patch('django.utils.timezone.now', return_value=test_char.next_level):
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'LOGIN test_login test')
self.plugin.handle_login(self.mock_connection, mock_event, match)
# refetch
refetch = Character.objects.get(name='test_login')
# should have been penalized
assert refetch.status == Character.CHARACTER_STATUS_LOGGED_IN
# the ordering of this surprises me... keep an eye on it
self.mock_bot.reply.assert_has_calls([
mock.call(None, "test_login, the level 0 tester, is now online from nickname bss. "
"Next level at 2024-05-17 17:00:00 UTC.", explicit_target='#test'),
mock.call(mock_event, "test_login, the level 0 tester, has been successfully logged in."),
])
def test_logout_character_not_found(self):
"""Test the LOGOUT command sent to the bot."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@test_login'
mock_event.recursing = False
match = re.match(IdleRPG.LOGOUT_COMMAND_PATTERN, 'LOGOUT')
self.plugin.handle_logout(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_once_with(mock_event, "You do not have a character in a running game.")
def test_logout_character_not_logged_in(self):
"""Test the LOGOUT command sent to the bot."""
mock_event = mock.MagicMock()
mock_event.source = 'bss2!bss@bss'
mock_event.recursing = False
match = re.match(IdleRPG.LOGOUT_COMMAND_PATTERN, 'LOGOUT')
self.plugin.handle_logout(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_once_with(
mock_event,
"Cannot log out bss2, either they already are, or they are in a state that cannot be modified."
)
def test_logout(self):
"""Check that the LOGOUT command actually sets the character offline."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@bss_logout'
mock_event.recursing = False
game = Game.objects.get(pk=1)
test_char = Character.objects.register('test_logout', game, 'test', 'bss!bss@bss_logout', 'tester')
match = re.match(IdleRPG.LOGOUT_COMMAND_PATTERN, 'LOGOUT')
self.plugin.handle_logout(self.mock_connection, mock_event, match)
# refetch
refetch = Character.objects.get(name='test_logout')
# should have been penalized
assert refetch.next_level > test_char.next_level
assert refetch.status == Character.CHARACTER_STATUS_OFFLINE
self.mock_bot.reply.assert_called_once_with(
mock_event, "test_logout, the level 0 tester, has been successfully logged out."
)
def test_register(self):
"""Test the ability to register a new character."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@bss_register'
mock_event.recursing = False
self.plugin.seen_hostmasks.add('bss!bss@bss_register')
now = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
with mock.patch('django.utils.timezone.now', return_value=now):
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test test tester')
self.plugin.handle_register(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_has_calls([
mock.call(None, "bss's newest character, reg_test, the tester, "
"has been summoned! Next level at 2024-05-18 06:15:58 UTC.",
explicit_target='#test'),
mock.call(mock_event, "You have registered reg_test, the level 0 tester, "
"and been automatically logged in."),
])
def test_register_no_active(self):
"""Test the behavior of a failed registration because there's no active game."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@bss_register'
mock_event.recursing = False
# manipulate the game temporarily
game = Game.objects.get(active=True)
game.active = False
game.save()
self.plugin.seen_hostmasks.add('bss!bss@bss_register')
now = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
with mock.patch('django.utils.timezone.now', return_value=now):
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test test tester')
self.plugin.handle_register(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_has_calls([
mock.call(mock_event, "No active game exists."),
])
# undo manipulation
game.active = True
game.save()
def test_register_not_in_channel(self):
"""Test the behavior of a failed registration because the user isn't in the game channel."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@bss_register'
mock_event.recursing = False
now = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
with mock.patch('django.utils.timezone.now', return_value=now):
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test test tester')
self.plugin.handle_register(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_has_calls([
mock.call(mock_event, "Please join #test before registering.")
])
def test_register_cant_double_register(self):
"""Test the ability to register a new character fails if you try to have two."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@bss_dupe_register'
mock_event.recursing = False
self.plugin.seen_hostmasks.add('bss!bss@bss_dupe_register')
now = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00')
with mock.patch('django.utils.timezone.now', return_value=now):
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test test tester')
self.plugin.handle_register(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_has_calls([
mock.call(None, "bss's newest character, reg_test, the tester, "
"has been summoned! Next level at 2024-05-18 06:15:58 UTC.",
explicit_target='#test'),
mock.call(mock_event, "You have registered reg_test, the level 0 tester, "
"and been automatically logged in."),
])
with mock.patch('django.utils.timezone.now', return_value=now):
match = re.match(IdleRPG.REGISTER_COMMAND_PATTERN, 'REGISTER reg_test2 test tester')
self.plugin.handle_register(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_has_calls([
mock.call(mock_event, "Registration failed; either you already have a character, "
"or the character name is already taken.")
])
def test_remove(self):
"""Test the remove command."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@bss_remove'
mock_event.recursing = False
game = Game.objects.get(pk=1)
test_char = Character.objects.register('test_remove', game, 'test', 'bss!bss@bss_remove', 'tester')
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'REMOVEME')
self.plugin.handle_remove(self.mock_connection, mock_event, match)
assert test_char.enabled is True
refetch = Character.objects.get(name='test_remove')
assert refetch.enabled is False
self.mock_bot.reply.assert_has_calls([
mock.call(mock_event, "Character test_remove has been disabled."),
])
def test_remove_no_match(self):
"""Test the remove command doesn't do anything if the hostname isn't found."""
mock_event = mock.MagicMock()
mock_event.source = 'bss2!bss@bss_remove'
mock_event.recursing = False
game = Game.objects.get(pk=1)
test_char = Character.objects.register('test_remove', game, 'test', 'bss!bss@bss_remove', 'tester')
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'REMOVEME')
self.plugin.handle_remove(self.mock_connection, mock_event, match)
assert test_char.enabled is True
refetch = Character.objects.get(name='test_remove')
assert refetch.enabled is True
self.mock_bot.reply.assert_has_calls([
mock.call(mock_event, "No character associated to your hostmask found (try logging in first)."),
])
def test_status(self):
"""Test the status command."""
mock_event = mock.MagicMock()
mock_event.source = 'bss!bss@bss'
mock_event.recursing = False
self.plugin.seen_hostmasks.add('bss!bss@bss')
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'STATUS')
self.plugin.handle_status(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_has_calls([
mock.call(mock_event, "bss, the level 0 tester, is bss!bss@bss."),
mock.call(mock_event, "bss is logged in."),
mock.call(mock_event, "bss will level up on 2024-05-05 05:20:45 UTC."),
])
assert self.mock_bot.reply.call_count == 3
self.plugin.seen_hostmasks.remove('bss!bss@bss')
def test_status_logged_out(self):
"""Test the status command."""
mock_event = mock.MagicMock()
mock_event.source = 'bss2!bss@bss'
mock_event.recursing = False
self.plugin.seen_hostmasks.add('bss2!bss@bss')
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'STATUS')
self.plugin.handle_status(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_has_calls([
mock.call(mock_event, "bss2, the level 0 tester, is bss2!bss@bss."),
mock.call(mock_event, "bss2 is offline."),
])
assert self.mock_bot.reply.call_count == 2
self.plugin.seen_hostmasks.remove('bss2!bss@bss')
def test_status_no_match(self):
"""Test the status command."""
mock_event = mock.MagicMock()
mock_event.source = 'bss3!bss@bss'
mock_event.recursing = False
self.plugin.seen_hostmasks.add('bss3!bss@bss')
match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'STATUS')
self.plugin.handle_status(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_has_calls([
mock.call(mock_event, "No character associated to your hostmask found (try logging in first)."),
])
assert self.mock_bot.reply.call_count == 1
self.plugin.seen_hostmasks.remove('bss3!bss@bss')
def test_level_up_characters(self):
"""Test the walking of games and characters to level them up."""
game = Game.objects.get(pk=1)
char_1 = Character.objects.register('test_level_1', game, 'test', 'test_1!test@test', 'tester')
char_2 = Character.objects.register('test_level_2', game, 'test', 'test_2!test@test', 'tester')
char_2.log_out()
level_time = datetime.datetime(year=2024, month=5, day=16, hour=00, minute=0, second=0)
char_1.next_level = level_time
char_2.next_level = level_time
char_1.save()
char_2.save()
# only one should level up, since char_2 isn't logged in
self.plugin._level_up_characters()
char_1 = Character.objects.get(pk=char_1.pk)
char_2 = Character.objects.get(pk=char_2.pk)
assert char_1.level == 1
assert char_2.level == 0
assert char_1.next_level != char_2.next_level
self.mock_bot.reply.assert_has_calls([
mock.call(None, "test_level_1, the tester, has attained level 1! Next level at 2024-05-16 13:15:58 UTC.",
explicit_target=game.channel.name),
])