drbotzo-idlerpg/tests/test_idlerpg_ircplugin.py
Brian S. Stephan d9a66d0c74
message the character's player when penalties are applied
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-07-15 18:44:20 -05:00

584 lines
26 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 re
import unittest.mock as mock
from django.test import TestCase
from ircbot.models import 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')
self.plugin.seen_hostmasks.add('bss!bss@test_nick_penalty')
with mock.patch('idlerpg.models.Character.penalize', return_value=30) 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, "a nick change")
mock_log_out.assert_called()
test_char = Character.objects.get(name='testnickpen')
assert test_char.time_penalized_nick_change == 30
assert 'bss!bss@test_nick_penalty' not in self.plugin.seen_hostmasks
self.mock_bot.reply.assert_called_once_with(
None,
"testnickpen, the level 0 tester has been penalized 30 seconds for a nick change, "
"and has been logged out.",
explicit_target='bss',
)
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')
self.plugin.seen_hostmasks.add('bss!bss@test_part_penalty')
with mock.patch('idlerpg.models.Character.penalize', return_value=200) 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 == 200
assert 'bss!bss@test_part_penalty' not in self.plugin.seen_hostmasks
self.mock_bot.reply.assert_called_once_with(
None,
"testpartpen, the level 0 tester has been penalized 200 seconds for parting the game channel, "
"and has been logged out.",
explicit_target='bss',
)
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=22) 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 == 22
self.mock_bot.reply.assert_called_once_with(
None,
"testpubmsgpen, the level 0 tester has been penalized 22 seconds for talking in the game channel.",
explicit_target='bss',
)
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_whoreply(self):
"""When joining, we do a /WHO to see who is online."""
mock_event = mock.MagicMock()
mock_event.arguments = ['', 'bss', 'bss', '', 'bss2']
assert 'bss2!bss@bss' not in self.plugin.seen_hostmasks
char = Character.objects.get(name='bss2', game=self.game)
assert char.status == Character.CHARACTER_STATUS_OFFLINE
self.plugin.handle_whoreply_response(self.mock_connection, mock_event)
assert 'bss2!bss@bss' in self.plugin.seen_hostmasks
char = Character.objects.get(name='bss2', game=self.game)
assert char.status == Character.CHARACTER_STATUS_LOGGED_IN
# clean up
char.log_out()
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. "
"They have been penalized 20 seconds.")
)
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),
])