""" tests the base functionality for activitypub dataclasses """ from io import BytesIO import json import pathlib from unittest.mock import patch from dataclasses import dataclass from django.test import TestCase from PIL import Image import responses from bookwyrm import activitypub from bookwyrm.activitypub.base_activity import ( ActivityObject, resolve_remote_id, set_related_field, get_representative, ) from bookwyrm.activitypub import ActivitySerializerError from bookwyrm import models @patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.suggested_users.rerank_user_task.delay") @patch("bookwyrm.suggested_users.remove_user_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.activitystreams.populate_stream_task.delay") class BaseActivity(TestCase): """the super class for model-linked activitypub dataclasses""" # pylint: disable=invalid-name def setUp(self): """we're probably going to re-use this so why copy/paste""" with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( "bookwyrm.activitystreams.populate_stream_task.delay" ), patch("bookwyrm.lists_stream.populate_lists_task.delay"): self.user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) self.user.remote_id = "http://example.com/a/b" self.user.save(broadcast=False, update_fields=["remote_id"]) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") self.userdata = json.loads(datafile.read_bytes()) # don't try to load the user icon del self.userdata["icon"] image_file = pathlib.Path(__file__).parent.joinpath( "../../static/images/default_avi.jpg" ) image = Image.open(image_file) output = BytesIO() image.save(output, format=image.format) self.image_data = output.getvalue() def test_get_representative_not_existing(self, *_): """test that an instance representative actor is created if it does not exist""" representative = get_representative() self.assertIsInstance(representative, models.User) def test_init(self, *_): """simple successfuly init""" instance = ActivityObject(id="a", type="b") self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "type")) def test_init_missing(self, *_): """init with missing required params""" with self.assertRaises(ActivitySerializerError): ActivityObject() def test_init_extra_fields(self, *_): """init ignoring additional fields""" instance = ActivityObject(id="a", type="b", fish="c") self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "type")) def test_init_default_field(self, *_): """replace an existing required field with a default field""" @dataclass(init=False) class TestClass(ActivityObject): """test class with default field""" type: str = "TestObject" instance = TestClass(id="a") self.assertEqual(instance.id, "a") self.assertEqual(instance.type, "TestObject") def test_serialize(self, *_): """simple function for converting dataclass to dict""" instance = ActivityObject(id="a", type="b") serialized = instance.serialize() self.assertIsInstance(serialized, dict) self.assertEqual(serialized["id"], "a") self.assertEqual(serialized["type"], "b") @responses.activate def test_resolve_remote_id(self, *_): """look up or load remote data""" # existing item result = resolve_remote_id("http://example.com/a/b", model=models.User) self.assertEqual(result, self.user) # remote item responses.add( responses.GET, "https://example.com/user/mouse", json=self.userdata, status=200, ) with patch("bookwyrm.models.user.set_remote_server.delay"): result = resolve_remote_id( "https://example.com/user/mouse", model=models.User ) self.assertIsInstance(result, models.User) self.assertEqual(result.remote_id, "https://example.com/user/mouse") self.assertEqual(result.name, "MOUSE?? MOUSE!!") def test_to_model_invalid_model(self, *_): """catch mismatch between activity type and model type""" instance = ActivityObject(id="a", type="b") with self.assertRaises(ActivitySerializerError): instance.to_model(model=models.User) @responses.activate def test_to_model_image(self, *_): """update an image field""" activity = activitypub.Person( id=self.user.remote_id, name="New Name", preferredUsername="mouse", inbox="http://www.com/", outbox="http://www.com/", followers="", summary="", publicKey={"id": "hi", "owner": self.user.remote_id, "publicKeyPem": "hi"}, endpoints={}, icon={"type": "Document", "url": "http://www.example.com/image.jpg"}, ) responses.add( responses.GET, "http://www.example.com/image.jpg", body=self.image_data, status=200, ) self.assertIsNone(self.user.avatar.name) with self.assertRaises(ValueError): self.user.avatar.file # pylint: disable=pointless-statement # this would trigger a broadcast because it's a local user with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"): activity.to_model(model=models.User, instance=self.user) self.assertIsNotNone(self.user.avatar.file) self.assertEqual(self.user.name, "New Name") self.assertEqual(self.user.key_pair.public_key, "hi") def test_to_model_many_to_many(self, *_): """annoying that these all need special handling""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"): status = models.Status.objects.create( content="test status", user=self.user, ) book = models.Edition.objects.create( title="Test Edition", remote_id="http://book.com/book" ) update_data = activitypub.Note( id=status.remote_id, content=status.content, attributedTo=self.user.remote_id, published="hi", to=[], cc=[], tag=[ {"type": "Mention", "name": "gerald", "href": "http://example.com/a/b"}, { "type": "Edition", "name": "gerald j. books", "href": "http://book.com/book", }, ], ) update_data.to_model(model=models.Status, instance=status) self.assertEqual(status.mention_users.first(), self.user) self.assertEqual(status.mention_books.first(), book) @responses.activate def test_to_model_one_to_many(self, *_): """these are reversed relationships, where the secondary object keys the primary object but not vice versa""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"): status = models.Status.objects.create( content="test status", user=self.user, ) update_data = activitypub.Note( id=status.remote_id, content=status.content, attributedTo=self.user.remote_id, published="hi", to=[], cc=[], attachment=[ { "url": "http://www.example.com/image.jpg", "name": "alt text", "type": "Document", } ], ) responses.add( responses.GET, "http://www.example.com/image.jpg", body=self.image_data, status=200, ) # sets the celery task call to the function call with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"): with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: discarder.return_value = False update_data.to_model(model=models.Status, instance=status) self.assertIsNone(status.attachments.first()) @responses.activate def test_set_related_field(self, *_): """celery task to add back-references to created objects""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"): status = models.Status.objects.create( content="test status", user=self.user, ) data = { "url": "http://www.example.com/image.jpg", "name": "alt text", "type": "Document", } responses.add( responses.GET, "http://www.example.com/image.jpg", body=self.image_data, status=200, ) set_related_field("Image", "Status", "status", status.remote_id, data) self.assertIsInstance(status.attachments.first(), models.Image) self.assertIsNotNone(status.attachments.first().image)