diff --git a/tests/test_management.py b/tests/test_management.py index 751d1b2..b18fa38 100644 --- a/tests/test_management.py +++ b/tests/test_management.py @@ -1,12 +1,78 @@ +import builtins from io import StringIO -from django.core.management import call_command +from django.core.management import call_command, CommandError from django.test import TestCase from fd_dj_accounts import management +from fd_dj_accounts.management.commands import createsuperuser from fd_dj_accounts.models import User +MOCK_INPUT_KEY_TO_PROMPTS = { + 'bypass': ['Bypass password validation and create user anyway? [y/N]: '], + 'email_address': ['Email address: '], + 'password': ['Password: '], +} + + +def mock_inputs(inputs): + """ + Decorator to temporarily replace input/getpass to allow interactive + createsuperuser. + """ + def inner(test_func): + def wrapped(*args): + class mock_getpass: + @staticmethod + def getpass(prompt=b'Password: ', stream=None): + if callable(inputs['password']): + return inputs['password']() + return inputs['password'] + + def mock_input(prompt): + assert '__proxy__' not in prompt + response = None + for key, val in inputs.items(): + if val == 'KeyboardInterrupt': + raise KeyboardInterrupt + # get() fallback because sometimes 'key' is the actual + # prompt rather than a shortcut name. + prompt_msgs = MOCK_INPUT_KEY_TO_PROMPTS.get(key, key) + if isinstance(prompt_msgs, list): + prompt_msgs = [msg() if callable(msg) else msg for msg in prompt_msgs] + if prompt in prompt_msgs: + if callable(val): + response = val() + else: + response = val + break + if response is None: + raise ValueError('Mock input for %r not found.' % prompt) + return response + + old_getpass = createsuperuser.getpass + old_input = builtins.input + createsuperuser.getpass = mock_getpass + builtins.input = mock_input + try: + test_func(*args) + finally: + createsuperuser.getpass = old_getpass + builtins.input = old_input + return wrapped + return inner + + +class MockTTY: + """ + A fake stdin object that pretends to be a TTY to be used in conjunction + with mock_inputs. + """ + def isatty(self): + return True + + class GetDefaultUsernameTestCase(TestCase): def test_simple(self) -> None: self.assertEqual(management.get_default_username(), '') @@ -36,6 +102,196 @@ def test_basic_usage(self) -> None: # created password should be unusable self.assertFalse(u.has_usable_password()) + def test_no_email_argument(self): + new_io = StringIO() + with self.assertRaisesMessage(CommandError, 'You must use --email_address with --noinput.'): + call_command('createsuperuser', interactive=False, stdout=new_io) + + def test_skip_if_not_in_TTY(self): + """ + If the command is not called from a TTY, it should be skipped and a + message should be displayed + """ + + class FakeStdin: + """A fake stdin object that has isatty() return False.""" + + def isatty(self): + return False + + out = StringIO() + call_command( + "createsuperuser", + stdin=FakeStdin(), + stdout=out, + interactive=True, + ) + + self.assertEqual(User._default_manager.count(), 0) + self.assertIn("Superuser creation skipped", out.getvalue()) + + def test_interactive_basic_usage(self): + @mock_inputs({ + 'email_address': 'new_user@somewhere.org', + 'password': 'nopasswd', + }) + def createsuperuser(): + new_io = StringIO() + call_command( + "createsuperuser", + interactive=True, + stdout=new_io, + stdin=MockTTY(), + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + + createsuperuser() + + users = User.objects.filter(email_address="new_user@somewhere.org") + self.assertEqual(users.count(), 1) + + def test_unique_usermane_validation(self): + new_io = StringIO() + # Create user with email address 'new_user@somewhere'. + call_command( + "createsuperuser", + interactive=False, + email_address="fake_email@somewhere.org", + stdout=new_io, + ) + + # The first two email_address duplicated emails, but the third is a valid one. + entered_emails = [ + "fake_email@somewhere.org", + "fake_email@somewhere.org", + "other_email@somewhere.org", + ] + + def duplicated_emails_then_valid(): + return entered_emails.pop(0) + + @mock_inputs({ + 'email_address': duplicated_emails_then_valid, + 'password': 'nopasswd', + }) + def createsuperuser(): + std_out = StringIO() + call_command( + "createsuperuser", + interactive=True, + stdin=MockTTY(), + stderr=std_out, + stdout=std_out, + ) + self.assertEqual( + std_out.getvalue().strip(), + "Error: That email address is already taken.\n" + "Superuser created successfully." + ) + + createsuperuser() + + users = User.objects.filter(email_address="new_user@somewhere.org") + self.assertEqual(users.count(), 1) + + def test_blank_username_non_interactive(self): + new_io = StringIO() + with self.assertRaisesMessage(CommandError, 'Email address cannot be blank.'): + call_command( + 'createsuperuser', + email_address='', + interactive=False, + stdin=MockTTY(), + stdout=new_io, + stderr=new_io, + ) + + def test_blank_username(self): + """Creation fails if --username is blank.""" + new_io = StringIO() + with self.assertRaisesMessage(CommandError, 'Email address cannot be blank.'): + call_command( + 'createsuperuser', + email_address='', + stdin=MockTTY(), + stdout=new_io, + stderr=new_io, + ) + + def test_validation_blank_password_entered(self): + """ + Creation should fail if the user enters blank passwords. + """ + new_io = StringIO() + + # The first two passwords are empty strings, but the second two are + # valid. + entered_passwords = ["", "", "password2", "password2"] + + def blank_passwords_then_valid(): + return entered_passwords.pop(0) + + @mock_inputs({ + 'password': blank_passwords_then_valid, + 'email_address': 'new_user@somewhere.org', + }) + def test(self): + call_command( + "createsuperuser", + interactive=True, + stdin=MockTTY(), + stdout=new_io, + stderr=new_io, + ) + self.assertEqual( + new_io.getvalue().strip(), + "Error: Blank passwords aren't allowed.\n" + "Superuser created successfully." + ) + + test(self) + + def test_password_validation_bypass(self): + """ + Password validation can be bypassed by entering 'y' at the prompt. + """ + new_io = StringIO() + + @mock_inputs({ + 'email_address': 'joe@example.com', + 'password': '1234567890', + 'bypass': 'y', + }) + def test(self): + call_command( + 'createsuperuser', + interactive=True, + stdin=MockTTY(), + stdout=new_io, + stderr=new_io, + ) + self.assertEqual( + new_io.getvalue().strip(), + 'This password is entirely numeric.\n' + 'Superuser created successfully.' + ) + + test(self) + + @mock_inputs({'email_address': 'KeyboardInterrupt'}) + def test_keyboard_interrupt(self): + new_io = StringIO() + with self.assertRaises(SystemExit): + call_command( + 'createsuperuser', + interactive=True, + stdin=MockTTY(), + stdout=new_io, + stderr=new_io, + ) + self.assertEqual(new_io.getvalue(), '\nOperation cancelled.\n') + # TODO: Add tests. # See: # - https://github.com/django/django/blob/3.2/tests/auth_tests/test_management.py#L253-L1036