259 lines
11 KiB
Python
259 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=47758)
|
|
# level 1 -> 2
|
|
char.level = 1
|
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=131601)
|
|
# level 24 -> 25
|
|
char.level = 24
|
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=437113)
|
|
# level 59 -> 60
|
|
char.level = 59
|
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=543010)
|
|
# level 60 -> 61
|
|
char.level = 60
|
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=545009)
|
|
# level 61 -> 62
|
|
char.level = 61
|
|
assert char.calculate_datetime_to_next_level() == char.next_level + timedelta(seconds=546976)
|
|
|
|
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=47758)
|
|
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=47758)
|
|
|
|
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()
|