Merge pull request #7 from DeeUnderscore/fix-email-tls

Fix email tls
This commit is contained in:
tofuwabohu 2021-04-09 13:25:14 +02:00 committed by GitHub
commit 4380cc98ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1490 additions and 1027 deletions

View file

@ -8,6 +8,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
- uses: psf/black@20.8b1 - uses: psf/black@stable
with: with:
args: ". --check -l 80 -S" args: ". --check -l 80 -S"

View file

@ -23,6 +23,7 @@ class Person(ActivityObject):
inbox: str inbox: str
publicKey: PublicKey publicKey: PublicKey
followers: str = None followers: str = None
following: str = None
outbox: str = None outbox: str = None
endpoints: Dict = None endpoints: Dict = None
name: str = None name: str = None

View file

@ -179,7 +179,11 @@ class AbstractConnector(AbstractMinimalConnector):
data = get_data(remote_id) data = get_data(remote_id)
mapped_data = dict_from_mappings(data, self.author_mappings) mapped_data = dict_from_mappings(data, self.author_mappings)
activity = activitypub.Author(**mapped_data) try:
activity = activitypub.Author(**mapped_data)
except activitypub.ActivitySerializerError:
return None
# this will dedupe # this will dedupe
return activity.to_model(model=models.Author) return activity.to_model(model=models.Author)

View file

@ -1,5 +1,6 @@
""" interface with whatever connectors the app has """ """ interface with whatever connectors the app has """
import importlib import importlib
import logging
import re import re
from urllib.parse import urlparse from urllib.parse import urlparse
@ -11,6 +12,8 @@ from requests import HTTPError
from bookwyrm import models from bookwyrm import models
from bookwyrm.tasks import app from bookwyrm.tasks import app
logger = logging.getLogger(__name__)
class ConnectorException(HTTPError): class ConnectorException(HTTPError):
""" when the connector can't do what was asked """ """ when the connector can't do what was asked """
@ -37,14 +40,17 @@ def search(query, min_confidence=0.1):
else: else:
try: try:
result_set = connector.isbn_search(isbn) result_set = connector.isbn_search(isbn)
except (HTTPError, ConnectorException): except Exception as e: # pylint: disable=broad-except
pass logger.exception(e)
continue
# if no isbn search or results, we fallback to generic search # if no isbn search or results, we fallback to generic search
if result_set in (None, []): if result_set in (None, []):
try: try:
result_set = connector.search(query, min_confidence=min_confidence) result_set = connector.search(query, min_confidence=min_confidence)
except (HTTPError, ConnectorException): except Exception as e: # pylint: disable=broad-except
# we don't want *any* error to crash the whole search page
logger.exception(e)
continue continue
# if the search results look the same, ignore them # if the search results look the same, ignore them

View file

@ -93,7 +93,10 @@ class Connector(AbstractConnector):
# this id is "/authors/OL1234567A" # this id is "/authors/OL1234567A"
author_id = author_blob["key"] author_id = author_blob["key"]
url = "%s%s" % (self.base_url, author_id) url = "%s%s" % (self.base_url, author_id)
yield self.get_or_create_author(url) author = self.get_or_create_author(url)
if not author:
continue
yield author
def get_cover_url(self, cover_blob, size="L"): def get_cover_url(self, cover_blob, size="L"):
""" ask openlibrary for the cover """ """ ask openlibrary for the cover """

View file

@ -0,0 +1,33 @@
# Generated by Django 3.1.6 on 2021-04-07 15:45
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0061_auto_20210402_1435"),
]
operations = [
migrations.AlterField(
model_name="book",
name="series",
field=bookwyrm.models.fields.TextField(
blank=True, max_length=255, null=True
),
),
migrations.AlterField(
model_name="book",
name="subtitle",
field=bookwyrm.models.fields.TextField(
blank=True, max_length=255, null=True
),
),
migrations.AlterField(
model_name="book",
name="title",
field=bookwyrm.models.fields.TextField(max_length=255),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 3.1.6 on 2021-04-08 15:56
import bookwyrm.models.fields
import django.contrib.postgres.fields.citext
import django.contrib.postgres.operations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0062_auto_20210407_1545"),
]
operations = [
django.contrib.postgres.operations.CITextExtension(),
migrations.AlterField(
model_name="user",
name="localname",
field=django.contrib.postgres.fields.citext.CICharField(
max_length=255,
null=True,
unique=True,
validators=[bookwyrm.models.fields.validate_localname],
),
),
]

View file

@ -1,5 +1,6 @@
""" activitypub model functionality """ """ activitypub model functionality """
from base64 import b64encode from base64 import b64encode
from collections import namedtuple
from functools import reduce from functools import reduce
import json import json
import operator import operator
@ -25,6 +26,15 @@ from bookwyrm.models.fields import ImageField, ManyToManyField
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# I tried to separate these classes into mutliple files but I kept getting # I tried to separate these classes into mutliple files but I kept getting
# circular import errors so I gave up. I'm sure it could be done though! # circular import errors so I gave up. I'm sure it could be done though!
PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
def set_activity_from_property_field(activity, obj, field):
""" assign a model property value to the activity json """
activity[field[1]] = getattr(obj, field[0])
class ActivitypubMixin: class ActivitypubMixin:
""" add this mixin for models that are AP serializable """ """ add this mixin for models that are AP serializable """
@ -52,6 +62,11 @@ class ActivitypubMixin:
self.activity_fields = ( self.activity_fields = (
self.image_fields + self.many_to_many_fields + self.simple_fields self.image_fields + self.many_to_many_fields + self.simple_fields
) )
if hasattr(self, "property_fields"):
self.activity_fields += [
PropertyField(lambda a, o: set_activity_from_property_field(a, o, f))
for f in self.property_fields
]
# these are separate to avoid infinite recursion issues # these are separate to avoid infinite recursion issues
self.deserialize_reverse_fields = ( self.deserialize_reverse_fields = (
@ -370,7 +385,7 @@ class CollectionItemMixin(ActivitypubMixin):
object_field = getattr(self, self.object_field) object_field = getattr(self, self.object_field)
collection_field = getattr(self, self.collection_field) collection_field = getattr(self, self.collection_field)
return activitypub.Add( return activitypub.Add(
id=self.remote_id, id=self.get_remote_id(),
actor=self.user.remote_id, actor=self.user.remote_id,
object=object_field, object=object_field,
target=collection_field.remote_id, target=collection_field.remote_id,
@ -381,7 +396,7 @@ class CollectionItemMixin(ActivitypubMixin):
object_field = getattr(self, self.object_field) object_field = getattr(self, self.object_field)
collection_field = getattr(self, self.collection_field) collection_field = getattr(self, self.collection_field)
return activitypub.Remove( return activitypub.Remove(
id=self.remote_id, id=self.get_remote_id(),
actor=self.user.remote_id, actor=self.user.remote_id,
object=object_field, object=object_field,
target=collection_field.remote_id, target=collection_field.remote_id,
@ -430,7 +445,7 @@ def generate_activity(obj):
) in obj.serialize_reverse_fields: ) in obj.serialize_reverse_fields:
related_field = getattr(obj, model_field_name) related_field = getattr(obj, model_field_name)
activity[activity_field_name] = unfurl_related_field( activity[activity_field_name] = unfurl_related_field(
related_field, sort_field related_field, sort_field=sort_field
) )
if not activity.get("id"): if not activity.get("id"):
@ -440,7 +455,7 @@ def generate_activity(obj):
def unfurl_related_field(related_field, sort_field=None): def unfurl_related_field(related_field, sort_field=None):
""" load reverse lookups (like public key owner or Status attachment """ """ load reverse lookups (like public key owner or Status attachment """
if hasattr(related_field, "all"): if sort_field and hasattr(related_field, "all"):
return [ return [
unfurl_related_field(i) for i in related_field.order_by(sort_field).all() unfurl_related_field(i) for i in related_field.order_by(sort_field).all()
] ]

View file

@ -53,14 +53,14 @@ class Book(BookDataModel):
connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True) connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
# book/work metadata # book/work metadata
title = fields.CharField(max_length=255) title = fields.TextField(max_length=255)
sort_title = fields.CharField(max_length=255, blank=True, null=True) sort_title = fields.CharField(max_length=255, blank=True, null=True)
subtitle = fields.CharField(max_length=255, blank=True, null=True) subtitle = fields.TextField(max_length=255, blank=True, null=True)
description = fields.HtmlField(blank=True, null=True) description = fields.HtmlField(blank=True, null=True)
languages = fields.ArrayField( languages = fields.ArrayField(
models.CharField(max_length=255), blank=True, default=list models.CharField(max_length=255), blank=True, default=list
) )
series = fields.CharField(max_length=255, blank=True, null=True) series = fields.TextField(max_length=255, blank=True, null=True)
series_number = fields.CharField(max_length=255, blank=True, null=True) series_number = fields.CharField(max_length=255, blank=True, null=True)
subjects = fields.ArrayField( subjects = fields.ArrayField(
models.CharField(max_length=255), blank=True, null=True, default=list models.CharField(max_length=255), blank=True, null=True, default=list

View file

@ -4,6 +4,7 @@ from urllib.parse import urlparse
from django.apps import apps from django.apps import apps
from django.contrib.auth.models import AbstractUser, Group from django.contrib.auth.models import AbstractUser, Group
from django.contrib.postgres.fields import CICharField
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
@ -54,7 +55,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
summary = fields.HtmlField(null=True, blank=True) summary = fields.HtmlField(null=True, blank=True)
local = models.BooleanField(default=False) local = models.BooleanField(default=False)
bookwyrm_user = fields.BooleanField(default=True) bookwyrm_user = fields.BooleanField(default=True)
localname = models.CharField( localname = CICharField(
max_length=255, max_length=255,
null=True, null=True,
unique=True, unique=True,
@ -112,6 +113,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
) )
name_field = "username" name_field = "username"
property_fields = [("following_link", "following")]
@property
def following_link(self):
""" just how to find out the following info """
return "{:s}/following".format(self.remote_id)
@property @property
def alt_text(self): def alt_text(self):

View file

@ -24,7 +24,7 @@ EMAIL_HOST = env("EMAIL_HOST")
EMAIL_PORT = env("EMAIL_PORT", 587) EMAIL_PORT = env("EMAIL_PORT", 587)
EMAIL_HOST_USER = env("EMAIL_HOST_USER") EMAIL_HOST_USER = env("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = env("EMAIL_USE_TLS", True) EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True)
DEFAULT_FROM_EMAIL = "admin@{:s}".format(env("DOMAIN")) DEFAULT_FROM_EMAIL = "admin@{:s}".format(env("DOMAIN"))
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)

View file

@ -88,12 +88,18 @@
<div class="column is-half"> <div class="column is-half">
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <h2 class="title is-4">{% trans "Metadata" %}</h2>
<p class="mb-2"><label class="label" for="id_title">{% trans "Title:" %}</label> {{ form.title }} </p> <p class="mb-2">
<label class="label" for="id_title">{% trans "Title:" %}</label>
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
</p>
{% for error in form.title.errors %} {% for error in form.title.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p class="mb-2"><label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label> {{ form.subtitle }} </p> <p class="mb-2">
<label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label>
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle">
</p>
{% for error in form.subtitle.errors %} {% for error in form.subtitle.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
@ -124,7 +130,7 @@
<p class="mb-2"> <p class="mb-2">
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label> <label class="label" for="id_first_published_date">{% trans "First published date:" %}</label>
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if book.first_published_date %} value="{{ book.first_published_date|date:'Y-m-d' }}"{% endif %}> <input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %}>
</p> </p>
{% for error in form.first_published_date.errors %} {% for error in form.first_published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -132,7 +138,7 @@
<p class="mb-2"> <p class="mb-2">
<label class="label" for="id_published_date">{% trans "Published date:" %}</label> <label class="label" for="id_published_date">{% trans "Published date:" %}</label>
<input type="date" name="published_date" class="input" id="id_published_date"{% if book.published_date %} value="{{ book.published_date|date:'Y-m-d' }}"{% endif %}> <input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %}>
</p> </p>
{% for error in form.published_date.errors %} {% for error in form.published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>

View file

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

View file

@ -0,0 +1,108 @@
""" tests incoming activities"""
import json
from unittest.mock import patch
from django.http import HttpResponseNotAllowed, HttpResponseNotFound
from django.test import TestCase, Client
from bookwyrm import models
# pylint: disable=too-many-public-methods
class Inbox(TestCase):
""" readthrough tests """
def setUp(self):
""" basic user and book data """
self.client = Client()
local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
local_user.remote_id = "https://example.com/user/mouse"
local_user.save(broadcast=False)
self.create_json = {
"id": "hi",
"type": "Create",
"actor": "hi",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {},
}
models.SiteSettings.objects.create()
def test_inbox_invalid_get(self):
""" shouldn't try to handle if the user is not found """
result = self.client.get("/inbox", content_type="application/json")
self.assertIsInstance(result, HttpResponseNotAllowed)
def test_inbox_invalid_user(self):
""" shouldn't try to handle if the user is not found """
result = self.client.post(
"/user/bleh/inbox",
'{"type": "Test", "object": "exists"}',
content_type="application/json",
)
self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_invalid_bad_signature(self):
""" bad request for invalid signature """
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = False
result = self.client.post(
"/user/mouse/inbox",
'{"type": "Announce", "object": "exists"}',
content_type="application/json",
)
self.assertEqual(result.status_code, 401)
def test_inbox_invalid_bad_signature_delete(self):
""" invalid signature for Delete is okay though """
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = False
result = self.client.post(
"/user/mouse/inbox",
'{"type": "Delete", "object": "exists"}',
content_type="application/json",
)
self.assertEqual(result.status_code, 200)
def test_inbox_unknown_type(self):
""" never heard of that activity type, don't have a handler for it """
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
result = self.client.post(
"/inbox",
'{"type": "Fish", "object": "exists"}',
content_type="application/json",
)
mock_valid.return_value = True
self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_success(self):
""" a known type, for which we start a task """
activity = self.create_json
activity["object"] = {
"id": "https://example.com/list/22",
"type": "BookList",
"totalItems": 1,
"first": "https://example.com/list/22?page=1",
"last": "https://example.com/list/22?page=1",
"name": "Test List",
"owner": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"summary": "summary text",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams",
}
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = True
with patch("bookwyrm.views.inbox.activity_task.delay"):
result = self.client.post(
"/inbox", json.dumps(activity), content_type="application/json"
)
self.assertEqual(result.status_code, 200)

View file

@ -0,0 +1,156 @@
""" tests incoming activities"""
from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" inbox tests """
def setUp(self):
""" basic user and book data """
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
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",
)
models.SiteSettings.objects.create()
def test_handle_add_book_to_shelf(self):
""" shelving a book """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf")
shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read"
shelf.save()
activity = {
"id": "https://bookwyrm.social/shelfbook/6189#add",
"type": "Add",
"actor": "https://example.com/users/rat",
"object": {
"type": "Edition",
"title": "Test Title",
"work": work.remote_id,
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://bookwyrm.social/user/mouse/shelf/to-read",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
self.assertEqual(shelf.books.first(), book)
@responses.activate
def test_handle_add_book_to_list(self):
""" listing a book """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
responses.add(
responses.GET,
"https://bookwyrm.social/user/mouse/list/to-read",
json={
"id": "https://example.com/list/22",
"type": "BookList",
"totalItems": 1,
"first": "https://example.com/list/22?page=1",
"last": "https://example.com/list/22?page=1",
"name": "Test List",
"owner": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"summary": "summary text",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams",
},
)
activity = {
"id": "https://bookwyrm.social/listbook/6189#add",
"type": "Add",
"actor": "https://example.com/users/rat",
"object": {
"type": "Edition",
"title": "Test Title",
"work": work.remote_id,
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://bookwyrm.social/user/mouse/list/to-read",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
booklist = models.List.objects.get()
self.assertEqual(booklist.name, "Test List")
self.assertEqual(booklist.books.first(), book)
@responses.activate
def test_handle_tag_book(self):
""" listing a book """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
responses.add(
responses.GET,
"https://www.example.com/tag/cool-tag",
json={
"id": "https://1b1a78582461.ngrok.io/tag/tag",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://1b1a78582461.ngrok.io/tag/tag?page=1",
"last": "https://1b1a78582461.ngrok.io/tag/tag?page=1",
"name": "cool tag",
"@context": "https://www.w3.org/ns/activitystreams",
},
)
activity = {
"id": "https://bookwyrm.social/listbook/6189#add",
"type": "Add",
"actor": "https://example.com/users/rat",
"object": {
"type": "Edition",
"title": "Test Title",
"work": work.remote_id,
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://www.example.com/tag/cool-tag",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
tag = models.Tag.objects.get()
self.assertFalse(models.List.objects.exists())
self.assertEqual(tag.name, "cool tag")
self.assertEqual(tag.books.first(), book)

View file

@ -0,0 +1,190 @@
""" tests incoming activities"""
from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" inbox tests """
def setUp(self):
""" basic user and book data """
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
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",
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
self.create_json = {
"id": "hi",
"type": "Create",
"actor": "hi",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {},
}
models.SiteSettings.objects.create()
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
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.remote_id,
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"published": "Mon, 25 May 2020 19:31:20 GMT",
}
with patch("bookwyrm.models.status.Status.ignore_activity") as discarder:
discarder.return_value = False
views.inbox.activity_task(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)
@responses.activate
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_boost_remote_status(self, redis_mock):
""" boost a status """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
"type": "Announce",
"id": "%s/boost" % self.status.remote_id,
"actor": self.remote_user.remote_id,
"object": "https://remote.com/status/1",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"published": "Mon, 25 May 2020 19:31:20 GMT",
}
responses.add(
responses.GET,
"https://remote.com/status/1",
json={
"id": "https://remote.com/status/1",
"type": "Comment",
"published": "2021-04-05T18:04:59.735190+00:00",
"attributedTo": self.remote_user.remote_id,
"content": "<p>a comment</p>",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://b875df3d118b.ngrok.io/user/mouse/followers"],
"inReplyTo": "",
"inReplyToBook": book.remote_id,
"summary": "",
"tag": [],
"sensitive": False,
"@context": "https://www.w3.org/ns/activitystreams",
},
)
with patch("bookwyrm.models.status.Status.ignore_activity") as discarder:
discarder.return_value = False
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status.remote_id, "https://remote.com/status/1")
self.assertEqual(boost.boosted_status.comment.status_type, "Comment")
self.assertEqual(boost.boosted_status.comment.book, book)
@responses.activate
def test_handle_discarded_boost(self):
""" test a boost of a mastodon status that will be discarded """
status = models.Status(
content="hi",
user=self.remote_user,
)
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status.save(broadcast=False)
activity = {
"type": "Announce",
"id": "http://www.faraway.com/boost/12",
"actor": self.remote_user.remote_id,
"object": status.remote_id,
}
responses.add(
responses.GET, status.remote_id, json=status.to_activity(), status=200
)
views.inbox.activity_task(activity)
self.assertEqual(models.Boost.objects.count(), 0)
def test_handle_unboost(self):
""" undo a boost """
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
boost = models.Boost.objects.create(
boosted_status=self.status, user=self.remote_user
)
activity = {
"type": "Undo",
"actor": "hi",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {
"type": "Announce",
"id": boost.remote_id,
"actor": self.remote_user.remote_id,
"object": self.status.remote_id,
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"published": "Mon, 25 May 2020 19:31:20 GMT",
},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
self.assertFalse(models.Boost.objects.exists())
def test_handle_unboost_unknown_boost(self):
""" undo a boost """
activity = {
"type": "Undo",
"actor": "hi",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {
"type": "Announce",
"id": "http://fake.com/unknown/boost",
"actor": self.remote_user.remote_id,
"object": self.status.remote_id,
},
}
views.inbox.activity_task(activity)

View file

@ -0,0 +1,98 @@
""" tests incoming activities"""
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxBlock(TestCase):
""" inbox tests """
def setUp(self):
""" basic user and book data """
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
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",
)
models.SiteSettings.objects.create()
def test_handle_blocks(self):
""" create a "block" database entry from an activity """
self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
self.assertTrue(models.UserFollows.objects.exists())
self.assertTrue(models.UserFollowRequest.objects.exists())
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/9e1f41ac-9ddd-4159",
"type": "Block",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse",
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_user_statuses"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
views.inbox.activity_task(activity)
block = models.UserBlocks.objects.get()
self.assertEqual(block.user_subject, self.remote_user)
self.assertEqual(block.user_object, self.local_user)
self.assertEqual(block.remote_id, "https://example.com/9e1f41ac-9ddd-4159")
self.assertFalse(models.UserFollows.objects.exists())
self.assertFalse(models.UserFollowRequest.objects.exists())
def test_handle_unblock(self):
""" unblock a user """
self.remote_user.blocks.add(self.local_user)
block = models.UserBlocks.objects.get()
block.remote_id = "https://example.com/9e1f41ac-9ddd-4159"
block.save()
self.assertEqual(block.user_subject, self.remote_user)
self.assertEqual(block.user_object, self.local_user)
activity = {
"type": "Undo",
"actor": "hi",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/9e1f41ac-9ddd-4159",
"type": "Block",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse",
},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.add_user_statuses"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
self.assertFalse(models.UserBlocks.objects.exists())

View file

@ -0,0 +1,151 @@
""" tests incoming activities"""
import json
import pathlib
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" readthrough tests """
def setUp(self):
""" basic user and book data """
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
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.create_json = {
"id": "hi",
"type": "Create",
"actor": "hi",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {},
}
models.SiteSettings.objects.create()
def test_handle_create_status(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 = self.create_json
activity["object"] = status_data
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
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
views.inbox.activity_task(activity)
self.assertEqual(models.Status.objects.count(), 2)
def test_handle_create_status_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 = self.create_json
activity["object"] = status_data
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
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_status_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 = self.create_json
activity["object"] = status_data
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
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_create_list(self):
""" a new list """
activity = self.create_json
activity["object"] = {
"id": "https://example.com/list/22",
"type": "BookList",
"totalItems": 1,
"first": "https://example.com/list/22?page=1",
"last": "https://example.com/list/22?page=1",
"name": "Test List",
"owner": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"summary": "summary text",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
book_list = models.List.objects.get()
self.assertEqual(book_list.name, "Test List")
self.assertEqual(book_list.curation, "curated")
self.assertEqual(book_list.description, "summary text")
self.assertEqual(book_list.remote_id, "https://example.com/list/22")

View file

@ -0,0 +1,106 @@
""" tests incoming activities"""
from datetime import datetime
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" inbox tests """
def setUp(self):
""" basic user and book data """
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
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",
)
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.remote_user,
content="Test status",
remote_id="https://example.com/status/1",
)
self.create_json = {
"id": "hi",
"type": "Create",
"actor": "hi",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {},
}
models.SiteSettings.objects.create()
def test_handle_delete_status(self):
""" remove a status """
self.assertFalse(self.status.deleted)
activity = {
"type": "Delete",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"id": "%s/activity" % self.status.remote_id,
"actor": self.remote_user.remote_id,
"object": {"id": self.status.remote_id, "type": "Tombstone"},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
# 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",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"id": "%s/activity" % self.status.remote_id,
"actor": self.remote_user.remote_id,
"object": {"id": self.status.remote_id, "type": "Tombstone"},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
# 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)

View file

@ -0,0 +1,205 @@
""" tests incoming activities"""
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxRelationships(TestCase):
""" inbox tests """
def setUp(self):
""" basic user and book data """
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
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",
)
models.SiteSettings.objects.create()
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",
}
self.assertFalse(models.UserFollowRequest.objects.exists())
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.inbox.activity_task(activity)
self.assertEqual(mock.call_count, 1)
# 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
self.assertFalse(models.UserFollowRequest.objects.exists())
# 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(broadcast=False)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.inbox.activity_task(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_undo_follow_request(self):
""" the requester cancels a follow request """
self.local_user.manually_approves_followers = True
self.local_user.save(broadcast=False)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
request = models.UserFollowRequest.objects.create(
user_subject=self.remote_user, user_object=self.local_user
)
self.assertTrue(self.local_user.follower_requests.exists())
activity = {
"type": "Undo",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"actor": self.remote_user.remote_id,
"@context": "https://www.w3.org/ns/activitystreams",
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": request.remote_id,
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse",
},
}
views.inbox.activity_task(activity)
self.assertFalse(self.local_user.follower_requests.exists())
def test_handle_unfollow(self):
""" remove a relationship """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user
)
activity = {
"type": "Undo",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"actor": self.remote_user.remote_id,
"@context": "https://www.w3.org/ns/activitystreams",
"object": {
"id": rel.remote_id,
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse",
},
}
self.assertEqual(self.remote_user, self.local_user.followers.first())
views.inbox.activity_task(activity)
self.assertIsNone(self.local_user.followers.first())
def test_handle_follow_accept(self):
""" a remote user approved a follow request from local """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
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": rel.remote_id,
"type": "Follow",
"actor": "https://example.com/user/mouse",
"object": "https://example.com/users/rat",
},
}
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
views.inbox.activity_task(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 """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
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": rel.remote_id,
"type": "Follow",
"actor": "https://example.com/user/mouse",
"object": "https://example.com/users/rat",
},
}
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
views.inbox.activity_task(activity)
# request should be deleted
self.assertFalse(models.UserFollowRequest.objects.exists())
self.assertFalse(self.remote_user.followers.exists())

View file

@ -0,0 +1,110 @@
""" tests incoming activities"""
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" inbox tests """
def setUp(self):
""" basic user and book data """
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
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",
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
self.create_json = {
"id": "hi",
"type": "Create",
"actor": "hi",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {},
}
models.SiteSettings.objects.create()
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",
"type": "Like",
"published": "Mon, 25 May 2020 19:31:20 GMT",
"object": self.status.remote_id,
}
views.inbox.activity_task(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_ignore_favorite(self):
""" don't try to save an unknown status """
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/fav/1",
"actor": "https://example.com/users/rat",
"type": "Like",
"published": "Mon, 25 May 2020 19:31:20 GMT",
"object": "https://unknown.status/not-found",
}
views.inbox.activity_task(activity)
self.assertFalse(models.Favorite.objects.exists())
def test_handle_unfavorite(self):
""" fav a status """
activity = {
"id": "https://example.com/fav/1#undo",
"type": "Undo",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"actor": self.remote_user.remote_id,
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/fav/1",
"actor": "https://example.com/users/rat",
"type": "Like",
"published": "Mon, 25 May 2020 19:31:20 GMT",
"object": self.status.remote_id,
},
}
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)
views.inbox.activity_task(activity)
self.assertEqual(models.Favorite.objects.count(), 0)

View file

@ -0,0 +1,61 @@
""" tests incoming activities"""
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" inbox tests """
def setUp(self):
""" basic user and book data """
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",
)
models.SiteSettings.objects.create()
def test_handle_unshelve_book(self):
""" remove a book from a shelf """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf")
shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read"
shelf.save()
shelfbook = models.ShelfBook.objects.create(
user=self.remote_user, shelf=shelf, book=book
)
self.assertEqual(shelf.books.first(), book)
self.assertEqual(shelf.books.count(), 1)
activity = {
"id": shelfbook.remote_id,
"type": "Remove",
"actor": "https://example.com/users/rat",
"object": {
"type": "Edition",
"title": "Test Title",
"work": work.remote_id,
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://bookwyrm.social/user/mouse/shelf/to-read",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
self.assertFalse(shelf.books.exists())

View file

@ -0,0 +1,149 @@
""" tests incoming activities"""
import json
import pathlib
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxUpdate(TestCase):
""" inbox tests """
def setUp(self):
""" basic user and book data """
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
self.create_json = {
"id": "hi",
"type": "Create",
"actor": "hi",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {},
}
models.SiteSettings.objects.create()
def test_handle_update_list(self):
""" a new list """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
book_list = models.List.objects.create(
name="hi", remote_id="https://example.com/list/22", user=self.local_user
)
activity = {
"type": "Update",
"to": [],
"cc": [],
"actor": "hi",
"id": "sdkjf",
"object": {
"id": "https://example.com/list/22",
"type": "BookList",
"totalItems": 1,
"first": "https://example.com/list/22?page=1",
"last": "https://example.com/list/22?page=1",
"name": "Test List",
"owner": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"summary": "summary text",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams",
},
}
views.inbox.activity_task(activity)
book_list.refresh_from_db()
self.assertEqual(book_list.name, "Test List")
self.assertEqual(book_list.curation, "curated")
self.assertEqual(book_list.description, "summary text")
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
def test_handle_update_user(self):
""" 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("../../data/ap_user.json")
userdata = json.loads(datafile.read_bytes())
del userdata["icon"]
self.assertIsNone(self.local_user.name)
views.inbox.activity_task(
{
"type": "Update",
"to": [],
"cc": [],
"actor": "hi",
"id": "sdkjf",
"object": userdata,
}
)
user = models.User.objects.get(id=self.local_user.id)
self.assertEqual(user.name, "MOUSE?? MOUSE!!")
self.assertEqual(user.username, "mouse@example.com")
self.assertEqual(user.localname, "mouse")
self.assertTrue(user.discoverable)
def test_handle_update_edition(self):
""" update an existing edition """
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_edition.json")
bookdata = json.loads(datafile.read_bytes())
models.Work.objects.create(
title="Test Work", remote_id="https://bookwyrm.social/book/5988"
)
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"):
views.inbox.activity_task(
{
"type": "Update",
"to": [],
"cc": [],
"actor": "hi",
"id": "sdkjf",
"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/bw_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"):
views.inbox.activity_task(
{
"type": "Update",
"to": [],
"cc": [],
"actor": "hi",
"id": "sdkjf",
"object": bookdata,
}
)
book = models.Work.objects.get(id=book.id)
self.assertEqual(book.title, "Piranesi")

View file

@ -112,6 +112,9 @@ class ViewsHelpers(TestCase):
result = views.helpers.handle_remote_webfinger("mouse@local.com") result = views.helpers.handle_remote_webfinger("mouse@local.com")
self.assertEqual(result, self.local_user) self.assertEqual(result, self.local_user)
result = views.helpers.handle_remote_webfinger("mOuSe@loCal.cOm")
self.assertEqual(result, self.local_user)
@responses.activate @responses.activate
def test_load_user(self, _): def test_load_user(self, _):
""" find a remote user using webfinger """ """ find a remote user using webfinger """

View file

@ -1,988 +0,0 @@
""" tests incoming activities"""
from datetime import datetime
import json
import pathlib
from unittest.mock import patch
from django.http import HttpResponseNotAllowed, HttpResponseNotFound
from django.test import TestCase, Client
import responses
from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class Inbox(TestCase):
""" readthrough tests """
def setUp(self):
""" basic user and book data """
self.client = Client()
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=True,
localname="mouse",
)
self.local_user.remote_id = "https://example.com/user/mouse"
self.local_user.save(broadcast=False)
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",
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
remote_id="https://example.com/status/1",
)
self.create_json = {
"id": "hi",
"type": "Create",
"actor": "hi",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {},
}
models.SiteSettings.objects.create()
def test_inbox_invalid_get(self):
""" shouldn't try to handle if the user is not found """
result = self.client.get("/inbox", content_type="application/json")
self.assertIsInstance(result, HttpResponseNotAllowed)
def test_inbox_invalid_user(self):
""" shouldn't try to handle if the user is not found """
result = self.client.post(
"/user/bleh/inbox",
'{"type": "Test", "object": "exists"}',
content_type="application/json",
)
self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_invalid_bad_signature(self):
""" bad request for invalid signature """
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = False
result = self.client.post(
"/user/mouse/inbox",
'{"type": "Announce", "object": "exists"}',
content_type="application/json",
)
self.assertEqual(result.status_code, 401)
def test_inbox_invalid_bad_signature_delete(self):
""" invalid signature for Delete is okay though """
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = False
result = self.client.post(
"/user/mouse/inbox",
'{"type": "Delete", "object": "exists"}',
content_type="application/json",
)
self.assertEqual(result.status_code, 200)
def test_inbox_unknown_type(self):
""" never heard of that activity type, don't have a handler for it """
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
result = self.client.post(
"/inbox",
'{"type": "Fish", "object": "exists"}',
content_type="application/json",
)
mock_valid.return_value = True
self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_success(self):
""" a known type, for which we start a task """
activity = self.create_json
activity["object"] = {
"id": "https://example.com/list/22",
"type": "BookList",
"totalItems": 1,
"first": "https://example.com/list/22?page=1",
"last": "https://example.com/list/22?page=1",
"name": "Test List",
"owner": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"summary": "summary text",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams",
}
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = True
with patch("bookwyrm.views.inbox.activity_task.delay"):
result = self.client.post(
"/inbox", json.dumps(activity), content_type="application/json"
)
self.assertEqual(result.status_code, 200)
def test_handle_create_status(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 = self.create_json
activity["object"] = status_data
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
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
views.inbox.activity_task(activity)
self.assertEqual(models.Status.objects.count(), 2)
def test_handle_create_status_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 = self.create_json
activity["object"] = status_data
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
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_status_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 = self.create_json
activity["object"] = status_data
with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
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_create_list(self):
""" a new list """
activity = self.create_json
activity["object"] = {
"id": "https://example.com/list/22",
"type": "BookList",
"totalItems": 1,
"first": "https://example.com/list/22?page=1",
"last": "https://example.com/list/22?page=1",
"name": "Test List",
"owner": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"summary": "summary text",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
book_list = models.List.objects.get()
self.assertEqual(book_list.name, "Test List")
self.assertEqual(book_list.curation, "curated")
self.assertEqual(book_list.description, "summary text")
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
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",
}
self.assertFalse(models.UserFollowRequest.objects.exists())
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
views.inbox.activity_task(activity)
self.assertEqual(mock.call_count, 1)
# 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
self.assertFalse(models.UserFollowRequest.objects.exists())
# 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(broadcast=False)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.inbox.activity_task(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_undo_follow_request(self):
""" the requester cancels a follow request """
self.local_user.manually_approves_followers = True
self.local_user.save(broadcast=False)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
request = models.UserFollowRequest.objects.create(
user_subject=self.remote_user, user_object=self.local_user
)
self.assertTrue(self.local_user.follower_requests.exists())
activity = {
"type": "Undo",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"actor": self.remote_user.remote_id,
"@context": "https://www.w3.org/ns/activitystreams",
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": request.remote_id,
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse",
},
}
views.inbox.activity_task(activity)
self.assertFalse(self.local_user.follower_requests.exists())
def test_handle_unfollow(self):
""" remove a relationship """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user
)
activity = {
"type": "Undo",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"actor": self.remote_user.remote_id,
"@context": "https://www.w3.org/ns/activitystreams",
"object": {
"id": rel.remote_id,
"type": "Follow",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse",
},
}
self.assertEqual(self.remote_user, self.local_user.followers.first())
views.inbox.activity_task(activity)
self.assertIsNone(self.local_user.followers.first())
def test_handle_follow_accept(self):
""" a remote user approved a follow request from local """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
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": rel.remote_id,
"type": "Follow",
"actor": "https://example.com/user/mouse",
"object": "https://example.com/users/rat",
},
}
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
views.inbox.activity_task(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 """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
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": rel.remote_id,
"type": "Follow",
"actor": "https://example.com/user/mouse",
"object": "https://example.com/users/rat",
},
}
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
views.inbox.activity_task(activity)
# request should be deleted
self.assertFalse(models.UserFollowRequest.objects.exists())
self.assertFalse(self.remote_user.followers.exists())
def test_handle_update_list(self):
""" a new list """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
book_list = models.List.objects.create(
name="hi", remote_id="https://example.com/list/22", user=self.local_user
)
activity = {
"type": "Update",
"to": [],
"cc": [],
"actor": "hi",
"id": "sdkjf",
"object": {
"id": "https://example.com/list/22",
"type": "BookList",
"totalItems": 1,
"first": "https://example.com/list/22?page=1",
"last": "https://example.com/list/22?page=1",
"name": "Test List",
"owner": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"summary": "summary text",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams",
},
}
views.inbox.activity_task(activity)
book_list.refresh_from_db()
self.assertEqual(book_list.name, "Test List")
self.assertEqual(book_list.curation, "curated")
self.assertEqual(book_list.description, "summary text")
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
def test_handle_delete_status(self):
""" remove a status """
self.status.user = self.remote_user
self.status.save(broadcast=False)
self.assertFalse(self.status.deleted)
activity = {
"type": "Delete",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"id": "%s/activity" % self.status.remote_id,
"actor": self.remote_user.remote_id,
"object": {"id": self.status.remote_id, "type": "Tombstone"},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
# 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 """
self.status.user = self.remote_user
self.status.save(broadcast=False)
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",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"id": "%s/activity" % self.status.remote_id,
"actor": self.remote_user.remote_id,
"object": {"id": self.status.remote_id, "type": "Tombstone"},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
# 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",
"type": "Like",
"published": "Mon, 25 May 2020 19:31:20 GMT",
"object": self.status.remote_id,
}
views.inbox.activity_task(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_ignore_favorite(self):
""" don't try to save an unknown status """
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/fav/1",
"actor": "https://example.com/users/rat",
"type": "Like",
"published": "Mon, 25 May 2020 19:31:20 GMT",
"object": "https://unknown.status/not-found",
}
views.inbox.activity_task(activity)
self.assertFalse(models.Favorite.objects.exists())
def test_handle_unfavorite(self):
""" fav a status """
activity = {
"id": "https://example.com/fav/1#undo",
"type": "Undo",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"actor": self.remote_user.remote_id,
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/fav/1",
"actor": "https://example.com/users/rat",
"type": "Like",
"published": "Mon, 25 May 2020 19:31:20 GMT",
"object": self.status.remote_id,
},
}
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)
views.inbox.activity_task(activity)
self.assertEqual(models.Favorite.objects.count(), 0)
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
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.remote_id,
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"published": "Mon, 25 May 2020 19:31:20 GMT",
}
with patch("bookwyrm.models.status.Status.ignore_activity") as discarder:
discarder.return_value = False
views.inbox.activity_task(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)
@responses.activate
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_boost_remote_status(self, redis_mock):
""" boost a status """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
"type": "Announce",
"id": "%s/boost" % self.status.remote_id,
"actor": self.remote_user.remote_id,
"object": "https://remote.com/status/1",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"published": "Mon, 25 May 2020 19:31:20 GMT",
}
responses.add(
responses.GET,
"https://remote.com/status/1",
json={
"id": "https://remote.com/status/1",
"type": "Comment",
"published": "2021-04-05T18:04:59.735190+00:00",
"attributedTo": self.remote_user.remote_id,
"content": "<p>a comment</p>",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://b875df3d118b.ngrok.io/user/mouse/followers"],
"inReplyTo": "",
"inReplyToBook": book.remote_id,
"summary": "",
"tag": [],
"sensitive": False,
"@context": "https://www.w3.org/ns/activitystreams",
},
)
with patch("bookwyrm.models.status.Status.ignore_activity") as discarder:
discarder.return_value = False
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status.remote_id, "https://remote.com/status/1")
self.assertEqual(boost.boosted_status.comment.status_type, "Comment")
self.assertEqual(boost.boosted_status.comment.book, book)
@responses.activate
def test_handle_discarded_boost(self):
""" test a boost of a mastodon status that will be discarded """
status = models.Status(
content="hi",
user=self.remote_user,
)
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
status.save(broadcast=False)
activity = {
"type": "Announce",
"id": "http://www.faraway.com/boost/12",
"actor": self.remote_user.remote_id,
"object": status.remote_id,
}
responses.add(
responses.GET, status.remote_id, json=status.to_activity(), status=200
)
views.inbox.activity_task(activity)
self.assertEqual(models.Boost.objects.count(), 0)
def test_handle_unboost(self):
""" undo a boost """
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
boost = models.Boost.objects.create(
boosted_status=self.status, user=self.remote_user
)
activity = {
"type": "Undo",
"actor": "hi",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {
"type": "Announce",
"id": boost.remote_id,
"actor": self.remote_user.remote_id,
"object": self.status.remote_id,
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"published": "Mon, 25 May 2020 19:31:20 GMT",
},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
self.assertFalse(models.Boost.objects.exists())
def test_handle_unboost_unknown_boost(self):
""" undo a boost """
activity = {
"type": "Undo",
"actor": "hi",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {
"type": "Announce",
"id": "http://fake.com/unknown/boost",
"actor": self.remote_user.remote_id,
"object": self.status.remote_id,
},
}
views.inbox.activity_task(activity)
def test_handle_add_book_to_shelf(self):
""" shelving a book """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf")
shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read"
shelf.save()
activity = {
"id": "https://bookwyrm.social/shelfbook/6189#add",
"type": "Add",
"actor": "https://example.com/users/rat",
"object": {
"type": "Edition",
"title": "Test Title",
"work": work.remote_id,
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://bookwyrm.social/user/mouse/shelf/to-read",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
self.assertEqual(shelf.books.first(), book)
def test_handle_unshelve_book(self):
""" remove a book from a shelf """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf")
shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read"
shelf.save()
shelfbook = models.ShelfBook.objects.create(
user=self.remote_user, shelf=shelf, book=book
)
self.assertEqual(shelf.books.first(), book)
self.assertEqual(shelf.books.count(), 1)
activity = {
"id": shelfbook.remote_id,
"type": "Remove",
"actor": "https://example.com/users/rat",
"object": {
"type": "Edition",
"title": "Test Title",
"work": work.remote_id,
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://bookwyrm.social/user/mouse/shelf/to-read",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
self.assertFalse(shelf.books.exists())
@responses.activate
def test_handle_add_book_to_list(self):
""" listing a book """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
responses.add(
responses.GET,
"https://bookwyrm.social/user/mouse/list/to-read",
json={
"id": "https://example.com/list/22",
"type": "BookList",
"totalItems": 1,
"first": "https://example.com/list/22?page=1",
"last": "https://example.com/list/22?page=1",
"name": "Test List",
"owner": "https://example.com/user/mouse",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/user/mouse/followers"],
"summary": "summary text",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams",
},
)
activity = {
"id": "https://bookwyrm.social/listbook/6189#add",
"type": "Add",
"actor": "https://example.com/users/rat",
"object": {
"type": "Edition",
"title": "Test Title",
"work": work.remote_id,
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://bookwyrm.social/user/mouse/list/to-read",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
booklist = models.List.objects.get()
self.assertEqual(booklist.name, "Test List")
self.assertEqual(booklist.books.first(), book)
@responses.activate
def test_handle_tag_book(self):
""" listing a book """
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
remote_id="https://bookwyrm.social/book/37292",
parent_work=work,
)
responses.add(
responses.GET,
"https://www.example.com/tag/cool-tag",
json={
"id": "https://1b1a78582461.ngrok.io/tag/tag",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://1b1a78582461.ngrok.io/tag/tag?page=1",
"last": "https://1b1a78582461.ngrok.io/tag/tag?page=1",
"name": "cool tag",
"@context": "https://www.w3.org/ns/activitystreams",
},
)
activity = {
"id": "https://bookwyrm.social/listbook/6189#add",
"type": "Add",
"actor": "https://example.com/users/rat",
"object": {
"type": "Edition",
"title": "Test Title",
"work": work.remote_id,
"id": "https://bookwyrm.social/book/37292",
},
"target": "https://www.example.com/tag/cool-tag",
"@context": "https://www.w3.org/ns/activitystreams",
}
views.inbox.activity_task(activity)
tag = models.Tag.objects.get()
self.assertFalse(models.List.objects.exists())
self.assertEqual(tag.name, "cool tag")
self.assertEqual(tag.books.first(), book)
def test_handle_update_user(self):
""" 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("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes())
del userdata["icon"]
self.assertIsNone(self.local_user.name)
views.inbox.activity_task(
{
"type": "Update",
"to": [],
"cc": [],
"actor": "hi",
"id": "sdkjf",
"object": userdata,
}
)
user = models.User.objects.get(id=self.local_user.id)
self.assertEqual(user.name, "MOUSE?? MOUSE!!")
self.assertEqual(user.username, "mouse@example.com")
self.assertEqual(user.localname, "mouse")
self.assertTrue(user.discoverable)
def test_handle_update_edition(self):
""" update an existing edition """
datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_edition.json")
bookdata = json.loads(datafile.read_bytes())
models.Work.objects.create(
title="Test Work", remote_id="https://bookwyrm.social/book/5988"
)
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"):
views.inbox.activity_task(
{
"type": "Update",
"to": [],
"cc": [],
"actor": "hi",
"id": "sdkjf",
"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/bw_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"):
views.inbox.activity_task(
{
"type": "Update",
"to": [],
"cc": [],
"actor": "hi",
"id": "sdkjf",
"object": bookdata,
}
)
book = models.Work.objects.get(id=book.id)
self.assertEqual(book.title, "Piranesi")
def test_handle_blocks(self):
""" create a "block" database entry from an activity """
self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
)
self.assertTrue(models.UserFollows.objects.exists())
self.assertTrue(models.UserFollowRequest.objects.exists())
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/9e1f41ac-9ddd-4159",
"type": "Block",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse",
}
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_user_statuses"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
views.inbox.activity_task(activity)
block = models.UserBlocks.objects.get()
self.assertEqual(block.user_subject, self.remote_user)
self.assertEqual(block.user_object, self.local_user)
self.assertEqual(block.remote_id, "https://example.com/9e1f41ac-9ddd-4159")
self.assertFalse(models.UserFollows.objects.exists())
self.assertFalse(models.UserFollowRequest.objects.exists())
def test_handle_unblock(self):
""" unblock a user """
self.remote_user.blocks.add(self.local_user)
block = models.UserBlocks.objects.get()
block.remote_id = "https://example.com/9e1f41ac-9ddd-4159"
block.save()
self.assertEqual(block.user_subject, self.remote_user)
self.assertEqual(block.user_object, self.local_user)
activity = {
"type": "Undo",
"actor": "hi",
"id": "bleh",
"to": ["https://www.w3.org/ns/activitystreams#public"],
"cc": ["https://example.com/user/mouse/followers"],
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/9e1f41ac-9ddd-4159",
"type": "Block",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse",
},
}
with patch(
"bookwyrm.activitystreams.ActivityStream.add_user_statuses"
) as redis_mock:
views.inbox.activity_task(activity)
self.assertTrue(redis_mock.called)
self.assertFalse(models.UserBlocks.objects.exists())

View file

@ -1,6 +1,7 @@
""" the good stuff! the books! """ """ the good stuff! the books! """
from uuid import uuid4 from uuid import uuid4
from dateutil.parser import parse as dateparse
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector from django.contrib.postgres.search import SearchRank, SearchVector
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
@ -10,6 +11,7 @@ from django.db.models import Avg, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
@ -172,6 +174,20 @@ class EditBook(View):
data["confirm_mode"] = True data["confirm_mode"] = True
# this isn't preserved because it isn't part of the form obj # this isn't preserved because it isn't part of the form obj
data["remove_authors"] = request.POST.getlist("remove_authors") data["remove_authors"] = request.POST.getlist("remove_authors")
# make sure the dates are passed in as datetime, they're currently a string
# QueryDicts are immutable, we need to copy
formcopy = data["form"].data.copy()
try:
formcopy["first_published_date"] = dateparse(
formcopy["first_published_date"]
)
except (MultiValueDictKeyError, ValueError):
pass
try:
formcopy["published_date"] = dateparse(formcopy["published_date"])
except (MultiValueDictKeyError, ValueError):
pass
data["form"].data = formcopy
return TemplateResponse(request, "book/edit_book.html", data) return TemplateResponse(request, "book/edit_book.html", data)
remove_authors = request.POST.getlist("remove_authors") remove_authors = request.POST.getlist("remove_authors")

View file

@ -124,7 +124,7 @@ def handle_remote_webfinger(query):
return None return None
try: try:
user = models.User.objects.get(username=query) user = models.User.objects.get(username__iexact=query)
except models.User.DoesNotExist: except models.User.DoesNotExist:
url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query) url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query)
try: try:
@ -138,7 +138,7 @@ def handle_remote_webfinger(query):
user = activitypub.resolve_remote_id( user = activitypub.resolve_remote_id(
link["href"], model=models.User link["href"], model=models.User
) )
except KeyError: except (KeyError, activitypub.ActivitySerializerError):
return None return None
return user return user

View file

@ -34,7 +34,7 @@ class Search(View):
if query and re.match(regex.full_username, query): if query and re.match(regex.full_username, query):
handle_remote_webfinger(query) handle_remote_webfinger(query)
# do a user search # do a user search
user_results = ( user_results = (
models.User.viewer_aware_objects(request.user) models.User.viewer_aware_objects(request.user)
.annotate( .annotate(

25
bw-dev
View file

@ -19,7 +19,6 @@ function clean {
function runweb { function runweb {
docker-compose run --rm web "$@" docker-compose run --rm web "$@"
clean
} }
function execdb { function execdb {
@ -64,17 +63,16 @@ case "$CMD" in
clean clean
;; ;;
makemigrations) makemigrations)
execweb python manage.py makemigrations "$@" runweb python manage.py makemigrations "$@"
;; ;;
migrate) migrate)
execweb python manage.py rename_app fedireads bookwyrm runweb python manage.py migrate "$@"
execweb python manage.py migrate "$@"
;; ;;
bash) bash)
execweb bash runweb bash
;; ;;
shell) shell)
execweb python manage.py shell runweb python manage.py shell
;; ;;
dbshell) dbshell)
execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB} execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB}
@ -83,22 +81,19 @@ case "$CMD" in
docker-compose restart celery_worker docker-compose restart celery_worker
;; ;;
test) test)
execweb coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@" runweb coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@"
;; ;;
pytest) pytest)
execweb pytest --no-cov-on-fail "$@" runweb pytest --no-cov-on-fail "$@"
;;
test_report)
execweb coverage report
;; ;;
collectstatic) collectstatic)
execweb python manage.py collectstatic --no-input runweb python manage.py collectstatic --no-input
;; ;;
makemessages) makemessages)
execweb django-admin makemessages --no-wrap --ignore=venv3 $@ runweb django-admin makemessages --no-wrap --ignore=venv3 $@
;; ;;
compilemessages) compilemessages)
execweb django-admin compilemessages --ignore venv3 $@ runweb django-admin compilemessages --ignore venv3 $@
;; ;;
build) build)
docker-compose build docker-compose build
@ -110,7 +105,7 @@ case "$CMD" in
makeitblack makeitblack
;; ;;
populate_streams) populate_streams)
execweb python manage.py populate_streams runweb python manage.py populate_streams
;; ;;
*) *)
echo "Unrecognised command. Try: build, clean, up, initdb, resetdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, test, pytest, test_report, black, populate_feeds" echo "Unrecognised command. Try: build, clean, up, initdb, resetdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, test, pytest, test_report, black, populate_feeds"

View file

@ -20,7 +20,7 @@ EMAIL_HOST = env("EMAIL_HOST")
EMAIL_PORT = env("EMAIL_PORT") EMAIL_PORT = env("EMAIL_PORT")
EMAIL_HOST_USER = env("EMAIL_HOST_USER") EMAIL_HOST_USER = env("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = env("EMAIL_USE_TLS") EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS")
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)

View file

@ -1,5 +1,5 @@
celery==4.4.2 celery==4.4.2
Django==3.1.6 Django==3.1.8
django-model-utils==4.0.0 django-model-utils==4.0.0
environs==7.2.0 environs==7.2.0
flower==0.9.4 flower==0.9.4