From 502f29082c04cc652e94090a7d8a0720576b585c Mon Sep 17 00:00:00 2001 From: n2no1 <7995366-n2no1@users.noreply.gitlab.com> Date: Tue, 6 Apr 2021 19:57:57 -0400 Subject: [PATCH 01/24] check the form for initial date values if the book has none --- bookwyrm/templates/book/edit_book.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index a9ce651e..8cf6f31c 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -124,7 +124,7 @@

- +

{% for error in form.first_published_date.errors %}

{{ error | escape }}

@@ -132,7 +132,7 @@

- +

{% for error in form.published_date.errors %}

{{ error | escape }}

From 0941c50c6913d94430d03525396df1b9dca75d3d Mon Sep 17 00:00:00 2001 From: n2no1 <7995366-n2no1@users.noreply.gitlab.com> Date: Tue, 6 Apr 2021 20:43:37 -0400 Subject: [PATCH 02/24] ensure that the book edit confirmation receives initial date data as a datetime --- bookwyrm/templates/book/edit_book.html | 4 ++-- bookwyrm/views/books.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index 8cf6f31c..0cde8dff 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -124,7 +124,7 @@

- +

{% for error in form.first_published_date.errors %}

{{ error | escape }}

@@ -132,7 +132,7 @@

- +

{% for error in form.published_date.errors %}

{{ error | escape }}

diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 58886cad..e982a7d3 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -1,4 +1,5 @@ """ the good stuff! the books! """ +from datetime import datetime from uuid import uuid4 from django.contrib.auth.decorators import login_required, permission_required @@ -172,6 +173,12 @@ class EditBook(View): data["confirm_mode"] = True # this isn't preserved because it isn't part of the form obj data["remove_authors"] = request.POST.getlist("remove_authors") + # we have to 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() + formcopy["first_published_date"] = datetime.strptime(formcopy["first_published_date"], "%Y-%m-%d") + formcopy["published_date"] = datetime.strptime(formcopy["published_date"], "%Y-%m-%d") + data["form"].data = formcopy return TemplateResponse(request, "book/edit_book.html", data) remove_authors = request.POST.getlist("remove_authors") From 51e16fba97b459eb110294628653db298aa8489c Mon Sep 17 00:00:00 2001 From: n2no1 <7995366-n2no1@users.noreply.gitlab.com> Date: Tue, 6 Apr 2021 20:49:19 -0400 Subject: [PATCH 03/24] run black, add a try/catch around the formcopy fix --- bookwyrm/views/books.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index e982a7d3..02f61ea7 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -176,8 +176,18 @@ class EditBook(View): # we have to 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() - formcopy["first_published_date"] = datetime.strptime(formcopy["first_published_date"], "%Y-%m-%d") - formcopy["published_date"] = datetime.strptime(formcopy["published_date"], "%Y-%m-%d") + try: + formcopy["first_published_date"] = datetime.strptime( + formcopy["first_published_date"], "%Y-%m-%d" + ) + except MultiValueDictKeyError: + pass + try: + formcopy["published_date"] = datetime.strptime( + formcopy["published_date"], "%Y-%m-%d" + ) + except MultiValueDictKeyError: + pass data["form"].data = formcopy return TemplateResponse(request, "book/edit_book.html", data) From 31146b00e47052e450eb93ca2559aaa33cfbdc5c Mon Sep 17 00:00:00 2001 From: n2no1 <7995366-n2no1@users.noreply.gitlab.com> Date: Tue, 6 Apr 2021 21:40:11 -0400 Subject: [PATCH 04/24] import MultiValueDictKeyError to catch with formcopy --- bookwyrm/views/books.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 02f61ea7..2e48942b 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -11,6 +11,7 @@ from django.db.models import Avg, Q from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse +from django.utils.datastructures import MultiValueDictKeyError from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.http import require_POST From 4cea7be77155cc549f3ba47402a2f83b91092c37 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 07:46:15 -0700 Subject: [PATCH 05/24] Gets black back on stable tag --- .github/workflows/black.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 5fc849d6..afa9cf2b 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -8,6 +8,6 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - uses: psf/black@20.8b1 + - uses: psf/black@stable with: args: ". --check -l 80 -S" From f11d64f984c23d0c867b9f5c1e18544fb12aeafc Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 08:09:47 -0700 Subject: [PATCH 06/24] Handle all connector errors in search --- bookwyrm/connectors/connector_manager.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index caf6bcbe..bf3c749d 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -1,5 +1,6 @@ """ interface with whatever connectors the app has """ import importlib +import logging import re from urllib.parse import urlparse @@ -11,6 +12,8 @@ from requests import HTTPError from bookwyrm import models from bookwyrm.tasks import app +logger = logging.getLogger(__name__) + class ConnectorException(HTTPError): """ when the connector can't do what was asked """ @@ -44,7 +47,9 @@ def search(query, min_confidence=0.1): if result_set in (None, []): try: 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 # if the search results look the same, ignore them From 45006afdf32707fd16f4492c5efb72030cafe4ab Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 08:50:50 -0700 Subject: [PATCH 07/24] Remove character limit on some book fields --- bookwyrm/models/book.py | 6 +++--- bookwyrm/templates/book/edit_book.html | 10 ++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 3204c603..a6824c0a 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -53,14 +53,14 @@ class Book(BookDataModel): connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True) # 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) - 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) languages = fields.ArrayField( 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) subjects = fields.ArrayField( models.CharField(max_length=255), blank=True, null=True, default=list diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index a9ce651e..15202d80 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -88,12 +88,18 @@

{% trans "Metadata" %}

-

{{ form.title }}

+

+ + +

{% for error in form.title.errors %}

{{ error | escape }}

{% endfor %} -

{{ form.subtitle }}

+

+ + +

{% for error in form.subtitle.errors %}

{{ error | escape }}

{% endfor %} From 63d37c281d59d26f1c83a13a6d62c3977a78e847 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 08:59:33 -0700 Subject: [PATCH 08/24] Handle invalid authors when importing books --- bookwyrm/connectors/abstract_connector.py | 6 +++++- bookwyrm/connectors/openlibrary.py | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 00b5c5c9..2483cc62 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -179,7 +179,11 @@ class AbstractConnector(AbstractMinimalConnector): data = get_data(remote_id) 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 return activity.to_model(model=models.Author) diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index 9be0266c..8ee738eb 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -93,7 +93,10 @@ class Connector(AbstractConnector): # this id is "/authors/OL1234567A" author_id = author_blob["key"] 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"): """ ask openlibrary for the cover """ From 514afdc12d6c42053e10ba08ea106682773ca93b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 09:02:39 -0700 Subject: [PATCH 09/24] Use run instead of exec for bw-dev web commands The issue I had with this initially was the `clean` part, not the `run` part --- bw-dev | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/bw-dev b/bw-dev index b9c4b2a1..42fb4a2e 100755 --- a/bw-dev +++ b/bw-dev @@ -19,7 +19,6 @@ function clean { function runweb { docker-compose run --rm web "$@" - clean } function execdb { @@ -64,17 +63,16 @@ case "$CMD" in clean ;; makemigrations) - execweb python manage.py makemigrations "$@" + runweb python manage.py makemigrations "$@" ;; migrate) - execweb python manage.py rename_app fedireads bookwyrm - execweb python manage.py migrate "$@" + runweb python manage.py migrate "$@" ;; bash) - execweb bash + runweb bash ;; shell) - execweb python manage.py shell + runweb python manage.py shell ;; dbshell) execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB} @@ -83,22 +81,19 @@ case "$CMD" in docker-compose restart celery_worker ;; 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) - execweb pytest --no-cov-on-fail "$@" - ;; - test_report) - execweb coverage report + runweb pytest --no-cov-on-fail "$@" ;; collectstatic) - execweb python manage.py collectstatic --no-input + runweb python manage.py collectstatic --no-input ;; makemessages) - execweb django-admin makemessages --no-wrap --ignore=venv3 $@ + runweb django-admin makemessages --no-wrap --ignore=venv3 $@ ;; compilemessages) - execweb django-admin compilemessages --ignore venv3 $@ + runweb django-admin compilemessages --ignore venv3 $@ ;; build) docker-compose build @@ -110,7 +105,7 @@ case "$CMD" in makeitblack ;; 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" From e3d01c6736b233ca59126ed3b6b6a1b467c85865 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 09:17:04 -0700 Subject: [PATCH 10/24] Gracefully handle errors in webfinger during search --- bookwyrm/views/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 75c5da8f..41e6a608 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -138,7 +138,7 @@ def handle_remote_webfinger(query): user = activitypub.resolve_remote_id( link["href"], model=models.User ) - except KeyError: + except (KeyError, activitypub.ActivitySerializerError): return None return user From ef12b077dd9e81ebd2c57fb07ed69146577a7751 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 10:32:16 -0700 Subject: [PATCH 11/24] Adds following field to actor serialization --- bookwyrm/activitypub/person.py | 1 + bookwyrm/models/activitypub_mixin.py | 19 +++++++++++++++++-- bookwyrm/models/user.py | 6 ++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index 4ab9f08e..9231bd95 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -23,6 +23,7 @@ class Person(ActivityObject): inbox: str publicKey: PublicKey followers: str = None + following: str = None outbox: str = None endpoints: Dict = None name: str = None diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index a253207a..1c4a274c 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -1,5 +1,6 @@ """ activitypub model functionality """ from base64 import b64encode +from collections import namedtuple from functools import reduce import json import operator @@ -25,6 +26,15 @@ from bookwyrm.models.fields import ImageField, ManyToManyField logger = logging.getLogger(__name__) # 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! + +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: """ add this mixin for models that are AP serializable """ @@ -52,6 +62,11 @@ class ActivitypubMixin: self.activity_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 self.deserialize_reverse_fields = ( @@ -430,7 +445,7 @@ def generate_activity(obj): ) in obj.serialize_reverse_fields: related_field = getattr(obj, model_field_name) activity[activity_field_name] = unfurl_related_field( - related_field, sort_field + related_field, sort_field=sort_field ) if not activity.get("id"): @@ -440,7 +455,7 @@ def generate_activity(obj): def unfurl_related_field(related_field, sort_field=None): """ load reverse lookups (like public key owner or Status attachment """ - if hasattr(related_field, "all"): + if sort_field and hasattr(related_field, "all"): return [ unfurl_related_field(i) for i in related_field.order_by(sort_field).all() ] diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 33dedc9e..dcc4162e 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -112,6 +112,12 @@ class User(OrderedCollectionPageMixin, AbstractUser): ) 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 def alt_text(self): From 7c5f078682b128eef96fe52398fa27b695959f1a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 10:33:56 -0700 Subject: [PATCH 12/24] Adds missing migration for #898 --- .../migrations/0062_auto_20210407_1545.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 bookwyrm/migrations/0062_auto_20210407_1545.py diff --git a/bookwyrm/migrations/0062_auto_20210407_1545.py b/bookwyrm/migrations/0062_auto_20210407_1545.py new file mode 100644 index 00000000..3a156637 --- /dev/null +++ b/bookwyrm/migrations/0062_auto_20210407_1545.py @@ -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), + ), + ] From 954958b6f98a66638e567d5403e036c9339f11a1 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 10:54:00 -0700 Subject: [PATCH 13/24] Handle arbitrary errors in isbn search --- bookwyrm/connectors/connector_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index bf3c749d..53198c0a 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -40,8 +40,9 @@ def search(query, min_confidence=0.1): else: try: result_set = connector.isbn_search(isbn) - except (HTTPError, ConnectorException): - pass + except Exception as e: # pylint: disable=broad-except + logger.exception(e) + continue # if no isbn search or results, we fallback to generic search if result_set in (None, []): From 5427790c4e14a03bc8d57377560f17d0fd9c2f3b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 11:02:56 -0700 Subject: [PATCH 14/24] Safer serialization of shelve and unshelve activities --- bookwyrm/models/activitypub_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index a253207a..a4f4f353 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -370,7 +370,7 @@ class CollectionItemMixin(ActivitypubMixin): object_field = getattr(self, self.object_field) collection_field = getattr(self, self.collection_field) return activitypub.Add( - id=self.remote_id, + id=self.get_remote_id(), actor=self.user.remote_id, object=object_field, target=collection_field.remote_id, @@ -381,7 +381,7 @@ class CollectionItemMixin(ActivitypubMixin): object_field = getattr(self, self.object_field) collection_field = getattr(self, self.collection_field) return activitypub.Remove( - id=self.remote_id, + id=self.get_remote_id(), actor=self.user.remote_id, object=object_field, target=collection_field.remote_id, From ac86c194d4550903e73414f4ad7ad9d93b21c5a8 Mon Sep 17 00:00:00 2001 From: n2no1 <7995366-n2no1@users.noreply.gitlab.com> Date: Wed, 7 Apr 2021 14:11:13 -0400 Subject: [PATCH 15/24] move from strptime to dateutil for parsing date inputs --- bookwyrm/views/books.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 2e48942b..b1b2d065 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -2,6 +2,7 @@ from datetime import datetime from uuid import uuid4 +from dateutil.parser import parse as dateparse from django.contrib.auth.decorators import login_required, permission_required from django.contrib.postgres.search import SearchRank, SearchVector from django.core.files.base import ContentFile @@ -178,15 +179,13 @@ class EditBook(View): # QueryDicts are immutable, we need to copy formcopy = data["form"].data.copy() try: - formcopy["first_published_date"] = datetime.strptime( - formcopy["first_published_date"], "%Y-%m-%d" + formcopy["first_published_date"] = dateparse( + formcopy["first_published_date"] ) except MultiValueDictKeyError: pass try: - formcopy["published_date"] = datetime.strptime( - formcopy["published_date"], "%Y-%m-%d" - ) + formcopy["published_date"] = dateparse(formcopy["published_date"]) except MultiValueDictKeyError: pass data["form"].data = formcopy From 89af144105c74d413bf1b87aeced7b5f34bb442a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 7 Apr 2021 18:38:26 -0700 Subject: [PATCH 16/24] Avoid showing "None" for title and subtitle fields --- bookwyrm/templates/book/edit_book.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index 963b2b75..1da7c3f7 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -90,7 +90,7 @@

{% trans "Metadata" %}

- +

{% for error in form.title.errors %}

{{ error | escape }}

@@ -98,7 +98,7 @@

- +

{% for error in form.subtitle.errors %}

{{ error | escape }}

From 779377bd0cf1bdf92dd74bafd08f00b252a7a812 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Apr 2021 08:59:07 -0700 Subject: [PATCH 17/24] Makes localname case insensitive --- .../migrations/0063_auto_20210408_1556.py | 27 +++++++++++++++++++ bookwyrm/models/user.py | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/migrations/0063_auto_20210408_1556.py diff --git a/bookwyrm/migrations/0063_auto_20210408_1556.py b/bookwyrm/migrations/0063_auto_20210408_1556.py new file mode 100644 index 00000000..750997fb --- /dev/null +++ b/bookwyrm/migrations/0063_auto_20210408_1556.py @@ -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], + ), + ), + ] diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index dcc4162e..c519f76c 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -4,6 +4,7 @@ from urllib.parse import urlparse from django.apps import apps from django.contrib.auth.models import AbstractUser, Group +from django.contrib.postgres.fields import CICharField from django.core.validators import MinValueValidator from django.db import models from django.utils import timezone @@ -54,7 +55,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): summary = fields.HtmlField(null=True, blank=True) local = models.BooleanField(default=False) bookwyrm_user = fields.BooleanField(default=True) - localname = models.CharField( + localname = CICharField( max_length=255, null=True, unique=True, From 0de1b8d0c39c85ffa015f1d8d6824685b38286ad Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Apr 2021 09:59:21 -0700 Subject: [PATCH 18/24] Case insensitive remote user search --- bookwyrm/tests/views/test_helpers.py | 3 +++ bookwyrm/views/helpers.py | 2 +- bookwyrm/views/search.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bookwyrm/tests/views/test_helpers.py b/bookwyrm/tests/views/test_helpers.py index c646b4b4..7d2bc42c 100644 --- a/bookwyrm/tests/views/test_helpers.py +++ b/bookwyrm/tests/views/test_helpers.py @@ -112,6 +112,9 @@ class ViewsHelpers(TestCase): result = views.helpers.handle_remote_webfinger("mouse@local.com") self.assertEqual(result, self.local_user) + result = views.helpers.handle_remote_webfinger("mOuSe@loCal.cOm") + self.assertEqual(result, self.local_user) + @responses.activate def test_load_user(self, _): """ find a remote user using webfinger """ diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 41e6a608..2b6501ff 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -124,7 +124,7 @@ def handle_remote_webfinger(query): return None try: - user = models.User.objects.get(username=query) + user = models.User.objects.get(username__iexact=query) except models.User.DoesNotExist: url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query) try: diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index 28f393c8..9e7df9f4 100644 --- a/bookwyrm/views/search.py +++ b/bookwyrm/views/search.py @@ -34,7 +34,7 @@ class Search(View): if query and re.match(regex.full_username, query): handle_remote_webfinger(query) - # do a user search + # do a user search user_results = ( models.User.viewer_aware_objects(request.user) .annotate( From 29f5b1512aab7c2c5b1f3bd417e3d44b25cd4379 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Apr 2021 18:47:59 +0000 Subject: [PATCH 19/24] Bump django from 3.1.6 to 3.1.8 Bumps [django](https://github.com/django/django) from 3.1.6 to 3.1.8. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.6...3.1.8) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 16561da5..6b7d82d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ celery==4.4.2 -Django==3.1.6 +Django==3.1.8 django-model-utils==4.0.0 environs==7.2.0 flower==0.9.4 From 9894b777b6bc1678a33ff78d980364fc02c54915 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Apr 2021 11:18:32 -0700 Subject: [PATCH 20/24] Separates inbox tests into multiple files --- bookwyrm/tests/views/inbox/__init__.py | 1 + bookwyrm/tests/views/inbox/test_inbox.py | 99 ++ bookwyrm/tests/views/inbox/test_inbox_add.py | 157 +++ .../tests/views/inbox/test_inbox_announce.py | 191 ++++ .../tests/views/inbox/test_inbox_block.py | 99 ++ .../tests/views/inbox/test_inbox_create.py | 151 +++ .../tests/views/inbox/test_inbox_delete.py | 107 ++ .../tests/views/inbox/test_inbox_follow.py | 206 ++++ bookwyrm/tests/views/inbox/test_inbox_like.py | 111 ++ .../tests/views/inbox/test_inbox_remove.py | 62 ++ .../tests/views/inbox/test_inbox_update.py | 150 +++ bookwyrm/tests/views/test_inbox.py | 988 ------------------ 12 files changed, 1334 insertions(+), 988 deletions(-) create mode 100644 bookwyrm/tests/views/inbox/__init__.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_add.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_announce.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_block.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_create.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_delete.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_follow.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_like.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_remove.py create mode 100644 bookwyrm/tests/views/inbox/test_inbox_update.py delete mode 100644 bookwyrm/tests/views/test_inbox.py diff --git a/bookwyrm/tests/views/inbox/__init__.py b/bookwyrm/tests/views/inbox/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/bookwyrm/tests/views/inbox/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/bookwyrm/tests/views/inbox/test_inbox.py b/bookwyrm/tests/views/inbox/test_inbox.py new file mode 100644 index 00000000..992394a9 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox.py @@ -0,0 +1,99 @@ +""" 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() + 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) diff --git a/bookwyrm/tests/views/inbox/test_inbox_add.py b/bookwyrm/tests/views/inbox/test_inbox_add.py new file mode 100644 index 00000000..c22691ad --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_add.py @@ -0,0 +1,157 @@ +""" 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) diff --git a/bookwyrm/tests/views/inbox/test_inbox_announce.py b/bookwyrm/tests/views/inbox/test_inbox_announce.py new file mode 100644 index 00000000..e08d0f5f --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_announce.py @@ -0,0 +1,191 @@ +""" 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": "

a comment

", + "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) diff --git a/bookwyrm/tests/views/inbox/test_inbox_block.py b/bookwyrm/tests/views/inbox/test_inbox_block.py new file mode 100644 index 00000000..5f6fcb37 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_block.py @@ -0,0 +1,99 @@ +""" 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()) diff --git a/bookwyrm/tests/views/inbox/test_inbox_create.py b/bookwyrm/tests/views/inbox/test_inbox_create.py new file mode 100644 index 00000000..ed8a777f --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_create.py @@ -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") diff --git a/bookwyrm/tests/views/inbox/test_inbox_delete.py b/bookwyrm/tests/views/inbox/test_inbox_delete.py new file mode 100644 index 00000000..7d97a251 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_delete.py @@ -0,0 +1,107 @@ +""" 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) diff --git a/bookwyrm/tests/views/inbox/test_inbox_follow.py b/bookwyrm/tests/views/inbox/test_inbox_follow.py new file mode 100644 index 00000000..58b577d1 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_follow.py @@ -0,0 +1,206 @@ +""" 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()) diff --git a/bookwyrm/tests/views/inbox/test_inbox_like.py b/bookwyrm/tests/views/inbox/test_inbox_like.py new file mode 100644 index 00000000..d569a9ca --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_like.py @@ -0,0 +1,111 @@ +""" 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) diff --git a/bookwyrm/tests/views/inbox/test_inbox_remove.py b/bookwyrm/tests/views/inbox/test_inbox_remove.py new file mode 100644 index 00000000..d875abda --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_remove.py @@ -0,0 +1,62 @@ +""" 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()) diff --git a/bookwyrm/tests/views/inbox/test_inbox_update.py b/bookwyrm/tests/views/inbox/test_inbox_update.py new file mode 100644 index 00000000..9d4c3cca --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_update.py @@ -0,0 +1,150 @@ +""" 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") diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py deleted file mode 100644 index 44a29a92..00000000 --- a/bookwyrm/tests/views/test_inbox.py +++ /dev/null @@ -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": "

a comment

", - "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()) From 94764160cc08d09794fe5ccddaa85c6811b766d6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Apr 2021 12:15:47 -0700 Subject: [PATCH 21/24] Whitespace fixes --- bookwyrm/tests/views/inbox/test_inbox_add.py | 1 - bookwyrm/tests/views/inbox/test_inbox_announce.py | 1 - bookwyrm/tests/views/inbox/test_inbox_block.py | 1 - bookwyrm/tests/views/inbox/test_inbox_create.py | 2 +- bookwyrm/tests/views/inbox/test_inbox_delete.py | 1 - bookwyrm/tests/views/inbox/test_inbox_follow.py | 1 - bookwyrm/tests/views/inbox/test_inbox_like.py | 1 - bookwyrm/tests/views/inbox/test_inbox_remove.py | 1 - bookwyrm/tests/views/inbox/test_inbox_update.py | 1 - 9 files changed, 1 insertion(+), 9 deletions(-) diff --git a/bookwyrm/tests/views/inbox/test_inbox_add.py b/bookwyrm/tests/views/inbox/test_inbox_add.py index c22691ad..638d56d7 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_add.py +++ b/bookwyrm/tests/views/inbox/test_inbox_add.py @@ -35,7 +35,6 @@ class InboxActivities(TestCase): models.SiteSettings.objects.create() - def test_handle_add_book_to_shelf(self): """ shelving a book """ work = models.Work.objects.create(title="work title") diff --git a/bookwyrm/tests/views/inbox/test_inbox_announce.py b/bookwyrm/tests/views/inbox/test_inbox_announce.py index e08d0f5f..a730045a 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_announce.py +++ b/bookwyrm/tests/views/inbox/test_inbox_announce.py @@ -50,7 +50,6 @@ class InboxActivities(TestCase): } models.SiteSettings.objects.create() - @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_handle_boost(self, _): """ boost a status """ diff --git a/bookwyrm/tests/views/inbox/test_inbox_block.py b/bookwyrm/tests/views/inbox/test_inbox_block.py index 5f6fcb37..e686c6b7 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_block.py +++ b/bookwyrm/tests/views/inbox/test_inbox_block.py @@ -34,7 +34,6 @@ class InboxBlock(TestCase): 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) diff --git a/bookwyrm/tests/views/inbox/test_inbox_create.py b/bookwyrm/tests/views/inbox/test_inbox_create.py index ed8a777f..437f6ffc 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_create.py +++ b/bookwyrm/tests/views/inbox/test_inbox_create.py @@ -31,7 +31,7 @@ class InboxActivities(TestCase): remote_id="https://example.com/status/1", ) with patch("bookwyrm.models.user.set_remote_server.delay"): - models.User.objects.create_user( + models.User.objects.create_user( "rat", "rat@rat.com", "ratword", diff --git a/bookwyrm/tests/views/inbox/test_inbox_delete.py b/bookwyrm/tests/views/inbox/test_inbox_delete.py index 7d97a251..65a75426 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_delete.py +++ b/bookwyrm/tests/views/inbox/test_inbox_delete.py @@ -49,7 +49,6 @@ class InboxActivities(TestCase): } models.SiteSettings.objects.create() - def test_handle_delete_status(self): """ remove a status """ self.assertFalse(self.status.deleted) diff --git a/bookwyrm/tests/views/inbox/test_inbox_follow.py b/bookwyrm/tests/views/inbox/test_inbox_follow.py index 58b577d1..c549c31b 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_follow.py +++ b/bookwyrm/tests/views/inbox/test_inbox_follow.py @@ -34,7 +34,6 @@ class InboxRelationships(TestCase): models.SiteSettings.objects.create() - def test_handle_follow(self): """ remote user wants to follow local user """ activity = { diff --git a/bookwyrm/tests/views/inbox/test_inbox_like.py b/bookwyrm/tests/views/inbox/test_inbox_like.py index d569a9ca..05105b75 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_like.py +++ b/bookwyrm/tests/views/inbox/test_inbox_like.py @@ -49,7 +49,6 @@ class InboxActivities(TestCase): } models.SiteSettings.objects.create() - def test_handle_favorite(self): """ fav a status """ activity = { diff --git a/bookwyrm/tests/views/inbox/test_inbox_remove.py b/bookwyrm/tests/views/inbox/test_inbox_remove.py index d875abda..b3e42bbd 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_remove.py +++ b/bookwyrm/tests/views/inbox/test_inbox_remove.py @@ -25,7 +25,6 @@ class InboxActivities(TestCase): models.SiteSettings.objects.create() - def test_handle_unshelve_book(self): """ remove a book from a shelf """ work = models.Work.objects.create(title="work title") diff --git a/bookwyrm/tests/views/inbox/test_inbox_update.py b/bookwyrm/tests/views/inbox/test_inbox_update.py index 9d4c3cca..012343e7 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_update.py +++ b/bookwyrm/tests/views/inbox/test_inbox_update.py @@ -34,7 +34,6 @@ class InboxUpdate(TestCase): } models.SiteSettings.objects.create() - def test_handle_update_list(self): """ a new list """ with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): From 45926eed347c91788486331fbbc54e31779d0865 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Apr 2021 13:33:24 -0700 Subject: [PATCH 22/24] Adds necessary user to inbox tests --- bookwyrm/tests/views/inbox/test_inbox.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bookwyrm/tests/views/inbox/test_inbox.py b/bookwyrm/tests/views/inbox/test_inbox.py index 992394a9..12d7a736 100644 --- a/bookwyrm/tests/views/inbox/test_inbox.py +++ b/bookwyrm/tests/views/inbox/test_inbox.py @@ -15,6 +15,15 @@ class Inbox(TestCase): 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", From 27664e323ad911aea89328a9dd60a3c41f6e88a9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 8 Apr 2021 16:08:35 -0700 Subject: [PATCH 23/24] Fixes edit book form throwing error on empty dates --- bookwyrm/templates/book/edit_book.html | 2 +- bookwyrm/views/books.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index 1da7c3f7..af5d4d69 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -98,7 +98,7 @@

- +

{% for error in form.subtitle.errors %}

{{ error | escape }}

diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index b1b2d065..731fc24c 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -1,5 +1,4 @@ """ the good stuff! the books! """ -from datetime import datetime from uuid import uuid4 from dateutil.parser import parse as dateparse @@ -175,18 +174,18 @@ class EditBook(View): data["confirm_mode"] = True # this isn't preserved because it isn't part of the form obj data["remove_authors"] = request.POST.getlist("remove_authors") - # we have to make sure the dates are passed in as datetime, they're currently a string + # 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: + except (MultiValueDictKeyError, ValueError): pass try: formcopy["published_date"] = dateparse(formcopy["published_date"]) - except MultiValueDictKeyError: + except (MultiValueDictKeyError, ValueError): pass data["form"].data = formcopy return TemplateResponse(request, "book/edit_book.html", data) From 5cbf7bbc7151b5051d6e7faf4fc165a6b8a21d0a Mon Sep 17 00:00:00 2001 From: D Anzorge Date: Fri, 9 Apr 2021 04:53:18 +0200 Subject: [PATCH 24/24] Ensure EMAIL_USE_TLS is read as a bool --- bookwyrm/settings.py | 2 +- celerywyrm/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 845f81c4..52dbc2b2 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -24,7 +24,7 @@ EMAIL_HOST = env("EMAIL_HOST") EMAIL_PORT = env("EMAIL_PORT", 587) EMAIL_HOST_USER = env("EMAIL_HOST_USER") 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")) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) diff --git a/celerywyrm/settings.py b/celerywyrm/settings.py index 7591163b..7947cb1c 100644 --- a/celerywyrm/settings.py +++ b/celerywyrm/settings.py @@ -20,7 +20,7 @@ EMAIL_HOST = env("EMAIL_HOST") EMAIL_PORT = env("EMAIL_PORT") EMAIL_HOST_USER = env("EMAIL_HOST_USER") 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, ...)