Compare commits

...

10 Commits

Author SHA1 Message Date
15df477fa4
add a manager query for level-up-able characters
will be used by the IRC plugin

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-15 12:00:14 -05:00
2730978663
start implementation of the IRC plugin
not functional yet, but this has some of the operations implemented and
under test

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-15 11:59:21 -05:00
497619e0ff
do a simple Game test of the string repr
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-14 21:17:51 -05:00
27fcecd8aa
add a Character manager operation to log everyone out
this is without penalty

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-14 14:10:33 -05:00
04e09af8a9
provide next_level as a nice-looking string
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-14 13:03:32 -05:00
fa56e51ce3
move the character password check into a separate method
just a bit of reuse

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-14 13:00:20 -05:00
2ad79285b3
unique constraint for only one hostmask enabled at a time
this replaces the need for a game+hostmask unique constraint, with this
change, a player can only have one active character they're playing
with anyway, regardless of how many games there are

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-14 11:13:13 -05:00
d0531bff53
have character status display strings lowercase for use mid-sentence
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-14 10:55:51 -05:00
d322429157
tweak the formatting of the character summary string
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-14 10:36:10 -05:00
f941762f26
get rid of the online character status, it's not necessary
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-12 23:36:25 -05:00
8 changed files with 619 additions and 14 deletions

139
idlerpg/ircplugin.py Normal file
View File

@ -0,0 +1,139 @@
"""IRC support for managing and executing the IdleRPG game.
SPDX-FileCopyrightText: © 2024 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
import logging
import threading
import time
import irc.client
from django.db.utils import IntegrityError
from ircbot.lib import Plugin
from idlerpg.models import Character, Game
logger = logging.getLogger(__name__)
class IdleRPG(Plugin):
"""Run a game of IdleRPG."""
SLEEP_BETWEEN_LEVEL_CHECKS = 1
LOGIN_COMMAND_PATTERN = r'^LOGIN\s+(?P<name>\S+)\s+(?P<password>\S+)$'
LOGOUT_COMMAND_PATTERN = r'^LOGOUT$'
REGISTER_COMMAND_PATTERN = r'^REGISTER\s+(?P<name>\S+)\s+(?P<password>\S+)\s+(?P<char_class>.*)$'
REMOVEME_COMMAND_PATTERN = r'^REMOVEME$'
STATUS_COMMAND_PATTERN = r'^(STATUS|WHOAMI)$'
def __init__(self, bot, connection, event):
"""Initialize miscellaneous state stuff."""
self.seen_hostmasks = set()
super().__init__(bot, connection, event)
def handle_join(self, connection, event):
"""Track a character as online if their player joins the game channel."""
self.seen_hostmasks.add(event.source)
logger.info("Added %s to the set of seen hostmasks.", event.source)
def handle_kick_penalty(self, connection, event):
"""Penalize characters for their user getting kicked from the channel."""
self.seen_hostmasks.discard(event.source)
logger.info("Removed %s from the set of seen hostmasks.", event.source)
return self._handle_generic_penalty_and_logout(event.source, event.target, 250,
"getting kicked from the game channel",
'time_penalized_kicked')
def handle_nick_penalty(self, connection, event):
"""Penalize characters for changing their nick while in the channel."""
# TODO: figure out how to update the character and seen hostmasks
return self._handle_generic_penalty_and_logout(event.source, event.target, 30,
"changing their nick",
'time_penalized_nick_change')
def handle_part_penalty(self, connection, event):
"""Penalize characters for their user parting."""
self.seen_hostmasks.discard(event.source)
logger.info("Removed %s from the set of seen hostmasks.", event.source)
return self._handle_generic_penalty_and_logout(event.source, event.target, 200,
"parting the game channel",
'time_penalized_part')
def handle_message_penalty(self, connection, event):
"""Pay attention to messages in the game channel, and penalize talkers."""
hostmask = event.source
logger.debug("looking for %s to try to penalize them", hostmask)
try:
character = Character.objects.get(enabled=True, hostmask=hostmask)
logger.debug("found character %s", character)
message = event.arguments[0]
penalty = character.penalize(len(message), "sending a privmsg to the game channel")
character.time_penalized_privmsg += penalty
character.save()
except Character.DoesNotExist:
logger.debug("no character found for %s", hostmask)
return
def handle_quit_penalty(self, connection, event):
"""Penalize characters for their user quitting IRC."""
self.seen_hostmasks.discard(event.source)
logger.info("Removed %s from the set of seen hostmasks.", event.source)
return self._handle_generic_penalty_and_logout(event.source, event.target, 20,
"quitting IRC",
'time_penalized_quit')
def handle_login(self, connection, event, match):
"""Log in a character when requested, assuming they're online."""
hostmask = event.source
nick = irc.client.NickMask(hostmask).nick
try:
character = Character.objects.get(enabled=True, name=match.group('name'))
character.check_password(match.group('password'))
except (Character.DoesNotExist, ValueError):
return self.bot.reply(event, "The requested character does not exist, or has been disabled, "
"or your password does not match.")
if hostmask not in self.seen_hostmasks:
return self.bot.reply(event, f"Please join {character.game.channel.name} before logging in.")
if character.status != Character.CHARACTER_STATUS_OFFLINE:
return self.bot.reply(event, f"Cannot log in {character.name}, either they already are, "
"or they are in a state that cannot be modified.")
character.log_in(match.group('password'), hostmask)
character.save()
self.bot.reply(None, f"{character}, is now online from nickname {nick}. "
f"Next level at {character.next_level_str()}.",
explicit_target=character.game.channel.name)
return self.bot.reply(event, f"{character} has been successfully logged in.")
def _handle_generic_penalty_and_logout(self, hostmask: str, channel: str, penalty: int, reason: str,
penalty_log_attr: str):
"""Penalize a character and log them out, for a provided reason.
Args:
hostmask: the hostmask for the character in question
channel: the game channel the event occurred in
penalty: the penalty to apply
reason: the reason for the penalty
"""
logger.debug("looking for %s to try to penalize them", hostmask)
try:
character = Character.objects.get(enabled=True, hostmask=hostmask)
logger.debug("character found for %s", hostmask)
seconds = character.penalize(penalty, reason)
log = getattr(character, penalty_log_attr)
setattr(character, penalty_log_attr, log + seconds)
try:
character.log_out()
except ValueError:
logger.debug("tried to log out %s but they already were", character)
character.save()
except Character.DoesNotExist:
logger.debug("no character found for %s", hostmask)
plugin = IdleRPG

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.5 on 2024-05-13 04:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('idlerpg', '0004_remove_character_no_characters_with_same_name_per_game_and_more'),
]
operations = [
migrations.AlterField(
model_name='character',
name='status',
field=models.CharField(choices=[('DISABLED', 'Disabled'), ('LOGGEDIN', 'Logged in'), ('OFFLINE', 'Offline')], default='OFFLINE', max_length=8),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.5 on 2024-05-14 15:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('idlerpg', '0005_alter_character_status'),
]
operations = [
migrations.AlterField(
model_name='character',
name='status',
field=models.CharField(choices=[('DISABLED', 'disabled'), ('LOGGEDIN', 'logged in'), ('OFFLINE', 'offline')], default='OFFLINE', max_length=8),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.0.5 on 2024-05-14 16:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('idlerpg', '0006_alter_character_status'),
]
operations = [
migrations.RemoveConstraint(
model_name='character',
name='one_player_character_per_game',
),
migrations.AddField(
model_name='character',
name='enabled',
field=models.BooleanField(default=True),
),
migrations.AddConstraint(
model_name='character',
constraint=models.UniqueConstraint(condition=models.Q(('enabled', True)), fields=('hostmask',), name='one_enabled_character_at_a_time'),
),
]

View File

@ -29,6 +29,10 @@ class Game(models.Model):
models.UniqueConstraint("active", "channel", name="one_game_per_channel"),
]
def __str__(self):
"""Provide a string representation of the game."""
return f"{self.name} in {self.channel} ({'active' if self.active else 'inactive'})"
@staticmethod
def level_formula(current_level: int = 0, base_time: datetime = None) -> timedelta:
"""DRY the formula for determining when a character's next level would be.
@ -52,6 +56,16 @@ class Game(models.Model):
class CharacterManager(models.Manager):
"""Query manager for creating a Character and taking operations relevant to the game."""
def log_out_everyone(self):
"""Log out every logged in character.
This is probably being called on shutdown or another situation when we can't monitor state, so we'll
just recreate it whenever it is we come back online.
"""
self.filter(enabled=True,
status=Character.CHARACTER_STATUS_LOGGED_IN).update(status=Character.CHARACTER_STATUS_OFFLINE)
logger.info("ALL::ALL: logged out all currently online or logged in characters")
def register(self, name: str, game: Game, password: str, hostmask: str, character_class: str):
"""Create a character, with guardrails.
@ -72,6 +86,19 @@ class CharacterManager(models.Manager):
character.game.name, character.name, character.next_level)
return character
def levelable(self, game) -> models.QuerySet:
"""Provide the characters that are ready to level.
Args:
game: the game instance to filter characters on
Returns:
QuerySet of the characters that can be leveled up
"""
if not game.active:
raise ValueError(f"{game.name} is not an active game!")
return self.filter(enabled=True, status=Character.CHARACTER_STATUS_LOGGED_IN,
next_level__lte=timezone.now(), game=game)
class Character(models.Model):
"""A character in a game."""
@ -79,12 +106,10 @@ class Character(models.Model):
CHARACTER_STATUS_DISABLED = 'DISABLED'
CHARACTER_STATUS_LOGGED_IN = 'LOGGEDIN'
CHARACTER_STATUS_OFFLINE = 'OFFLINE'
CHARACTER_STATUS_ONLINE = 'ONLINE'
CHARACTER_STATUSES = {
CHARACTER_STATUS_DISABLED: "Disabled",
CHARACTER_STATUS_LOGGED_IN: "Logged in",
CHARACTER_STATUS_OFFLINE: "Offline",
CHARACTER_STATUS_ONLINE: "Online",
CHARACTER_STATUS_DISABLED: "disabled",
CHARACTER_STATUS_LOGGED_IN: "logged in",
CHARACTER_STATUS_OFFLINE: "offline",
}
NICK_CHANGE_P = 30
@ -97,6 +122,7 @@ class Character(models.Model):
password = models.CharField(max_length=256)
hostmask = models.CharField(max_length=256)
status = models.CharField(max_length=8, choices=CHARACTER_STATUSES, default='OFFLINE')
enabled = models.BooleanField(default=True)
character_class = models.CharField(max_length=30)
level = models.PositiveIntegerField(default=0)
@ -121,12 +147,17 @@ class Character(models.Model):
"""Options for the Character and its objects."""
constraints = [
models.UniqueConstraint("game", "hostmask", name="one_player_character_per_game"),
models.UniqueConstraint(fields=['hostmask'], condition=models.Q(enabled=True),
name='one_enabled_character_at_a_time'),
]
def __str__(self):
"""Provide a string representation for display, etc. purposes."""
return f"{self.name}, level {self.level} {self.character_class}"
return f"{self.name}, the level {self.level} {self.character_class}"
def next_level_str(self):
"""Return a nicely-formatted version of next_level."""
return self.next_level.strftime('%Y-%m-%d %H:%M:%S %Z')
def calculate_datetime_to_next_level(self):
"""Determine when the next level will be reached (barring no penalties or logoffs).
@ -144,6 +175,19 @@ class Character(models.Model):
# character is past their old time and in the process of leveling up, calculate next based on that
return Game.level_formula(self.level, self.next_level)
def check_password(self, password: str):
"""Check that the provided password is correct for this character.
Used to authenicate without logging in (yet).
Args:
password: the password to check
Raises:
ValueError: if the password is incorrect
"""
if not check_password(password, self.password):
raise ValueError(f"incorrect password for character '{self.name}'!")
def level_up(self):
"""Set this character's level and next time if they meet the necessary criteria.
@ -169,10 +213,9 @@ class Character(models.Model):
Raises:
ValueError: if the provided password was incorrect, or the character isn't logged out
"""
if self.status != self.CHARACTER_STATUS_ONLINE:
raise ValueError(f"character '{self.name}' can't be logged in, isn't online!")
if not check_password(password, self.password):
raise ValueError(f"incorrect password for character '{self.name}'!")
if self.status != self.CHARACTER_STATUS_OFFLINE:
raise ValueError(f"character '{self.name}' can't be logged in, isn't offline!")
self.check_password(password)
logger.debug("logging %s in...", str(self))
# we need to apply the time lost to the next level time

View File

@ -7,6 +7,8 @@ import logging
from datetime import timedelta
from unittest.mock import patch
from django.db import transaction
from django.db.utils import IntegrityError
from django.test import TestCase
from django.utils import timezone
@ -24,7 +26,12 @@ class CharacterTest(TestCase):
"""Test the basic string summary."""
char = Character.objects.get(pk=1)
logger.debug(str(char))
assert str(char) == "bss, level 0 tester"
assert str(char) == "bss, the level 0 tester"
def test_next_level_str(self):
"""Test the format of the readable next level string."""
char = Character.objects.get(pk=1)
assert char.next_level_str() == "2024-05-05 05:20:45 UTC"
def test_log_out(self):
"""Test basic log out functionality and end result."""
@ -37,6 +44,20 @@ class CharacterTest(TestCase):
assert char.status == Character.CHARACTER_STATUS_OFFLINE
assert char.last_login == logout_time
def test_log_out_everyone(self):
"""Test logging out everyone in the database."""
game = Game.objects.get(pk=1)
# make some data
Character.objects.register('loe1', game, 'test', 'loe1', 'log_out_everyone_tester')
Character.objects.register('loe2', game, 'test', 'loe2', 'log_out_everyone_tester')
Character.objects.register('loe3', game, 'test', 'loe3', 'log_out_everyone_tester')
assert Character.objects.filter(status=Character.CHARACTER_STATUS_LOGGED_IN).count() >= 3
Character.objects.log_out_everyone()
assert Character.objects.filter(status=Character.CHARACTER_STATUS_LOGGED_IN).count() == 0
def test_cant_log_out_offline(self):
"""Test that we error if trying to log out a character already offline."""
char = Character.objects.get(pk=1)
@ -56,7 +77,6 @@ class CharacterTest(TestCase):
char.log_out()
# logout has a penalty of its own, so this post-logout value is what will be altered
old_next_level = char.next_level
char.status = Character.CHARACTER_STATUS_ONLINE
with patch('django.utils.timezone.now', return_value=login_time):
char.log_in('bss', 'bss!bss@test_log_in')
@ -74,7 +94,6 @@ class CharacterTest(TestCase):
"""Test that we can't log in the character if we don't have the right password."""
char = Character.objects.get(pk=1)
char.log_out()
char.status = Character.CHARACTER_STATUS_ONLINE
with self.assertRaises(ValueError):
char.log_in('bad pass', 'bss!bss@test_bad_password')
@ -154,6 +173,27 @@ class CharacterTest(TestCase):
assert new_char.last_login == register_time
assert new_char.password[0:13] == 'pbkdf2_sha256'
def test_cant_register_twice(self):
"""Test that we get a unique constraint error if we try to make a second enabled character."""
game = Game.objects.get(pk=1)
register_time = timezone.now()
with patch('django.utils.timezone.now', return_value=register_time):
new_char = Character.objects.register('new1', game, 'pass', 'bss!bss@test_double_register', 'unit tester')
with self.assertRaises(IntegrityError):
with transaction.atomic():
Character.objects.register('new2', game, 'pass', 'bss!bss@test_double_register', 'unit tester')
# try again with the first character un-enabled
new_char.enabled = False
new_char.save()
register_time = timezone.now()
with patch('django.utils.timezone.now', return_value=register_time):
newer_char = Character.objects.register('new2', game, 'pass', 'bss!bss@test_double_register', 'unit tester')
assert new_char.hostmask == newer_char.hostmask
def test_level_up(self):
"""Test the level up functionality."""
char = Character.objects.get(pk=1)
@ -178,3 +218,43 @@ class CharacterTest(TestCase):
char.log_out()
with self.assertRaises(ValueError):
char.level_up()
def test_levelable_query(self):
"""Test that the right things are returned by the levelable query used to find characters to update."""
game = Game.objects.get(pk=1)
# base data is one character ready to level
assert len(Character.objects.levelable(game)) == 1
# add one to fiddle with
with patch('django.utils.timezone.now', return_value=timezone.now() - timedelta(days=1)):
new_char = Character.objects.register('levelable-test', game, 'pass',
'bss!bss@levelable_test', 'unit tester')
assert len(Character.objects.levelable(game)) == 2
# log the new one out
new_char.log_out()
new_char.save()
assert len(Character.objects.levelable(game)) == 1
# log the new one back in but penalize it heavily
new_char.log_in('pass', 'bss!bss@levelable_test')
new_char.save()
assert len(Character.objects.levelable(game)) == 2
new_char.penalize(60*60*24*2, 'test')
new_char.save()
assert len(Character.objects.levelable(game)) == 1
# actually level them
new_char.next_level = timezone.now() - timedelta(seconds=30)
new_char.save()
assert len(Character.objects.levelable(game)) == 2
new_char.level_up()
new_char.save()
assert len(Character.objects.levelable(game)) == 1
def test_levelable_query_bad_game(self):
"""Test that trying to find levelable characters for a non-active game errors."""
game = Game.objects.get(pk=1)
game.active = False
game.save()
with self.assertRaises(ValueError):
Character.objects.levelable(game)
# clean up
game.active = True
game.save()

View File

@ -0,0 +1,24 @@
"""Test IdleRPG game operations.
SPDX-FileCopyrightText: © 2024 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
import logging
from django.test import TestCase
from idlerpg.models import Game
logger = logging.getLogger(__name__)
class GameTest(TestCase):
"""Test the Character model."""
fixtures = ['tests/fixtures/simple_character.json']
def test_string_repr(self):
"""Test the basic string summary."""
game = Game.objects.get(pk=1)
logger.debug(str(game))
assert str(game) == "test in #test on default (active)"

View File

@ -0,0 +1,257 @@
"""Test IdleRPG character operations.
SPDX-FileCopyrightText: © 2024 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
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')