a prior commit made this determination for now, for simplicity's sake (and also what are the odds of running two games at once on the same codebase), but it was'n really enforced until now Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
448 lines
19 KiB
Python
448 lines
19 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_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_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),
|
|
])
|