Merge pull request #390 from mouse-reeve/incoming-tests

Incoming tests
This commit is contained in:
Mouse Reeve 2020-12-15 12:43:28 -08:00 committed by GitHub
commit 9d7ad3c492
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 610 additions and 204 deletions

View File

@ -6,6 +6,7 @@ import django.db.utils
from django.http import HttpResponse from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import requests import requests
from bookwyrm import activitypub, models, outgoing from bookwyrm import activitypub, models, outgoing
@ -15,6 +16,7 @@ from bookwyrm.signatures import Signature
@csrf_exempt @csrf_exempt
@require_POST
def inbox(request, username): def inbox(request, username):
''' incoming activitypub events ''' ''' incoming activitypub events '''
try: try:
@ -26,11 +28,9 @@ def inbox(request, username):
@csrf_exempt @csrf_exempt
@require_POST
def shared_inbox(request): def shared_inbox(request):
''' incoming activitypub events ''' ''' incoming activitypub events '''
if request.method == 'GET':
return HttpResponseNotFound()
try: try:
resp = request.body resp = request.body
activity = json.loads(resp) activity = json.loads(resp)
@ -66,8 +66,8 @@ def shared_inbox(request):
}, },
'Update': { 'Update': {
'Person': handle_update_user, 'Person': handle_update_user,
'Edition': handle_update_book, 'Edition': handle_update_edition,
'Work': handle_update_book, 'Work': handle_update_work,
}, },
} }
activity_type = activity['type'] activity_type = activity['type']
@ -141,7 +141,7 @@ def handle_follow(activity):
def handle_unfollow(activity): def handle_unfollow(activity):
''' unfollow a local user ''' ''' unfollow a local user '''
obj = activity['object'] obj = activity['object']
requester = activitypub.resolve_remote_id(models.user, obj['actor']) requester = activitypub.resolve_remote_id(models.User, obj['actor'])
to_unfollow = models.User.objects.get(remote_id=obj['object']) to_unfollow = models.User.objects.get(remote_id=obj['object'])
# raises models.User.DoesNotExist # raises models.User.DoesNotExist
@ -226,6 +226,16 @@ def handle_create(activity):
related_user=status.user, related_user=status.user,
related_status=status, related_status=status,
) )
if status.mention_users.exists():
for mentioned_user in status.mention_users.all():
if not mentioned_user.local:
continue
status_builder.create_notification(
mentioned_user,
'MENTION',
related_user=status.user,
related_status=status,
)
@app.task @app.task
@ -238,11 +248,12 @@ def handle_delete_status(activity):
# is trying to delete a user. # is trying to delete a user.
return return
try: try:
status = models.Status.objects.select_subclasses().get( status = models.Status.objects.get(
remote_id=status_id remote_id=status_id
) )
except models.Status.DoesNotExist: except models.Status.DoesNotExist:
return return
models.Notification.objects.filter(related_status=status).all().delete()
status_builder.delete_status(status) status_builder.delete_status(status)
@ -327,6 +338,12 @@ def handle_update_user(activity):
@app.task @app.task
def handle_update_book(activity): def handle_update_edition(activity):
''' a remote instance changed a book (Document) ''' ''' a remote instance changed a book (Document) '''
activitypub.Edition(**activity['object']).to_model(models.Edition) activitypub.Edition(**activity['object']).to_model(models.Edition)
@app.task
def handle_update_work(activity):
''' a remote instance changed a book (Document) '''
activitypub.Work(**activity['object']).to_model(models.Work)

View File

@ -42,7 +42,13 @@ class ActivitypubFieldMixin:
def set_field_from_activity(self, instance, data): def set_field_from_activity(self, instance, data):
''' helper function for assinging a value to the field ''' ''' helper function for assinging a value to the field '''
value = getattr(data, self.get_activitypub_field()) try:
value = getattr(data, self.get_activitypub_field())
except AttributeError:
# masssively hack-y workaround for boosts
if self.get_activitypub_field() != 'attributedTo':
raise
value = getattr(data, 'actor')
formatted = self.field_from_activity(value) formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING: if formatted is None or formatted is MISSING:
return return
@ -231,6 +237,8 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
def field_from_activity(self, value): def field_from_activity(self, value):
items = [] items = []
if value is None or value is MISSING:
return []
for remote_id in value: for remote_id in value:
try: try:
validate_remote_id(remote_id) validate_remote_id(remote_id)

View File

@ -218,6 +218,18 @@ class Boost(Status):
activitypub_field='object', activitypub_field='object',
) )
def __init__(self, *args, **kwargs):
''' the user field is "actor" here instead of "attributedTo" '''
super().__init__(*args, **kwargs)
reserve_fields = ['user', 'boosted_status']
self.simple_fields = [f for f in self.simple_fields if \
f.name in reserve_fields]
self.activity_fields = self.simple_fields
self.many_to_many_fields = []
self.image_fields = []
self.deserialize_reverse_fields = []
activity_serializer = activitypub.Boost activity_serializer = activitypub.Boost
# This constraint can't work as it would cross tables. # This constraint can't work as it would cross tables.

View File

@ -0,0 +1,51 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"sensitive": "as:sensitive",
"toot": "http://joinmastodon.org/ns#",
"votersCount": "toot:votersCount"
}
],
"id": "https://example.com/users/rat/statuses/1234567",
"type": "Note",
"summary": null,
"inReplyTo": null,
"published": "2020-12-13T05:09:29Z",
"url": "https://example.com/@rat/1234567",
"attributedTo": "https://example.com/users/rat",
"to": [
"https://example.com/user/mouse"
],
"cc": [],
"sensitive": false,
"atomUri": "https://example.com/users/rat/statuses/1234567",
"inReplyToAtomUri": null,
"conversation": "tag:example.com,2020-12-13:objectId=7309346:objectType=Conversation",
"content": "test content in note",
"contentMap": {
"en": "<p><span class=\"h-card\"><a href=\"https://5ebd724a6abd.ngrok.io/user/mouse\" class=\"u-url mention\">@<span>mouse</span></a></span> hi</p>"
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "https://example.com/user/mouse",
"name": "@mouse@example.com"
}
],
"replies": {
"id": "https://example.com/users/rat/statuses/105371151200548049/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://example.com/users/rat/statuses/105371151200548049/replies?only_other_accounts=true&page=true",
"partOf": "https://example.com/users/rat/statuses/105371151200548049/replies",
"items": []
}
}
}

View File

@ -1 +0,0 @@
from . import *

View File

@ -1,51 +0,0 @@
import json
import pathlib
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, incoming
class Favorite(TestCase):
def setUp(self):
with patch('bookwyrm.models.user.set_remote_server.delay'):
with patch('bookwyrm.models.user.get_remote_reviews.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.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True,
remote_id='http://local.com/user/mouse')
self.status = models.Status.objects.create(
user=self.local_user,
content='Test status',
remote_id='http://local.com/status/1',
)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json'
)
self.user_data = json.loads(datafile.read_bytes())
def test_handle_favorite(self):
activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://example.com/fav/1',
'actor': 'https://example.com/users/rat',
'published': 'Mon, 25 May 2020 19:31:20 GMT',
'object': 'http://local.com/status/1',
}
incoming.handle_favorite(activity)
fav = models.Favorite.objects.get(remote_id='http://example.com/fav/1')
self.assertEqual(fav.status, self.status)
self.assertEqual(fav.remote_id, 'http://example.com/fav/1')
self.assertEqual(fav.user, self.remote_user)

View File

@ -1,77 +0,0 @@
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, incoming
class IncomingFollow(TestCase):
def setUp(self):
with patch('bookwyrm.models.user.set_remote_server.delay'):
with patch('bookwyrm.models.user.get_remote_reviews.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.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True)
self.local_user.remote_id = 'http://local.com/user/mouse'
self.local_user.save()
def test_handle_follow(self):
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "http://local.com/user/mouse"
}
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
incoming.handle_follow(activity)
# notification created
notification = models.Notification.objects.get()
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.notification_type, 'FOLLOW')
# the request should have been deleted
requests = models.UserFollowRequest.objects.all()
self.assertEqual(list(requests), [])
# the follow relationship should exist
follow = models.UserFollows.objects.get(user_object=self.local_user)
self.assertEqual(follow.user_subject, self.remote_user)
def test_handle_follow_manually_approved(self):
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "http://local.com/user/mouse"
}
self.local_user.manually_approves_followers = True
self.local_user.save()
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
incoming.handle_follow(activity)
# notification created
notification = models.Notification.objects.get()
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.notification_type, 'FOLLOW_REQUEST')
# the request should exist
request = models.UserFollowRequest.objects.get()
self.assertEqual(request.user_subject, self.remote_user)
self.assertEqual(request.user_object, self.local_user)
# the follow relationship should not exist
follow = models.UserFollows.objects.all()
self.assertEqual(list(follow), [])

View File

@ -1,52 +0,0 @@
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, incoming
class IncomingFollowAccept(TestCase):
def setUp(self):
with patch('bookwyrm.models.user.set_remote_server.delay'):
with patch('bookwyrm.models.user.get_remote_reviews.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.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', local=True)
self.local_user.remote_id = 'http://local.com/user/mouse'
self.local_user.save()
def test_handle_follow_accept(self):
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123#accepts",
"type": "Accept",
"actor": "https://example.com/users/rat",
"object": {
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "http://local.com/user/mouse",
"object": "https://example.com/users/rat"
}
}
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
incoming.handle_follow_accept(activity)
# request should be deleted
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
# relationship should be created
follows = self.remote_user.followers
self.assertEqual(follows.count(), 1)
self.assertEqual(follows.first(), self.local_user)

View File

@ -8,6 +8,7 @@ from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10
class Book(TestCase): class Book(TestCase):
''' not too much going on in the books model but here we are ''' ''' not too much going on in the books model but here we are '''
def setUp(self): def setUp(self):
''' we'll need some books '''
self.work = models.Work.objects.create( self.work = models.Work.objects.create(
title='Example Work', title='Example Work',
remote_id='https://example.com/book/1' remote_id='https://example.com/book/1'
@ -22,6 +23,7 @@ class Book(TestCase):
) )
def test_remote_id(self): def test_remote_id(self):
''' fanciness with remote/origin ids '''
remote_id = 'https://%s/book/%d' % (settings.DOMAIN, self.work.id) remote_id = 'https://%s/book/%d' % (settings.DOMAIN, self.work.id)
self.assertEqual(self.work.get_remote_id(), remote_id) self.assertEqual(self.work.get_remote_id(), remote_id)
self.assertEqual(self.work.remote_id, remote_id) self.assertEqual(self.work.remote_id, remote_id)
@ -54,17 +56,3 @@ class Book(TestCase):
isbn_13 = '978-1788-16167-1' isbn_13 = '978-1788-16167-1'
isbn_10 = isbn_13_to_10(isbn_13) isbn_10 = isbn_13_to_10(isbn_13)
self.assertEqual(isbn_10, '178816167X') self.assertEqual(isbn_10, '178816167X')
class Shelf(TestCase):
def setUp(self):
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True)
models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf', user=user)
def test_remote_id(self):
''' editions and works use the same absolute id syntax '''
shelf = models.Shelf.objects.get(identifier='test-shelf')
expected_id = 'https://%s/user/mouse/shelf/test-shelf' % settings.DOMAIN
self.assertEqual(shelf.get_remote_id(), expected_id)

View File

@ -0,0 +1,30 @@
''' testing models '''
from django.test import TestCase
from bookwyrm import models, settings
class Shelf(TestCase):
''' some activitypub oddness ahead '''
def setUp(self):
''' look, a shelf '''
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword', local=True)
self.shelf = models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf', user=self.user)
def test_remote_id(self):
''' shelves use custom remote ids '''
expected_id = 'https://%s/user/mouse/shelf/test-shelf' % settings.DOMAIN
self.assertEqual(self.shelf.get_remote_id(), expected_id)
def test_to_activity(self):
''' jsonify it '''
activity_json = self.shelf.to_activity()
self.assertIsInstance(activity_json, dict)
self.assertEqual(activity_json['id'], self.shelf.remote_id)
self.assertEqual(activity_json['totalItems'], 0)
self.assertEqual(activity_json['type'], 'OrderedCollection')
self.assertEqual(activity_json['name'], 'Test Shelf')
self.assertEqual(activity_json['owner'], self.user.remote_id)

View File

@ -0,0 +1,481 @@
''' test incoming activities '''
from datetime import datetime
import json
import pathlib
from unittest.mock import patch
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed, \
HttpResponseNotFound
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, incoming
#pylint: disable=too-many-public-methods
class Incoming(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.factory = RequestFactory()
def test_inbox_invalid_get(self):
''' shouldn't try to handle if the user is not found '''
request = self.factory.get('https://www.example.com/')
self.assertIsInstance(
incoming.inbox(request, 'anything'), HttpResponseNotAllowed)
self.assertIsInstance(
incoming.shared_inbox(request), HttpResponseNotAllowed)
def test_inbox_invalid_user(self):
''' shouldn't try to handle if the user is not found '''
request = self.factory.post('https://www.example.com/')
self.assertIsInstance(
incoming.inbox(request, 'fish@tomato.com'), HttpResponseNotFound)
def test_inbox_invalid_no_object(self):
''' json is missing "object" field '''
request = self.factory.post(
self.local_user.shared_inbox, data={})
self.assertIsInstance(
incoming.shared_inbox(request), HttpResponseBadRequest)
def test_inbox_invalid_bad_signature(self):
''' bad request for invalid signature '''
request = self.factory.post(
self.local_user.shared_inbox,
'{"type": "Test", "object": "exists"}',
content_type='application/json')
with patch('bookwyrm.incoming.has_valid_signature') as mock_has_valid:
mock_has_valid.return_value = False
self.assertEqual(
incoming.shared_inbox(request).status_code, 401)
def test_inbox_invalid_bad_signature_delete(self):
''' invalid signature for Delete is okay though '''
request = self.factory.post(
self.local_user.shared_inbox,
'{"type": "Delete", "object": "exists"}',
content_type='application/json')
with patch('bookwyrm.incoming.has_valid_signature') as mock_has_valid:
mock_has_valid.return_value = False
self.assertEqual(
incoming.shared_inbox(request).status_code, 200)
def test_inbox_unknown_type(self):
''' never heard of that activity type, don't have a handler for it '''
request = self.factory.post(
self.local_user.shared_inbox,
'{"type": "Fish", "object": "exists"}',
content_type='application/json')
with patch('bookwyrm.incoming.has_valid_signature') as mock_has_valid:
mock_has_valid.return_value = True
self.assertIsInstance(
incoming.shared_inbox(request), HttpResponseNotFound)
def test_inbox_success(self):
''' a known type, for which we start a task '''
request = self.factory.post(
self.local_user.shared_inbox,
'{"type": "Accept", "object": "exists"}',
content_type='application/json')
with patch('bookwyrm.incoming.has_valid_signature') as mock_has_valid:
mock_has_valid.return_value = True
with patch('bookwyrm.incoming.handle_follow_accept.delay'):
self.assertEqual(
incoming.shared_inbox(request).status_code, 200)
def test_handle_follow(self):
''' remote user wants to follow local user '''
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse"
}
with patch('bookwyrm.broadcast.broadcast_task.delay'):
incoming.handle_follow(activity)
# notification created
notification = models.Notification.objects.get()
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.notification_type, 'FOLLOW')
# the request should have been deleted
requests = models.UserFollowRequest.objects.all()
self.assertEqual(list(requests), [])
# the follow relationship should exist
follow = models.UserFollows.objects.get(user_object=self.local_user)
self.assertEqual(follow.user_subject, self.remote_user)
def test_handle_follow_manually_approved(self):
''' needs approval before following '''
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse"
}
self.local_user.manually_approves_followers = True
self.local_user.save()
with patch('bookwyrm.broadcast.broadcast_task.delay'):
incoming.handle_follow(activity)
# notification created
notification = models.Notification.objects.get()
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.notification_type, 'FOLLOW_REQUEST')
# the request should exist
request = models.UserFollowRequest.objects.get()
self.assertEqual(request.user_subject, self.remote_user)
self.assertEqual(request.user_object, self.local_user)
# the follow relationship should not exist
follow = models.UserFollows.objects.all()
self.assertEqual(list(follow), [])
def test_handle_unfollow(self):
''' remove a relationship '''
activity = {
"type": "Undo",
"@context": "https://www.w3.org/ns/activitystreams",
"object": {
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse"
}
}
models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user)
self.assertEqual(self.remote_user, self.local_user.followers.first())
incoming.handle_unfollow(activity)
self.assertIsNone(self.local_user.followers.first())
def test_handle_follow_accept(self):
''' a remote user approved a follow request from local '''
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123#accepts",
"type": "Accept",
"actor": "https://example.com/users/rat",
"object": {
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "https://example.com/user/mouse",
"object": "https://example.com/users/rat"
}
}
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
incoming.handle_follow_accept(activity)
# request should be deleted
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
# relationship should be created
follows = self.remote_user.followers
self.assertEqual(follows.count(), 1)
self.assertEqual(follows.first(), self.local_user)
def test_handle_follow_reject(self):
''' turn down a follow request '''
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123#accepts",
"type": "Reject",
"actor": "https://example.com/users/rat",
"object": {
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "https://example.com/user/mouse",
"object": "https://example.com/users/rat"
}
}
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
incoming.handle_follow_reject(activity)
# request should be deleted
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
# relationship should be created
follows = self.remote_user.followers
self.assertEqual(follows.count(), 0)
def test_handle_create(self):
''' the "it justs works" mode '''
self.assertEqual(models.Status.objects.count(), 1)
datafile = pathlib.Path(__file__).parent.joinpath(
'data/ap_quotation.json')
status_data = json.loads(datafile.read_bytes())
models.Edition.objects.create(
title='Test Book', remote_id='https://example.com/book/1')
activity = {'object': status_data, 'type': 'Create'}
incoming.handle_create(activity)
status = models.Quotation.objects.get()
self.assertEqual(
status.remote_id, 'https://example.com/user/mouse/quotation/13')
self.assertEqual(status.quote, 'quote body')
self.assertEqual(status.content, 'commentary')
self.assertEqual(status.user, self.local_user)
self.assertEqual(models.Status.objects.count(), 2)
# while we're here, lets ensure we avoid dupes
incoming.handle_create(activity)
self.assertEqual(models.Status.objects.count(), 2)
def test_handle_create_remote_note_with_mention(self):
''' should only create it under the right circumstances '''
self.assertEqual(models.Status.objects.count(), 1)
self.assertFalse(
models.Notification.objects.filter(user=self.local_user).exists())
datafile = pathlib.Path(__file__).parent.joinpath(
'data/ap_note.json')
status_data = json.loads(datafile.read_bytes())
activity = {'object': status_data, 'type': 'Create'}
incoming.handle_create(activity)
status = models.Status.objects.last()
self.assertEqual(status.content, 'test content in note')
self.assertEqual(status.mention_users.first(), self.local_user)
self.assertTrue(
models.Notification.objects.filter(user=self.local_user).exists())
self.assertEqual(
models.Notification.objects.get().notification_type, 'MENTION')
def test_handle_create_remote_note_with_reply(self):
''' should only create it under the right circumstances '''
self.assertEqual(models.Status.objects.count(), 1)
self.assertFalse(
models.Notification.objects.filter(user=self.local_user))
datafile = pathlib.Path(__file__).parent.joinpath(
'data/ap_note.json')
status_data = json.loads(datafile.read_bytes())
del status_data['tag']
status_data['inReplyTo'] = self.status.remote_id
activity = {'object': status_data, 'type': 'Create'}
incoming.handle_create(activity)
status = models.Status.objects.last()
self.assertEqual(status.content, 'test content in note')
self.assertEqual(status.reply_parent, self.status)
self.assertTrue(
models.Notification.objects.filter(user=self.local_user))
self.assertEqual(
models.Notification.objects.get().notification_type, 'REPLY')
def test_handle_delete_status(self):
''' remove a status '''
self.assertFalse(self.status.deleted)
activity = {
'type': 'Delete',
'id': '%s/activity' % self.status.remote_id,
'actor': self.local_user.remote_id,
'object': {'id': self.status.remote_id},
}
incoming.handle_delete_status(activity)
# deletion doens't remove the status, it turns it into a tombstone
status = models.Status.objects.get()
self.assertTrue(status.deleted)
self.assertIsInstance(status.deleted_date, datetime)
def test_handle_delete_status_notifications(self):
''' remove a status with related notifications '''
models.Notification.objects.create(
related_status=self.status,
user=self.local_user,
notification_type='MENTION'
)
# this one is innocent, don't delete it
notif = models.Notification.objects.create(
user=self.local_user,
notification_type='MENTION'
)
self.assertFalse(self.status.deleted)
self.assertEqual(models.Notification.objects.count(), 2)
activity = {
'type': 'Delete',
'id': '%s/activity' % self.status.remote_id,
'actor': self.local_user.remote_id,
'object': {'id': self.status.remote_id},
}
incoming.handle_delete_status(activity)
# deletion doens't remove the status, it turns it into a tombstone
status = models.Status.objects.get()
self.assertTrue(status.deleted)
self.assertIsInstance(status.deleted_date, datetime)
# notifications should be truly deleted
self.assertEqual(models.Notification.objects.count(), 1)
self.assertEqual(models.Notification.objects.get(), notif)
def test_handle_favorite(self):
''' fav a status '''
activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://example.com/fav/1',
'actor': 'https://example.com/users/rat',
'published': 'Mon, 25 May 2020 19:31:20 GMT',
'object': 'https://example.com/status/1',
}
incoming.handle_favorite(activity)
fav = models.Favorite.objects.get(remote_id='https://example.com/fav/1')
self.assertEqual(fav.status, self.status)
self.assertEqual(fav.remote_id, 'https://example.com/fav/1')
self.assertEqual(fav.user, self.remote_user)
def test_handle_unfavorite(self):
''' fav a status '''
activity = {
'id': 'https://example.com/fav/1#undo',
'type': 'Undo',
'object': {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://example.com/fav/1',
'actor': 'https://example.com/users/rat',
'published': 'Mon, 25 May 2020 19:31:20 GMT',
'object': 'https://example.com/fav/1',
}
}
models.Favorite.objects.create(
status=self.status,
user=self.remote_user,
remote_id='https://example.com/fav/1')
self.assertEqual(models.Favorite.objects.count(), 1)
incoming.handle_unfavorite(activity)
self.assertEqual(models.Favorite.objects.count(), 0)
def test_handle_boost(self):
''' boost a status '''
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
'type': 'Announce',
'id': '%s/boost' % self.status.remote_id,
'actor': self.remote_user.remote_id,
'object': self.status.to_activity(),
}
incoming.handle_boost(activity)
boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status, self.status)
notification = models.Notification.objects.get()
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_status, self.status)
def test_handle_unboost(self):
''' undo a boost '''
activity = {
'type': 'Undo',
'object': {
'type': 'Announce',
'id': '%s/boost' % self.status.remote_id,
'actor': self.local_user.remote_id,
'object': self.status.to_activity(),
}
}
models.Boost.objects.create(
boosted_status=self.status, user=self.remote_user)
incoming.handle_unboost(activity)
def test_handle_update_user(self):
''' update an existing user '''
datafile = pathlib.Path(__file__).parent.joinpath(
'data/ap_user.json')
userdata = json.loads(datafile.read_bytes())
del userdata['icon']
self.assertEqual(self.local_user.name, '')
incoming.handle_update_user({'object': userdata})
user = models.User.objects.get(id=self.local_user.id)
self.assertEqual(user.name, 'MOUSE?? MOUSE!!')
def test_handle_update_edition(self):
''' update an existing edition '''
datafile = pathlib.Path(__file__).parent.joinpath(
'data/fr_edition.json')
bookdata = json.loads(datafile.read_bytes())
book = models.Edition.objects.create(
title='Test Book', remote_id='https://bookwyrm.social/book/5989')
del bookdata['authors']
self.assertEqual(book.title, 'Test Book')
with patch(
'bookwyrm.activitypub.base_activity.set_related_field.delay'):
incoming.handle_update_edition({'object': bookdata})
book = models.Edition.objects.get(id=book.id)
self.assertEqual(book.title, 'Piranesi')
def test_handle_update_work(self):
''' update an existing edition '''
datafile = pathlib.Path(__file__).parent.joinpath(
'data/fr_work.json')
bookdata = json.loads(datafile.read_bytes())
book = models.Work.objects.create(
title='Test Book', remote_id='https://bookwyrm.social/book/5988')
del bookdata['authors']
self.assertEqual(book.title, 'Test Book')
with patch(
'bookwyrm.activitypub.base_activity.set_related_field.delay'):
incoming.handle_update_work({'object': bookdata})
book = models.Work.objects.get(id=book.id)
self.assertEqual(book.title, 'Piranesi')