"""Test IdleRPG character operations. SPDX-FileCopyrightText: © 2024 Brian S. Stephan 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()