Merge pull request #423 from mouse-reeve/validate-username

Makes registration user/localname fields more sensible
This commit is contained in:
Mouse Reeve 2021-01-04 12:11:56 -08:00 committed by GitHub
commit c996a413a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 182 additions and 100 deletions

View file

@ -35,7 +35,7 @@ class CustomForm(ModelForm):
class LoginForm(CustomForm): class LoginForm(CustomForm):
class Meta: class Meta:
model = models.User model = models.User
fields = ['username', 'password'] fields = ['localname', 'password']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
widgets = { widgets = {
'password': PasswordInput(), 'password': PasswordInput(),
@ -45,7 +45,7 @@ class LoginForm(CustomForm):
class RegisterForm(CustomForm): class RegisterForm(CustomForm):
class Meta: class Meta:
model = models.User model = models.User
fields = ['username', 'email', 'password'] fields = ['localname', 'email', 'password']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
widgets = { widgets = {
'password': PasswordInput() 'password': PasswordInput()

View file

@ -0,0 +1,19 @@
# Generated by Django 3.0.7 on 2020-12-24 19:39
import bookwyrm.models.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0029_auto_20201221_2014'),
]
operations = [
migrations.AlterField(
model_name='user',
name='localname',
field=models.CharField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_localname]),
),
]

View file

@ -26,11 +26,20 @@ def validate_remote_id(value):
) )
def validate_localname(value):
''' make sure localnames look okay '''
if not re.match(r'^[A-Za-z\-_\.0-9]+$', value):
raise ValidationError(
_('%(value)s is not a valid username'),
params={'value': value},
)
def validate_username(value): def validate_username(value):
''' make sure usernames look okay ''' ''' make sure usernames look okay '''
if not re.match(r'^[A-Za-z\-_\.]+$', value): if not re.match(r'^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$', value):
raise ValidationError( raise ValidationError(
_('%(value)s is not a valid remote_id'), _('%(value)s is not a valid username'),
params={'value': value}, params={'value': value},
) )
@ -147,7 +156,7 @@ class RemoteIdField(ActivitypubFieldMixin, models.CharField):
class UsernameField(ActivitypubFieldMixin, models.CharField): class UsernameField(ActivitypubFieldMixin, models.CharField):
''' activitypub-aware username field ''' ''' activitypub-aware username field '''
def __init__(self, activitypub_field='preferredUsername'): def __init__(self, activitypub_field='preferredUsername', **kwargs):
self.activitypub_field = activitypub_field self.activitypub_field = activitypub_field
# I don't totally know why pylint is mad at this, but it makes it work # I don't totally know why pylint is mad at this, but it makes it work
super( #pylint: disable=bad-super-call super( #pylint: disable=bad-super-call

View file

@ -1,4 +1,5 @@
''' database schema for user data ''' ''' database schema for user data '''
import re
from urllib.parse import urlparse from urllib.parse import urlparse
from django.apps import apps from django.apps import apps
@ -13,6 +14,7 @@ from bookwyrm.models.status import Status, Review
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from bookwyrm.signatures import create_key_pair from bookwyrm.signatures import create_key_pair
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.utils import regex
from .base_model import OrderedCollectionPageMixin from .base_model import OrderedCollectionPageMixin
from .base_model import ActivitypubMixin, BookWyrmModel from .base_model import ActivitypubMixin, BookWyrmModel
from .federated_server import FederatedServer from .federated_server import FederatedServer
@ -49,7 +51,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
localname = models.CharField( localname = models.CharField(
max_length=255, max_length=255,
null=True, null=True,
unique=True unique=True,
validators=[fields.validate_localname],
) )
# name is your display name, which you can change at will # name is your display name, which you can change at will
name = fields.CharField(max_length=100, null=True, blank=True) name = fields.CharField(max_length=100, null=True, blank=True)
@ -167,20 +170,17 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
''' populate fields for new local users ''' ''' populate fields for new local users '''
# this user already exists, no need to populate fields # this user already exists, no need to populate fields
if self.id: if not self.local and not re.match(regex.full_username, self.username):
return super().save(*args, **kwargs)
if not self.local:
# generate a username that uses the domain (webfinger format) # generate a username that uses the domain (webfinger format)
actor_parts = urlparse(self.remote_id) actor_parts = urlparse(self.remote_id)
self.username = '%s@%s' % (self.username, actor_parts.netloc) self.username = '%s@%s' % (self.username, actor_parts.netloc)
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
if self.id or not self.local:
return super().save(*args, **kwargs)
# populate fields for local users # populate fields for local users
self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.username) self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.localname)
self.localname = self.username
self.username = '%s@%s' % (self.username, DOMAIN)
self.actor = self.remote_id
self.inbox = '%s/inbox' % self.remote_id self.inbox = '%s/inbox' % self.remote_id
self.shared_inbox = 'https://%s/inbox' % DOMAIN self.shared_inbox = 'https://%s/inbox' % DOMAIN
self.outbox = '%s/outbox' % self.remote_id self.outbox = '%s/outbox' % self.remote_id

View file

@ -119,15 +119,15 @@
</div> </div>
{% else %} {% else %}
<div class="navbar-item"> <div class="navbar-item">
{% if request.path != '/login' and request.path != '/login/' %} {% if request.path != '/login' and request.path != '/login/' and request.path != '/user-login' %}
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<form name="login" method="post" action="/user-login"> <form name="login" method="post" action="/user-login">
{% csrf_token %} {% csrf_token %}
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<label class="is-sr-only" for="id_username">Username:</label> <label class="is-sr-only" for="id_localname">Username:</label>
<input type="text" name="username" maxlength="150" class="input" required="" id="id_username" placeholder="username"> <input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname" placeholder="username">
</div> </div>
<div class="control"> <div class="control">
<label class="is-sr-only" for="id_password">Username:</label> <label class="is-sr-only" for="id_password">Username:</label>

View file

@ -11,9 +11,9 @@
<form name="login" method="post" action="/user-login"> <form name="login" method="post" action="/user-login">
{% csrf_token %} {% csrf_token %}
<div class="field"> <div class="field">
<label class="label" for="id_username">Username:</label> <label class="label" for="id_localname">Username:</label>
<div class="control"> <div class="control">
{{ login_form.username }} {{ login_form.localname }}
</div> </div>
</div> </div>
<div class="field"> <div class="field">

View file

@ -1,10 +1,10 @@
{% csrf_token %} {% csrf_token %}
<div class="field"> <div class="field">
<label class="label" for="id_username_register">Username:</label> <label class="label" for="id_localname_register">Username:</label>
<div class="control"> <div class="control">
<input type="text" name="username" maxlength="150" class="input" required="" id="id_username_register" value="{% if register_form.username.value %}{{ register_form.username.value }} {% endif %}"> <input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname_register" value="{% if register_form.localname.value %}{{ register_form.localname.value }}{% endif %}">
</div> </div>
{% for error in register_form.username.errors %} {% for error in register_form.localname.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -20,7 +20,8 @@ class BaseActivity(TestCase):
def setUp(self): def setUp(self):
''' we're probably going to re-use this so why copy/paste ''' ''' we're probably going to re-use this so why copy/paste '''
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
self.user.remote_id = 'http://example.com/a/b' self.user.remote_id = 'http://example.com/a/b'
self.user.save() self.user.save()

View file

@ -22,7 +22,8 @@ class BaseModel(TestCase):
def test_remote_id_with_user(self): def test_remote_id_with_user(self):
''' format of remote id when there's a user object ''' ''' format of remote id when there's a user object '''
user = models.User.objects.create_user( user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
instance = base_model.BookWyrmModel() instance = base_model.BookWyrmModel()
instance.user = user instance.user = user
instance.id = 1 instance.id = 1
@ -51,7 +52,8 @@ class BaseModel(TestCase):
def test_to_create_activity(self): def test_to_create_activity(self):
''' wrapper for ActivityPub "create" action ''' ''' wrapper for ActivityPub "create" action '''
user = models.User.objects.create_user( user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
object_activity = { object_activity = {
'to': 'to field', 'cc': 'cc field', 'to': 'to field', 'cc': 'cc field',
@ -81,7 +83,8 @@ class BaseModel(TestCase):
def test_to_delete_activity(self): def test_to_delete_activity(self):
''' wrapper for Delete activity ''' ''' wrapper for Delete activity '''
user = models.User.objects.create_user( user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity')) MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf( mock_self = MockSelf(
@ -105,7 +108,8 @@ class BaseModel(TestCase):
def test_to_update_activity(self): def test_to_update_activity(self):
''' ditto above but for Update ''' ''' ditto above but for Update '''
user = models.User.objects.create_user( user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity')) MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf( mock_self = MockSelf(
@ -129,7 +133,8 @@ class BaseModel(TestCase):
def test_to_undo_activity(self): def test_to_undo_activity(self):
''' and again, for Undo ''' ''' and again, for Undo '''
user = models.User.objects.create_user( user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity')) MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf( mock_self = MockSelf(
@ -173,7 +178,8 @@ class BaseModel(TestCase):
book = models.Edition.objects.create( book = models.Edition.objects.create(
title='Test Edition', remote_id='http://book.com/book') title='Test Edition', remote_id='http://book.com/book')
user = models.User.objects.create_user( user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
user.remote_id = 'http://example.com/a/b' user.remote_id = 'http://example.com/a/b'
user.save() user.save()

View file

@ -111,10 +111,17 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.max_length, 150) self.assertEqual(instance.max_length, 150)
self.assertEqual(instance.unique, True) self.assertEqual(instance.unique, True)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
instance.run_validators('one two') instance.run_validators('mouse')
instance.run_validators('a*&') instance.run_validators('mouseexample.com')
instance.run_validators('trailingwhite ') instance.run_validators('mouse@example.c')
self.assertIsNone(instance.run_validators('aksdhf')) instance.run_validators('@example.com')
instance.run_validators('mouse@examplecom')
instance.run_validators('one two@fish.aaaa')
instance.run_validators('a*&@exampke.com')
instance.run_validators('trailingwhite@example.com ')
self.assertIsNone(instance.run_validators('mouse@example.com'))
self.assertIsNone(instance.run_validators('mo-2use@ex3ample.com'))
self.assertIsNone(instance.run_validators('aksdhf@sdkjf-df.cm'))
self.assertEqual(instance.field_to_activity('test@example.com'), 'test') self.assertEqual(instance.field_to_activity('test@example.com'), 'test')
@ -173,7 +180,8 @@ class ActivitypubFields(TestCase):
def test_privacy_field_set_activity_from_field(self): def test_privacy_field_set_activity_from_field(self):
''' translate between to/cc fields and privacy ''' ''' translate between to/cc fields and privacy '''
user = User.objects.create_user( user = User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', local=True) 'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
public = 'https://www.w3.org/ns/activitystreams#Public' public = 'https://www.w3.org/ns/activitystreams#Public'
followers = '%s/followers' % user.remote_id followers = '%s/followers' % user.remote_id
@ -230,7 +238,8 @@ class ActivitypubFields(TestCase):
# it shouldn't match with this unrelated user: # it shouldn't match with this unrelated user:
unrelated_user = User.objects.create_user( unrelated_user = User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', local=True) 'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
# test receiving an unknown remote id and loading data # test receiving an unknown remote id and loading data
responses.add( responses.add(
@ -258,7 +267,8 @@ class ActivitypubFields(TestCase):
# it shouldn't match with this unrelated user: # it shouldn't match with this unrelated user:
unrelated_user = User.objects.create_user( unrelated_user = User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', local=True) 'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
with patch('bookwyrm.models.user.set_remote_server.delay'): with patch('bookwyrm.models.user.set_remote_server.delay'):
value = instance.field_from_activity(userdata) value = instance.field_from_activity(userdata)
self.assertIsInstance(value, User) self.assertIsInstance(value, User)
@ -276,11 +286,13 @@ class ActivitypubFields(TestCase):
) )
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())
user = User.objects.create_user( user = User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
user.remote_id = 'https://example.com/user/mouse' user.remote_id = 'https://example.com/user/mouse'
user.save() user.save()
User.objects.create_user( User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', local=True) 'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
value = instance.field_from_activity(userdata) value = instance.field_from_activity(userdata)
self.assertEqual(value, user) self.assertEqual(value, user)
@ -290,9 +302,11 @@ class ActivitypubFields(TestCase):
''' test receiving a remote id of an existing object in the db ''' ''' test receiving a remote id of an existing object in the db '''
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
user = User.objects.create_user( user = User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
User.objects.create_user( User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', local=True) 'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
value = instance.field_from_activity(user.remote_id) value = instance.field_from_activity(user.remote_id)
self.assertEqual(value, user) self.assertEqual(value, user)
@ -382,7 +396,8 @@ class ActivitypubFields(TestCase):
def test_image_field(self): def test_image_field(self):
''' storing images ''' ''' storing images '''
user = User.objects.create_user( user = User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
'../../static/images/default_avi.jpg') '../../static/images/default_avi.jpg')
image = Image.open(image_file) image = Image.open(image_file)

View file

@ -59,7 +59,8 @@ class ImportJob(TestCase):
unknown_read_data['Date Read'] = '' unknown_read_data['Date Read'] = ''
user = models.User.objects.create_user( user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
job = models.ImportJob.objects.create(user=user) job = models.ImportJob.objects.create(user=user)
self.item_1 = models.ImportItem.objects.create( self.item_1 = models.ImportItem.objects.create(
job=job, index=1, data=currently_reading_data) job=job, index=1, data=currently_reading_data)

View file

@ -16,7 +16,8 @@ class Relationship(TestCase):
outbox='https://example.com/users/rat/outbox', outbox='https://example.com/users/rat/outbox',
) )
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'http://local.com/user/mouse' self.local_user.remote_id = 'http://local.com/user/mouse'
self.local_user.save() self.local_user.save()

View file

@ -9,7 +9,8 @@ class Shelf(TestCase):
def setUp(self): def setUp(self):
''' look, a shelf ''' ''' look, a shelf '''
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
self.shelf = models.Shelf.objects.create( self.shelf = models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf', user=self.user) name='Test Shelf', identifier='test-shelf', user=self.user)

View file

@ -16,7 +16,8 @@ class Status(TestCase):
def setUp(self): def setUp(self):
''' useful things for creating a status ''' ''' useful things for creating a status '''
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
self.book = models.Edition.objects.create(title='Test Edition') self.book = models.Edition.objects.create(title='Test Edition')
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(

View file

@ -9,7 +9,8 @@ from bookwyrm.settings import DOMAIN
class User(TestCase): class User(TestCase):
def setUp(self): def setUp(self):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse@%s' % DOMAIN, 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
def test_computed_fields(self): def test_computed_fields(self):
''' username instead of id here ''' ''' username instead of id here '''

View file

@ -7,10 +7,12 @@ from bookwyrm import models, broadcast
class Book(TestCase): class Book(TestCase):
def setUp(self): def setUp(self):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
local_follower = models.User.objects.create_user( local_follower = models.User.objects.create_user(
'joe', 'joe@mouse.mouse', 'jeoword', local=True) 'joe', 'joe@mouse.mouse', 'jeoword',
local=True, localname='joe')
self.user.followers.add(local_follower) self.user.followers.add(local_follower)
with patch('bookwyrm.models.user.set_remote_server.delay'): with patch('bookwyrm.models.user.set_remote_server.delay'):

View file

@ -19,7 +19,8 @@ class Incoming(TestCase):
def setUp(self): def setUp(self):
''' we need basic things, like users ''' ''' we need basic things, like users '''
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse@example.com', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'https://example.com/user/mouse' self.local_user.remote_id = 'https://example.com/user/mouse'
self.local_user.save() self.local_user.save()
with patch('bookwyrm.models.user.set_remote_server.delay'): with patch('bookwyrm.models.user.set_remote_server.delay'):
@ -486,6 +487,10 @@ class Incoming(TestCase):
def test_handle_update_user(self): def test_handle_update_user(self):
''' update an existing user ''' ''' update an existing user '''
# we only do this with remote users
self.local_user.local = False
self.local_user.save()
datafile = pathlib.Path(__file__).parent.joinpath( datafile = pathlib.Path(__file__).parent.joinpath(
'data/ap_user.json') 'data/ap_user.json')
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())
@ -494,6 +499,8 @@ class Incoming(TestCase):
incoming.handle_update_user({'object': userdata}) incoming.handle_update_user({'object': userdata})
user = models.User.objects.get(id=self.local_user.id) user = models.User.objects.get(id=self.local_user.id)
self.assertEqual(user.name, 'MOUSE?? MOUSE!!') self.assertEqual(user.name, 'MOUSE?? MOUSE!!')
self.assertEqual(user.username, 'mouse@example.com')
self.assertEqual(user.localname, 'mouse')
def test_handle_update_edition(self): def test_handle_update_edition(self):

View file

@ -21,15 +21,16 @@ class Outgoing(TestCase):
self.factory = RequestFactory() self.factory = RequestFactory()
with patch('bookwyrm.models.user.set_remote_server'): with patch('bookwyrm.models.user.set_remote_server'):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword', 'rat', 'rat@email.com', 'ratword',
local=False, local=False,
remote_id='https://example.com/users/rat', remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox', inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox', outbox='https://example.com/users/rat/outbox',
) )
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True, 'mouse@local.com', 'mouse@mouse.com', 'mouseword',
localname='mouse', remote_id='https://example.com/users/mouse', local=True, localname='mouse',
remote_id='https://example.com/users/mouse',
) )
datafile = pathlib.Path(__file__).parent.joinpath( datafile = pathlib.Path(__file__).parent.joinpath(
@ -175,10 +176,10 @@ class Outgoing(TestCase):
def test_existing_user(self): def test_existing_user(self):
''' simple database lookup by username ''' ''' simple database lookup by username '''
result = outgoing.handle_remote_webfinger('@mouse@%s' % DOMAIN) result = outgoing.handle_remote_webfinger('@mouse@local.com')
self.assertEqual(result, self.local_user) self.assertEqual(result, self.local_user)
result = outgoing.handle_remote_webfinger('mouse@%s' % DOMAIN) result = outgoing.handle_remote_webfinger('mouse@local.com')
self.assertEqual(result, self.local_user) self.assertEqual(result, self.local_user)
@ -398,7 +399,8 @@ class Outgoing(TestCase):
def test_handle_status_mentions(self): def test_handle_status_mentions(self):
''' @mention a user in a post ''' ''' @mention a user in a post '''
user = models.User.objects.create_user( user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'password', local=True) 'rat@%s' % DOMAIN, 'rat@rat.com', 'password',
local=True, localname='rat')
form = forms.CommentForm({ form = forms.CommentForm({
'content': 'hi @rat', 'content': 'hi @rat',
'user': self.local_user.id, 'user': self.local_user.id,
@ -409,16 +411,17 @@ class Outgoing(TestCase):
with patch('bookwyrm.broadcast.broadcast_task.delay'): with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(self.local_user, form) outgoing.handle_status(self.local_user, form)
status = models.Status.objects.get() status = models.Status.objects.get()
self.assertEqual(list(status.mention_users.all()), [user])
self.assertEqual(models.Notification.objects.get().user, user)
self.assertEqual( self.assertEqual(
status.content, status.content,
'<p>hi <a href="%s">@rat</a></p>' % user.remote_id) '<p>hi <a href="%s">@rat</a></p>' % user.remote_id)
self.assertEqual(list(status.mention_users.all()), [user])
self.assertEqual(models.Notification.objects.get().user, user)
def test_handle_status_reply_with_mentions(self): def test_handle_status_reply_with_mentions(self):
''' reply to a post with an @mention'ed user ''' ''' reply to a post with an @mention'ed user '''
user = models.User.objects.create_user( user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'password', local=True) 'rat', 'rat@rat.com', 'password',
local=True, localname='rat')
form = forms.CommentForm({ form = forms.CommentForm({
'content': 'hi @rat@example.com', 'content': 'hi @rat@example.com',
'user': self.local_user.id, 'user': self.local_user.id,

View file

@ -31,11 +31,14 @@ Sender = namedtuple('Sender', ('remote_id', 'key_pair'))
class Signature(TestCase): class Signature(TestCase):
def setUp(self): def setUp(self):
self.mouse = User.objects.create_user( self.mouse = User.objects.create_user(
'mouse', 'mouse@example.com', '', local=True) 'mouse@%s' % DOMAIN, 'mouse@example.com', '',
local=True, localname='mouse')
self.rat = User.objects.create_user( self.rat = User.objects.create_user(
'rat', 'rat@example.com', '', local=True) 'rat@%s' % DOMAIN, 'rat@example.com', '',
local=True, localname='rat')
self.cat = User.objects.create_user( self.cat = User.objects.create_user(
'cat', 'cat@example.com', '', local=True) 'cat@%s' % DOMAIN, 'cat@example.com', '',
local=True, localname='cat')
private_key, public_key = create_key_pair() private_key, public_key = create_key_pair()

View file

@ -16,7 +16,8 @@ class TemplateTags(TestCase):
def setUp(self): def setUp(self):
''' create some filler objects ''' ''' create some filler objects '''
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) 'mouse@example.com', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
with patch('bookwyrm.models.user.set_remote_server.delay'): with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', 'rat', 'rat@rat.rat', 'ratword',

View file

@ -18,7 +18,8 @@ class ViewActions(TestCase):
def setUp(self): def setUp(self):
''' we need basic things, like users ''' ''' we need basic things, like users '''
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True) 'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'https://example.com/user/mouse' self.local_user.remote_id = 'https://example.com/user/mouse'
self.local_user.save() self.local_user.save()
self.group = Group.objects.create(name='editor') self.group = Group.objects.create(name='editor')
@ -54,7 +55,7 @@ class ViewActions(TestCase):
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nutria-user.user_nutria', 'localname': 'nutria-user.user_nutria',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.cccc' 'email': 'aa@bb.cccc'
}) })
@ -72,7 +73,7 @@ class ViewActions(TestCase):
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nutria ', 'localname': 'nutria ',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.ccc' 'email': 'aa@bb.ccc'
}) })
@ -91,7 +92,7 @@ class ViewActions(TestCase):
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nutria', 'localname': 'nutria',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa' 'email': 'aa'
}) })
@ -105,7 +106,7 @@ class ViewActions(TestCase):
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nut@ria', 'localname': 'nut@ria',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.ccc' 'email': 'aa@bb.ccc'
}) })
@ -116,7 +117,7 @@ class ViewActions(TestCase):
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nutr ia', 'localname': 'nutr ia',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.ccc' 'email': 'aa@bb.ccc'
}) })
@ -127,7 +128,7 @@ class ViewActions(TestCase):
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nut@ria', 'localname': 'nut@ria',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.ccc' 'email': 'aa@bb.ccc'
}) })
@ -143,7 +144,7 @@ class ViewActions(TestCase):
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nutria ', 'localname': 'nutria ',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.ccc' 'email': 'aa@bb.ccc'
}) })
@ -161,7 +162,7 @@ class ViewActions(TestCase):
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nutria', 'localname': 'nutria',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.ccc', 'email': 'aa@bb.ccc',
'invite_code': 'testcode' 'invite_code': 'testcode'
@ -172,23 +173,24 @@ class ViewActions(TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(models.SiteInvite.objects.get().times_used, 1) self.assertEqual(models.SiteInvite.objects.get().times_used, 1)
# invalid invite # invite already used to max capacity
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nutria2', 'localname': 'nutria2',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.ccc', 'email': 'aa@bb.ccc',
'invite_code': 'testcode' 'invite_code': 'testcode'
}) })
response = actions.register(request) with self.assertRaises(PermissionDenied):
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 3) self.assertEqual(models.User.objects.count(), 3)
# bad invite code # bad invite code
request = self.factory.post( request = self.factory.post(
'register/', 'register/',
{ {
'username': 'nutria3', 'localname': 'nutria3',
'password': 'mouseword', 'password': 'mouseword',
'email': 'aa@bb.ccc', 'email': 'aa@bb.ccc',
'invite_code': 'dkfkdjgdfkjgkdfj' 'invite_code': 'dkfkdjgdfkjgkdfj'
@ -379,7 +381,7 @@ class ViewActions(TestCase):
def test_untag(self): def test_untag(self):
''' remove a tag from a book ''' ''' remove a tag from a book '''
tag = models.Tag.objects.create(name='A Tag!?') tag = models.Tag.objects.create(name='A Tag!?')
user_tag = models.UserTag.objects.create( models.UserTag.objects.create(
user=self.local_user, book=self.book, tag=tag) user=self.local_user, book=self.book, tag=tag)
request = self.factory.post( request = self.factory.post(
'', { '', {

View file

@ -9,6 +9,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import abstract_connector from bookwyrm.connectors import abstract_connector
from bookwyrm.settings import DOMAIN, USER_AGENT from bookwyrm.settings import DOMAIN, USER_AGENT
@ -28,7 +29,8 @@ class Views(TestCase):
local=True local=True
) )
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'password', local=True) 'mouse@local.com', 'mouse@mouse.mouse', 'password',
local=True, localname='mouse')
with patch('bookwyrm.models.user.set_remote_server.delay'): with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword', 'rat', 'rat@rat.com', 'ratword',
@ -52,7 +54,7 @@ class Views(TestCase):
self.assertEqual( self.assertEqual(
views.get_user_from_username('mouse'), self.local_user) views.get_user_from_username('mouse'), self.local_user)
self.assertEqual( self.assertEqual(
views.get_user_from_username('mouse@%s' % DOMAIN), self.local_user) views.get_user_from_username('mouse@local.com'), self.local_user)
with self.assertRaises(models.User.DoesNotExist): with self.assertRaises(models.User.DoesNotExist):
views.get_user_from_username('mojfse@example.com') views.get_user_from_username('mojfse@example.com')
@ -343,7 +345,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.user_page(request, 'mouse') result = views.user_page(request, 'mouse')
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -361,7 +363,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.followers_page(request, 'mouse') result = views.followers_page(request, 'mouse')
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -379,7 +381,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.following_page(request, 'mouse') result = views.following_page(request, 'mouse')
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -399,7 +401,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.status_page(request, 'mouse', status.id) result = views.status_page(request, 'mouse', status.id)
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -419,7 +421,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.replies_page(request, 'mouse', status.id) result = views.replies_page(request, 'mouse', status.id)
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -448,7 +450,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.book_page(request, self.book.id) result = views.book_page(request, self.book.id)
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -489,7 +491,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.editions_page(request, self.work.id) result = views.editions_page(request, self.work.id)
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -508,7 +510,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.author_page(request, author.id) result = views.author_page(request, author.id)
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -529,7 +531,7 @@ class Views(TestCase):
with patch('bookwyrm.views.is_api_request') as is_api: with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = views.tag_page(request, tag.identifier) result = views.tag_page(request, tag.identifier)
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@ -550,18 +552,22 @@ class Views(TestCase):
is_api.return_value = True is_api.return_value = True
result = views.shelf_page( result = views.shelf_page(
request, self.local_user.username, shelf.identifier) request, self.local_user.username, shelf.identifier)
self.assertIsInstance(result, JsonResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_is_bookwyrm_request(self): def test_is_bookwyrm_request(self):
''' tests the function that checks if a request came from a bookwyrm instance ''' ''' checks if a request came from a bookwyrm instance '''
request = self.factory.get('', {'q': 'Test Book'}) request = self.factory.get('', {'q': 'Test Book'})
self.assertFalse(views.is_bookworm_request(request)) self.assertFalse(views.is_bookworm_request(request))
request = self.factory.get('', {'q': 'Test Book'}, request = self.factory.get(
HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)") '', {'q': 'Test Book'},
HTTP_USER_AGENT=\
"http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)"
)
self.assertFalse(views.is_bookworm_request(request)) self.assertFalse(views.is_bookworm_request(request))
request = self.factory.get('', {'q': 'Test Book'}, HTTP_USER_AGENT=USER_AGENT) request = self.factory.get(
'', {'q': 'Test Book'}, HTTP_USER_AGENT=USER_AGENT)
self.assertTrue(views.is_bookworm_request(request)) self.assertTrue(views.is_bookworm_request(request))

View file

@ -30,8 +30,8 @@ def user_login(request):
''' authenticate user login ''' ''' authenticate user login '''
login_form = forms.LoginForm(request.POST) login_form = forms.LoginForm(request.POST)
username = login_form.data['username'] localname = login_form.data['localname']
username = '%s@%s' % (username, DOMAIN) username = '%s@%s' % (localname, DOMAIN)
password = login_form.data['password'] password = login_form.data['password']
user = authenticate(request, username=username, password=password) user = authenticate(request, username=username, password=password)
if user is not None: if user is not None:
@ -59,6 +59,8 @@ def register(request):
raise PermissionDenied raise PermissionDenied
invite = get_object_or_404(models.SiteInvite, code=invite_code) invite = get_object_or_404(models.SiteInvite, code=invite_code)
if not invite.valid():
raise PermissionDenied
else: else:
invite = None invite = None
@ -67,13 +69,13 @@ def register(request):
if not form.is_valid(): if not form.is_valid():
errors = True errors = True
username = form.data['username'].strip() localname = form.data['localname'].strip()
email = form.data['email'] email = form.data['email']
password = form.data['password'] password = form.data['password']
# check username and email uniqueness # check localname and email uniqueness
if models.User.objects.filter(localname=username).first(): if models.User.objects.filter(localname=localname).first():
form.add_error('username', 'User with this username already exists') form.errors['localname'] = ['User with this username already exists']
errors = True errors = True
if errors: if errors:
@ -83,8 +85,9 @@ def register(request):
} }
return TemplateResponse(request, 'login.html', data) return TemplateResponse(request, 'login.html', data)
username = '%s@%s' % (localname, DOMAIN)
user = models.User.objects.create_user( user = models.User.objects.create_user(
username, email, password, local=True) username, email, password, localname=localname, local=True)
if invite: if invite:
invite.times_used += 1 invite.times_used += 1
invite.save() invite.save()

2
bw-dev
View file

@ -83,7 +83,7 @@ case "$1" in
;; ;;
pytest) pytest)
shift 1 shift 1
execweb pytest "$@" execweb pytest --no-cov-on-fail "$@"
;; ;;
test_report) test_report)
execweb coverage report execweb coverage report