diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index f6142e374..b8efc71d0 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -5,7 +5,6 @@ from uuid import uuid4 import dateutil.parser from dateutil.parser import ParserError -from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import ArrayField as DjangoArrayField from django.core.exceptions import ValidationError from django.core.files.base import ContentFile @@ -25,6 +24,14 @@ def validate_remote_id(value): params={'value': value}, ) +def validate_username(value): + ''' make sure usernames look okay ''' + if not re.match(r'^[A-Za-z\-_\.]+$', value): + raise ValidationError( + _('%(value)s is not a valid remote_id'), + params={'value': value}, + ) + class ActivitypubFieldMixin: ''' make a database field serializable ''' @@ -134,7 +141,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField): _('username'), max_length=150, unique=True, - validators=[AbstractUser.username_validator], + validators=[validate_username], error_messages={ 'unique': _('A user with that username already exists.'), }, diff --git a/bookwyrm/tests/test_view_actions.py b/bookwyrm/tests/test_view_actions.py new file mode 100644 index 000000000..bb0fcdb24 --- /dev/null +++ b/bookwyrm/tests/test_view_actions.py @@ -0,0 +1,239 @@ +''' test for app action functionality ''' +from unittest.mock import patch + +from django.core.exceptions import PermissionDenied +from django.http.response import Http404 +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import view_actions as actions, models +from bookwyrm.settings import DOMAIN + + +#pylint: disable=too-many-public-methods +class ViewActions(TestCase): + ''' a lot here: all handlers for receiving activitypub requests ''' + def setUp(self): + ''' we need basic things, like users ''' + self.local_user = models.User.objects.create_user( + 'mouse', 'mouse@mouse.com', 'mouseword', local=True) + self.local_user.remote_id = 'https://example.com/user/mouse' + self.local_user.save() + with patch('bookwyrm.models.user.set_remote_server.delay'): + self.remote_user = models.User.objects.create_user( + 'rat', 'rat@rat.com', 'ratword', + local=False, + remote_id='https://example.com/users/rat', + inbox='https://example.com/users/rat/inbox', + outbox='https://example.com/users/rat/outbox', + ) + self.status = models.Status.objects.create( + user=self.local_user, + content='Test status', + remote_id='https://example.com/status/1', + ) + self.settings = models.SiteSettings.objects.create(id=1) + self.factory = RequestFactory() + + + def test_register(self): + ''' create a user ''' + self.assertEqual(models.User.objects.count(), 2) + request = self.factory.post( + 'register/', + { + 'username': 'nutria-user.user_nutria', + 'password': 'mouseword', + 'email': 'aa@bb.cccc' + }) + with patch('bookwyrm.view_actions.login'): + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 3) + self.assertEqual(response.status_code, 302) + nutria = models.User.objects.last() + self.assertEqual(nutria.username, 'nutria-user.user_nutria@%s' % DOMAIN) + self.assertEqual(nutria.localname, 'nutria-user.user_nutria') + self.assertEqual(nutria.local, True) + + def test_register_trailing_space(self): + ''' django handles this so weirdly ''' + request = self.factory.post( + 'register/', + { + 'username': 'nutria ', + 'password': 'mouseword', + 'email': 'aa@bb.ccc' + }) + with patch('bookwyrm.view_actions.login'): + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 3) + self.assertEqual(response.status_code, 302) + nutria = models.User.objects.last() + self.assertEqual(nutria.username, 'nutria@%s' % DOMAIN) + self.assertEqual(nutria.localname, 'nutria') + self.assertEqual(nutria.local, True) + + def test_register_invalid_email(self): + ''' gotta have an email ''' + self.assertEqual(models.User.objects.count(), 2) + request = self.factory.post( + 'register/', + { + 'username': 'nutria', + 'password': 'mouseword', + 'email': 'aa' + }) + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 2) + self.assertEqual(response.template_name, 'login.html') + + def test_register_invalid_username(self): + ''' gotta have an email ''' + self.assertEqual(models.User.objects.count(), 2) + request = self.factory.post( + 'register/', + { + 'username': 'nut@ria', + 'password': 'mouseword', + 'email': 'aa@bb.ccc' + }) + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 2) + self.assertEqual(response.template_name, 'login.html') + + request = self.factory.post( + 'register/', + { + 'username': 'nutr ia', + 'password': 'mouseword', + 'email': 'aa@bb.ccc' + }) + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 2) + self.assertEqual(response.template_name, 'login.html') + + request = self.factory.post( + 'register/', + { + 'username': 'nut@ria', + 'password': 'mouseword', + 'email': 'aa@bb.ccc' + }) + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 2) + self.assertEqual(response.template_name, 'login.html') + + + def test_register_closed_instance(self): + ''' you can't just register ''' + self.settings.allow_registration = False + self.settings.save() + request = self.factory.post( + 'register/', + { + 'username': 'nutria ', + 'password': 'mouseword', + 'email': 'aa@bb.ccc' + }) + with self.assertRaises(PermissionDenied): + actions.register(request) + + def test_register_invite(self): + ''' you can't just register ''' + self.settings.allow_registration = False + self.settings.save() + models.SiteInvite.objects.create( + code='testcode', user=self.local_user, use_limit=1) + self.assertEqual(models.SiteInvite.objects.get().times_used, 0) + + request = self.factory.post( + 'register/', + { + 'username': 'nutria', + 'password': 'mouseword', + 'email': 'aa@bb.ccc', + 'invite_code': 'testcode' + }) + with patch('bookwyrm.view_actions.login'): + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 3) + self.assertEqual(response.status_code, 302) + self.assertEqual(models.SiteInvite.objects.get().times_used, 1) + + # invalid invite + request = self.factory.post( + 'register/', + { + 'username': 'nutria2', + 'password': 'mouseword', + 'email': 'aa@bb.ccc', + 'invite_code': 'testcode' + }) + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 3) + + # bad invite code + request = self.factory.post( + 'register/', + { + 'username': 'nutria3', + 'password': 'mouseword', + 'email': 'aa@bb.ccc', + 'invite_code': 'dkfkdjgdfkjgkdfj' + }) + with self.assertRaises(Http404): + response = actions.register(request) + self.assertEqual(models.User.objects.count(), 3) + + + def test_password_reset_request(self): + ''' send 'em an email ''' + request = self.factory.post('', {'email': 'aa@bb.ccc'}) + resp = actions.password_reset_request(request) + self.assertEqual(resp.status_code, 302) + + request = self.factory.post( + '', {'email': 'mouse@mouse.com'}) + with patch('bookwyrm.emailing.send_email.delay'): + resp = actions.password_reset_request(request) + self.assertEqual(resp.template_name, 'password_reset_request.html') + + self.assertEqual( + models.PasswordReset.objects.get().user, self.local_user) + + def test_password_reset(self): + ''' reset from code ''' + code = models.PasswordReset.objects.create(user=self.local_user) + request = self.factory.post('', { + 'reset-code': code.code, + 'password': 'hi', + 'confirm-password': 'hi' + }) + with patch('bookwyrm.view_actions.login'): + resp = actions.password_reset(request) + self.assertEqual(resp.status_code, 302) + self.assertFalse(models.PasswordReset.objects.exists()) + + def test_password_reset_wrong_code(self): + ''' reset from code ''' + models.PasswordReset.objects.create(user=self.local_user) + request = self.factory.post('', { + 'reset-code': 'jhgdkfjgdf', + 'password': 'hi', + 'confirm-password': 'hi' + }) + resp = actions.password_reset(request) + self.assertEqual(resp.template_name, 'password_reset.html') + self.assertTrue(models.PasswordReset.objects.exists()) + + def test_password_reset_mismatch(self): + ''' reset from code ''' + code = models.PasswordReset.objects.create(user=self.local_user) + request = self.factory.post('', { + 'reset-code': code.code, + 'password': 'hi', + 'confirm-password': 'hihi' + }) + resp = actions.password_reset(request) + self.assertEqual(resp.template_name, 'password_reset.html') + self.assertTrue(models.PasswordReset.objects.exists()) diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 7126b1b22..f193e1277 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -66,7 +66,7 @@ def register(request): if not form.is_valid(): errors = True - username = form.data['username'] + username = form.data['username'].strip() email = form.data['email'] password = form.data['password'] @@ -215,6 +215,7 @@ def edit_profile(request): return redirect('/user/%s' % request.user.localname) +@require_POST def resolve_book(request): ''' figure out the local path to a book from a remote_id ''' remote_id = request.POST.get('remote_id')