drbotzo-idlerpg/tests/test_idlerpg_character.py

261 lines
11 KiB
Python

"""Test IdleRPG character operations.
SPDX-FileCopyrightText: © 2024 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
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
from idlerpg.models import Character, Game
logger = logging.getLogger(__name__)
class CharacterTest(TestCase):
"""Test the Character model."""
fixtures = ['tests/fixtures/simple_character.json']
def test_string_repr(self):
"""Test the basic string summary."""
char = Character.objects.get(pk=1)
logger.debug(str(char))
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."""
char = Character.objects.get(pk=1)
logout_time = timezone.now()
with patch('django.utils.timezone.now', return_value=logout_time):
char.log_out()
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)
char.log_out()
with self.assertRaises(ValueError):
char.log_out()
def test_log_in(self):
"""Test the result of logging in."""
char = Character.objects.get(pk=1)
logout_time = timezone.now()
login_time = logout_time + timedelta(seconds=300)
# manipulate the time so that we log out and then log in 300 seconds later, which
# adds 20 seconds to the next level time
with patch('django.utils.timezone.now', return_value=logout_time):
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
with patch('django.utils.timezone.now', return_value=login_time):
char.log_in('bss', 'bss!bss@test_log_in')
assert char.next_level == old_next_level + timedelta(seconds=300)
assert char.status == Character.CHARACTER_STATUS_LOGGED_IN
assert char.hostmask == 'bss!bss@test_log_in'
def test_cant_log_in_when_already_online(self):
"""Test that we can't log in the character if they're already online."""
char = Character.objects.get(pk=1)
with self.assertRaises(ValueError):
char.log_in('bss', 'bss!bss@test_online')
def test_cant_log_in_bad_password(self):
"""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()
with self.assertRaises(ValueError):
char.log_in('bad pass', 'bss!bss@test_bad_password')
def test_set_password(self):
"""Test that the password is actually changed when requested."""
char = Character.objects.get(pk=1)
old_password = char.password
char.set_password('something')
assert old_password != char.password
assert char.password[0:13] == 'pbkdf2_sha256'
def test_penalize(self):
"""Test that penalties apply properly, including level adjustments."""
char = Character.objects.get(pk=1)
next_level = char.next_level
# level 0
next_level += timedelta(seconds=20)
char.penalize(20, 'test')
assert char.next_level == next_level
# level 1
char.level = 1
next_level += timedelta(seconds=23)
char.penalize(20, 'test')
assert char.next_level == next_level
# level 5
char.level = 5
next_level += timedelta(seconds=39)
char.penalize(20, 'test')
assert char.next_level == next_level
# level 25
char.level = 25
next_level += timedelta(seconds=530)
char.penalize(20, 'test')
assert char.next_level == next_level
def test_calculate_datetime_to_next_level(self):
"""Test that the next level calculation behaves as expected in a number of situations."""
char = Character.objects.get(pk=1)
# level 0 -> 1
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=600)
# level 1 -> 2
char.level = 1
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=696)
# level 24 -> 25
char.level = 24
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=21142)
# level 59 -> 60
char.level = 59
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=3812174)
# level 60 -> 61
char.level = 60
assert char.calculate_datetime_to_next_level() == char.next_level + (timedelta(seconds=3812174) +
timedelta(seconds=86400))
# level 61 -> 62
char.level = 61
assert char.calculate_datetime_to_next_level() == char.next_level + (timedelta(seconds=3812174) +
timedelta(seconds=86400*2))
def test_calculate_datetime_to_next_level_not_time_yet(self):
"""Test that we just bail with the current next_level if it hasn't been reached yet."""
char = Character.objects.get(pk=1)
with patch('django.utils.timezone.now', return_value=char.next_level - timedelta(days=1)):
assert char.calculate_datetime_to_next_level() == char.next_level
def test_register(self):
"""Test the primary way to create a character with proper initial values."""
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('new', game, 'pass', 'bss!bss@test_register', 'unit tester')
assert new_char.status == Character.CHARACTER_STATUS_LOGGED_IN
assert new_char.next_level == register_time + timedelta(seconds=600)
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)
old_level = char.level
old_next_level = char.next_level
char.level_up()
assert char.level == old_level + 1
assert char.next_level == old_next_level + timedelta(seconds=600)
def test_level_up_fail_not_time(self):
"""Test the failure condition for trying to level up before the timestamp."""
char = Character.objects.get(pk=1)
with patch('django.utils.timezone.now', return_value=char.next_level-timedelta(minutes=30)):
with self.assertRaises(ValueError):
char.level_up()
def test_level_up_fail_offline(self):
"""Test the failure condition for trying to level up a character not online."""
char = Character.objects.get(pk=1)
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()