diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 8986d18473..654da9924f 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -649,6 +649,51 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): logger.log_sec(f"Password successfully changed for {self}.") self.at_password_change() + def create_character(self, *args, **kwargs): + """ + Create a character linked to this account. + + Args: + key (str, optional): If not given, use the same name as the account. + typeclass (str, optional): Typeclass to use for this character. If + not given, use settings.BASE_CHARACTER_TYPECLASS. + permissions (list, optional): If not given, use the account's permissions. + ip (str, optiona): The client IP creating this character. Will fall back to the + one stored for the account if not given. + kwargs (any): Other kwargs will be used in the create_call. + Returns: + Object: A new character of the `character_typeclass` type. None on an error. + list or None: A list of errors, or None. + + """ + # parse inputs + character_key = kwargs.pop("key", self.key) + character_ip = kwargs.pop("ip", self.db.creator_ip) + character_permissions = kwargs.pop("permissions", self.permissions) + + # Load the appropriate Character class + character_typeclass = kwargs.pop("typeclass", None) + character_typeclass = character_typeclass if character_typeclass else settings.BASE_CHARACTER_TYPECLASS + Character = class_from_module(character_typeclass) + + # Create the character + character, errs = Character.create( + character_key, + self, + ip=character_ip, + typeclass=character_typeclass, + permissions=character_permissions, + **kwargs + ) + if character: + # Update playable character list + if character not in self.characters: + self.db._playable_characters.append(character) + + # We need to set this to have @ic auto-connect to this character + self.db._last_puppet = character + return character, errs + @classmethod def create(cls, *args, **kwargs): """ @@ -755,31 +800,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): logger.log_err(string) if account and settings.MULTISESSION_MODE < 2: - # Load the appropriate Character class - character_typeclass = kwargs.get( - "character_typeclass", settings.BASE_CHARACTER_TYPECLASS - ) - character_home = kwargs.get("home") - Character = class_from_module(character_typeclass) + # Auto-create a character to go with this account - # Create the character - character, errs = Character.create( - account.key, - account, - ip=ip, - typeclass=character_typeclass, - permissions=permissions, - home=character_home, - ) - errors.extend(errs) - - if character: - # Update playable character list - if character not in account.characters: - account.db._playable_characters.append(character) - - # We need to set this to have @ic auto-connect to this character - account.db._last_puppet = character + character, errs = account.create_character(typeclass=kwargs.get("character_typeclass")) + if errs: + errors.extend(errs) except Exception: # We are in the middle between logged in and -not, so we have @@ -1540,7 +1565,7 @@ class DefaultGuest(DefaultAccount): try: # Find an available guest name. for name in settings.GUEST_LIST: - if not AccountDB.objects.filter(username__iexact=name).count(): + if not AccountDB.objects.filter(username__iexact=name).exists(): username = name break if not username: @@ -1566,6 +1591,15 @@ class DefaultGuest(DefaultAccount): ip=ip, ) errors.extend(errs) + + if not account.characters: + # this can happen for multisession_mode > 1. For guests we + # always auto-create a character, regardless of multi-session-mode. + character, errs = account.create_character() + + if errs: + errors.extend(errs) + return account, errors except Exception as e: diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index ee0d93d2c8..c2e5bed658 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -17,7 +17,7 @@ import datetime from anything import Anything from django.conf import settings -from mock import Mock, mock +from unittest.mock import patch, Mock, MagicMock from evennia import DefaultRoom, DefaultExit, ObjectDB from evennia.commands.default.cmdset_character import CharacterCmdSet @@ -56,6 +56,7 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE) # ------------------------------------------------------------ +@patch("evennia.server.portal.portal.LoopingCall", new=MagicMock()) class CommandTest(EvenniaTest): """ Tests a command @@ -505,7 +506,7 @@ class TestBuilding(CommandTest): self.call(building.CmdSetAttribute(), "Obj2/test2", "Attribute Obj2/test2 = value2") self.call(building.CmdSetAttribute(), "Obj2/NotFound", "Obj2 has no attribute 'notfound'.") - with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed: + with patch("evennia.commands.default.building.EvEditor") as mock_ed: self.call(building.CmdSetAttribute(), "/edit Obj2/test3") mock_ed.assert_called_with(self.char1, Anything, Anything, key="Obj2/test3") @@ -789,7 +790,7 @@ class TestBuilding(CommandTest): ) self.call(building.CmdDesc(), "", "Usage: ") - with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed: + with patch("evennia.commands.default.building.EvEditor") as mock_ed: self.call(building.CmdDesc(), "/edit") mock_ed.assert_called_with( self.char1, @@ -1004,9 +1005,9 @@ class TestBuilding(CommandTest): } ) ] - with mock.patch( + with patch( "evennia.commands.default.building.protlib.search_prototype", - new=mock.MagicMock(return_value=test_prototype), + new=MagicMock(return_value=test_prototype), ) as mprot: self.call( building.CmdTypeclass(), @@ -1072,7 +1073,7 @@ class TestBuilding(CommandTest): self.call(building.CmdFind(), "/exact Obj", "One Match") # Test multitype filtering - with mock.patch( + with patch( "evennia.commands.default.building.CHAR_TYPECLASS", "evennia.objects.objects.DefaultCharacter", ): @@ -1540,11 +1541,11 @@ class TestSystemCommands(CommandTest): self.call(multimatch, "look", "") - @mock.patch("evennia.commands.default.syscommands.ChannelDB") + @patch("evennia.commands.default.syscommands.ChannelDB") def test_channelcommand(self, mock_channeldb): - channel = mock.MagicMock() - channel.msg = mock.MagicMock() - mock_channeldb.objects.get_channel = mock.MagicMock(return_value=channel) + channel = MagicMock() + channel.msg = MagicMock() + mock_channeldb.objects.get_channel = MagicMock(return_value=channel) self.call(syscommands.SystemSendToChannel(), "public:Hello") channel.msg.assert_called() diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 1bf3fa2acf..4bdd8d41ad 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -163,6 +163,7 @@ class Portal(object): self.server_info_dict = {} self.start_time = time.time() + self.maintenance_task = LoopingCall(_portal_maintenance) self.maintenance_task.start(60, now=True) # call every minute diff --git a/evennia/server/tests/test_amp_connection.py b/evennia/server/tests/test_amp_connection.py index 8f41150866..2c09eb48aa 100644 --- a/evennia/server/tests/test_amp_connection.py +++ b/evennia/server/tests/test_amp_connection.py @@ -6,7 +6,7 @@ Test AMP client import pickle from model_mommy import mommy from unittest import TestCase -from mock import MagicMock, patch +from unittest.mock import MagicMock, patch from twisted.trial.unittest import TestCase as TwistedTestCase from evennia.server import amp_client from evennia.server.portal import amp_server @@ -36,6 +36,7 @@ class _TestAMP(TwistedTestCase): self.server.sessions[1] = self.session self.portal = portal.Portal(MagicMock()) + self.portal.maintenance_task.stop() self.portalsession = session.Session() self.portalsession.sessid = 1 self.portal.sessions[1] = self.portalsession diff --git a/evennia/server/tests/testrunner.py b/evennia/server/tests/testrunner.py index 63bbad49e0..c2eb334a7d 100644 --- a/evennia/server/tests/testrunner.py +++ b/evennia/server/tests/testrunner.py @@ -6,6 +6,7 @@ Runs as part of the Evennia's test suite with 'evennia test evennia" """ from django.test.runner import DiscoverRunner +from unittest import mock class EvenniaTestSuiteRunner(DiscoverRunner): @@ -21,9 +22,16 @@ class EvenniaTestSuiteRunner(DiscoverRunner): Build a test suite for Evennia. test_labels is a list of apps to test. If not given, a subset of settings.INSTALLED_APPS will be used. """ + # the portal looping call starts before the unit-test suite so we + # can't mock it - instead we stop it before starting the test - otherwise + # we'd get unclean reactor errors across test boundaries. + from evennia.server.portal.portal import PORTAL + PORTAL.maintenance_task.stop() + import evennia evennia._init() return super(EvenniaTestSuiteRunner, self).build_suite( test_labels, extra_tests=extra_tests, **kwargs ) +