moviewyrm/bookwyrm/tests/models/test_fields.py

443 lines
18 KiB
Python
Raw Normal View History

2021-03-08 16:49:10 +00:00
""" testing models """
from io import BytesIO
2020-12-05 01:42:01 +00:00
from collections import namedtuple
2020-12-13 23:43:39 +00:00
from dataclasses import dataclass
import json
import pathlib
import re
2020-12-13 23:43:39 +00:00
from typing import List
from unittest.mock import patch
from PIL import Image
import responses
2020-12-05 01:42:01 +00:00
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.db import models
from django.test import TestCase
from django.utils import timezone
2021-02-17 16:35:17 +00:00
from bookwyrm import activitypub
2020-12-13 23:43:39 +00:00
from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm.models import fields, User, Status
from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
2021-03-08 16:49:10 +00:00
# pylint: disable=too-many-public-methods
class ActivitypubFields(TestCase):
2021-03-08 16:49:10 +00:00
""" overwrites standard model feilds to work with activitypub """
def test_validate_remote_id(self):
2021-03-08 16:49:10 +00:00
""" should look like a url """
self.assertIsNone(fields.validate_remote_id("http://www.example.com"))
self.assertIsNone(fields.validate_remote_id("https://www.example.com"))
self.assertIsNone(fields.validate_remote_id("http://exle.com/dlg-23/x"))
self.assertRaises(
2021-03-08 16:49:10 +00:00
ValidationError, fields.validate_remote_id, "http:/example.com/dlfjg-23/x"
)
self.assertRaises(
2021-03-08 16:49:10 +00:00
ValidationError, fields.validate_remote_id, "www.example.com/dlfjg-23/x"
)
self.assertRaises(
2021-03-08 16:49:10 +00:00
ValidationError,
fields.validate_remote_id,
"http://www.example.com/dlfjg 23/x",
)
def test_activitypub_field_mixin(self):
2021-03-08 16:49:10 +00:00
""" generic mixin with super basic to and from functionality """
instance = fields.ActivitypubFieldMixin()
2021-03-08 16:49:10 +00:00
self.assertEqual(instance.field_to_activity("fish"), "fish")
self.assertEqual(instance.field_from_activity("fish"), "fish")
2020-12-12 21:39:55 +00:00
self.assertFalse(instance.deduplication_field)
instance = fields.ActivitypubFieldMixin(
2021-03-08 16:49:10 +00:00
activitypub_wrapper="endpoints", activitypub_field="outbox"
)
2021-03-08 16:49:10 +00:00
self.assertEqual(instance.field_to_activity("fish"), {"outbox": "fish"})
self.assertEqual(instance.field_from_activity({"outbox": "fish"}), "fish")
self.assertEqual(instance.get_activitypub_field(), "endpoints")
instance = fields.ActivitypubFieldMixin()
2021-03-08 16:49:10 +00:00
instance.name = "snake_case_name"
self.assertEqual(instance.get_activitypub_field(), "snakeCaseName")
2020-12-17 02:39:18 +00:00
def test_set_field_from_activity(self):
2021-03-08 16:49:10 +00:00
""" setter from entire json blob """
2020-12-17 02:39:18 +00:00
@dataclass
class TestModel:
2021-03-08 16:49:10 +00:00
""" real simple mock """
2020-12-17 02:39:18 +00:00
field_name: str
2021-03-08 16:49:10 +00:00
mock_model = TestModel(field_name="bip")
TestActivity = namedtuple("test", ("fieldName", "unrelated"))
data = TestActivity(fieldName="hi", unrelated="bfkjh")
2020-12-17 02:39:18 +00:00
instance = fields.ActivitypubFieldMixin()
2021-03-08 16:49:10 +00:00
instance.name = "field_name"
2020-12-17 02:39:18 +00:00
instance.set_field_from_activity(mock_model, data)
2021-03-08 16:49:10 +00:00
self.assertEqual(mock_model.field_name, "hi")
2020-12-17 02:39:18 +00:00
def test_set_activity_from_field(self):
2021-03-08 16:49:10 +00:00
""" set json field given entire model """
2020-12-17 02:39:18 +00:00
@dataclass
class TestModel:
2021-03-08 16:49:10 +00:00
""" real simple mock """
2020-12-17 02:39:18 +00:00
field_name: str
unrelated: str
2021-03-08 16:49:10 +00:00
mock_model = TestModel(field_name="bip", unrelated="field")
2020-12-17 02:39:18 +00:00
instance = fields.ActivitypubFieldMixin()
2021-03-08 16:49:10 +00:00
instance.name = "field_name"
2020-12-17 02:39:18 +00:00
data = {}
instance.set_activity_from_field(data, mock_model)
2021-03-08 16:49:10 +00:00
self.assertEqual(data["fieldName"], "bip")
2020-12-17 02:39:18 +00:00
def test_remote_id_field(self):
2021-03-08 16:49:10 +00:00
""" just sets some defaults on charfield """
instance = fields.RemoteIdField()
self.assertEqual(instance.max_length, 255)
2020-12-12 21:39:55 +00:00
self.assertTrue(instance.deduplication_field)
with self.assertRaises(ValidationError):
2021-03-08 16:49:10 +00:00
instance.run_validators("http://www.example.com/dlfjg 23/x")
def test_username_field(self):
2021-03-08 16:49:10 +00:00
""" again, just setting defaults on username field """
instance = fields.UsernameField()
2021-03-08 16:49:10 +00:00
self.assertEqual(instance.activitypub_field, "preferredUsername")
self.assertEqual(instance.max_length, 150)
self.assertEqual(instance.unique, True)
with self.assertRaises(ValidationError):
2021-03-08 16:49:10 +00:00
instance.run_validators("mouse")
instance.run_validators("mouseexample.com")
instance.run_validators("mouse@example.c")
instance.run_validators("@example.com")
instance.run_validators("mouse@examplecom")
instance.run_validators("one two@fish.aaaa")
instance.run_validators("a*&@exampke.com")
instance.run_validators("trailingwhite@example.com ")
self.assertIsNone(instance.run_validators("mouse@example.com"))
self.assertIsNone(instance.run_validators("mo-2use@ex3ample.com"))
self.assertIsNone(instance.run_validators("aksdhf@sdkjf-df.cm"))
self.assertEqual(instance.field_to_activity("test@example.com"), "test")
2020-12-13 23:43:39 +00:00
def test_privacy_field_defaults(self):
2021-03-08 16:49:10 +00:00
""" post privacy field's many default values """
2020-12-13 23:43:39 +00:00
instance = fields.PrivacyField()
self.assertEqual(instance.max_length, 255)
self.assertEqual(
[c[0] for c in instance.choices],
2021-03-08 16:49:10 +00:00
["public", "unlisted", "followers", "direct"],
)
self.assertEqual(instance.default, "public")
2020-12-13 23:43:39 +00:00
self.assertEqual(
2021-03-08 16:49:10 +00:00
instance.public, "https://www.w3.org/ns/activitystreams#Public"
)
2020-12-13 23:43:39 +00:00
def test_privacy_field_set_field_from_activity(self):
2021-03-08 16:49:10 +00:00
""" translate between to/cc fields and privacy """
2020-12-13 23:43:39 +00:00
@dataclass(init=False)
class TestActivity(ActivityObject):
2021-03-08 16:49:10 +00:00
""" real simple mock """
2020-12-13 23:43:39 +00:00
to: List[str]
cc: List[str]
2021-03-08 16:49:10 +00:00
id: str = "http://hi.com"
type: str = "Test"
2020-12-13 23:43:39 +00:00
2020-12-13 23:56:30 +00:00
class TestPrivacyModel(ActivitypubMixin, BookWyrmModel):
2021-03-08 16:49:10 +00:00
""" real simple mock model because BookWyrmModel is abstract """
2020-12-13 23:43:39 +00:00
privacy_field = fields.PrivacyField()
mention_users = fields.TagField(User)
user = fields.ForeignKey(User, on_delete=models.CASCADE)
2021-03-08 16:49:10 +00:00
public = "https://www.w3.org/ns/activitystreams#Public"
2020-12-13 23:43:39 +00:00
data = TestActivity(
to=[public],
2021-03-08 16:49:10 +00:00
cc=["bleh"],
2020-12-13 23:43:39 +00:00
)
2021-03-08 16:49:10 +00:00
model_instance = TestPrivacyModel(privacy_field="direct")
self.assertEqual(model_instance.privacy_field, "direct")
2020-12-13 23:43:39 +00:00
instance = fields.PrivacyField()
2021-03-08 16:49:10 +00:00
instance.name = "privacy_field"
2020-12-13 23:43:39 +00:00
instance.set_field_from_activity(model_instance, data)
2021-03-08 16:49:10 +00:00
self.assertEqual(model_instance.privacy_field, "public")
2020-12-13 23:43:39 +00:00
2021-03-08 16:49:10 +00:00
data.to = ["bleh"]
2020-12-13 23:43:39 +00:00
data.cc = []
instance.set_field_from_activity(model_instance, data)
2021-03-08 16:49:10 +00:00
self.assertEqual(model_instance.privacy_field, "direct")
2020-12-13 23:43:39 +00:00
2021-03-08 16:49:10 +00:00
data.to = ["bleh"]
data.cc = [public, "waah"]
2020-12-13 23:43:39 +00:00
instance.set_field_from_activity(model_instance, data)
2021-03-08 16:49:10 +00:00
self.assertEqual(model_instance.privacy_field, "unlisted")
2020-12-13 23:43:39 +00:00
2021-03-08 16:49:10 +00:00
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
2021-02-07 00:13:59 +00:00
def test_privacy_field_set_activity_from_field(self, _):
2021-03-08 16:49:10 +00:00
""" translate between to/cc fields and privacy """
2020-12-13 23:43:39 +00:00
user = User.objects.create_user(
2021-03-08 16:49:10 +00:00
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
public = "https://www.w3.org/ns/activitystreams#Public"
followers = "%s/followers" % user.remote_id
2020-12-13 23:43:39 +00:00
instance = fields.PrivacyField()
2021-03-08 16:49:10 +00:00
instance.name = "privacy_field"
2020-12-13 23:43:39 +00:00
2021-03-08 16:49:10 +00:00
model_instance = Status.objects.create(user=user, content="hi")
2020-12-13 23:43:39 +00:00
activity = {}
instance.set_activity_from_field(activity, model_instance)
2021-03-08 16:49:10 +00:00
self.assertEqual(activity["to"], [public])
self.assertEqual(activity["cc"], [followers])
2020-12-13 23:43:39 +00:00
2021-02-07 00:13:59 +00:00
model_instance = Status.objects.create(
2021-03-08 16:49:10 +00:00
user=user, content="hi", privacy="unlisted"
)
2020-12-13 23:43:39 +00:00
activity = {}
instance.set_activity_from_field(activity, model_instance)
2021-03-08 16:49:10 +00:00
self.assertEqual(activity["to"], [followers])
self.assertEqual(activity["cc"], [public])
2020-12-13 23:43:39 +00:00
2021-02-07 00:13:59 +00:00
model_instance = Status.objects.create(
2021-03-08 16:49:10 +00:00
user=user, content="hi", privacy="followers"
)
2020-12-13 23:43:39 +00:00
activity = {}
instance.set_activity_from_field(activity, model_instance)
2021-03-08 16:49:10 +00:00
self.assertEqual(activity["to"], [followers])
self.assertEqual(activity["cc"], [])
2020-12-13 23:43:39 +00:00
model_instance = Status.objects.create(
user=user,
2021-03-08 16:49:10 +00:00
content="hi",
privacy="direct",
2020-12-13 23:43:39 +00:00
)
model_instance.mention_users.set([user])
activity = {}
instance.set_activity_from_field(activity, model_instance)
2021-03-08 16:49:10 +00:00
self.assertEqual(activity["to"], [user.remote_id])
self.assertEqual(activity["cc"], [])
2020-12-13 23:43:39 +00:00
def test_foreign_key(self):
2021-03-08 16:49:10 +00:00
""" should be able to format a related model """
instance = fields.ForeignKey("User", on_delete=models.CASCADE)
Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c")
# returns the remote_id field of the related object
2021-03-08 16:49:10 +00:00
self.assertEqual(instance.field_to_activity(item), "https://e.b/c")
2020-12-13 23:43:39 +00:00
@responses.activate
2020-12-12 21:39:55 +00:00
def test_foreign_key_from_activity_str(self):
2021-03-08 16:49:10 +00:00
""" create a new object from a foreign key """
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
2021-03-08 16:49:10 +00:00
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes())
# don't try to load the user icon
2021-03-08 16:49:10 +00:00
del userdata["icon"]
# it shouldn't match with this unrelated user:
unrelated_user = User.objects.create_user(
2021-03-08 16:49:10 +00:00
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
# test receiving an unknown remote id and loading data
responses.add(
2021-03-08 16:49:10 +00:00
responses.GET, "https://example.com/user/mouse", json=userdata, status=200
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
value = instance.field_from_activity("https://example.com/user/mouse")
2020-12-12 21:39:55 +00:00
self.assertIsInstance(value, User)
self.assertNotEqual(value, unrelated_user)
2021-03-08 16:49:10 +00:00
self.assertEqual(value.remote_id, "https://example.com/user/mouse")
self.assertEqual(value.name, "MOUSE?? MOUSE!!")
2020-12-12 21:39:55 +00:00
def test_foreign_key_from_activity_dict(self):
2021-03-08 16:49:10 +00:00
""" test recieving activity json """
2020-12-12 21:39:55 +00:00
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
2021-03-08 16:49:10 +00:00
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
2020-12-12 21:39:55 +00:00
userdata = json.loads(datafile.read_bytes())
# don't try to load the user icon
2021-03-08 16:49:10 +00:00
del userdata["icon"]
# it shouldn't match with this unrelated user:
unrelated_user = User.objects.create_user(
2021-03-08 16:49:10 +00:00
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
2021-02-17 16:35:17 +00:00
value = instance.field_from_activity(activitypub.Person(**userdata))
self.assertIsInstance(value, User)
self.assertNotEqual(value, unrelated_user)
2021-03-08 16:49:10 +00:00
self.assertEqual(value.remote_id, "https://example.com/user/mouse")
self.assertEqual(value.name, "MOUSE?? MOUSE!!")
# et cetera but we're not testing serializing user json
2020-12-12 21:39:55 +00:00
def test_foreign_key_from_activity_dict_existing(self):
2021-03-08 16:49:10 +00:00
""" test receiving a dict of an existing object in the db """
2020-12-12 21:39:55 +00:00
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
2021-03-08 16:49:10 +00:00
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
2020-12-12 21:39:55 +00:00
userdata = json.loads(datafile.read_bytes())
user = User.objects.create_user(
2021-03-08 16:49:10 +00:00
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
user.remote_id = "https://example.com/user/mouse"
2021-02-07 00:13:59 +00:00
user.save(broadcast=False)
User.objects.create_user(
2021-03-08 16:49:10 +00:00
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
2021-03-08 16:49:10 +00:00
with patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast"):
2021-02-17 16:35:17 +00:00
value = instance.field_from_activity(activitypub.Person(**userdata))
2020-12-12 21:39:55 +00:00
self.assertEqual(value, user)
def test_foreign_key_from_activity_str_existing(self):
2021-03-08 16:49:10 +00:00
""" test receiving a remote id of an existing object in the db """
2020-12-12 21:39:55 +00:00
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
user = User.objects.create_user(
2021-03-08 16:49:10 +00:00
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
User.objects.create_user(
2021-03-08 16:49:10 +00:00
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
value = instance.field_from_activity(user.remote_id)
self.assertEqual(value, user)
def test_one_to_one_field(self):
2021-03-08 16:49:10 +00:00
""" a gussied up foreign key """
instance = fields.OneToOneField("User", on_delete=models.CASCADE)
Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c")
self.assertEqual(instance.field_to_activity(item), {"a": "b"})
def test_many_to_many_field(self):
2021-03-08 16:49:10 +00:00
""" lists! """
instance = fields.ManyToManyField("User")
2021-03-08 16:49:10 +00:00
Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
Queryset = namedtuple("Queryset", ("all", "instance"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c")
another_item = Serializable(lambda: {}, "example.com")
items = Queryset(lambda: [item], another_item)
2021-03-08 16:49:10 +00:00
self.assertEqual(instance.field_to_activity(items), ["https://e.b/c"])
2021-03-08 16:49:10 +00:00
instance = fields.ManyToManyField("User", link_only=True)
instance.name = "snake_case"
self.assertEqual(instance.field_to_activity(items), "example.com/snake_case")
@responses.activate
def test_many_to_many_field_from_activity(self):
2021-03-08 16:49:10 +00:00
""" resolve related fields for a list, takes a list of remote ids """
instance = fields.ManyToManyField(User)
2021-03-08 16:49:10 +00:00
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes())
# don't try to load the user icon
2021-03-08 16:49:10 +00:00
del userdata["icon"]
# test receiving an unknown remote id and loading data
responses.add(
2021-03-08 16:49:10 +00:00
responses.GET, "https://example.com/user/mouse", json=userdata, status=200
)
with patch("bookwyrm.models.user.set_remote_server.delay"):
value = instance.field_from_activity(
2021-03-08 16:49:10 +00:00
["https://example.com/user/mouse", "bleh"]
)
self.assertIsInstance(value, list)
self.assertEqual(len(value), 1)
self.assertIsInstance(value[0], User)
def test_tag_field(self):
2021-03-08 16:49:10 +00:00
""" a special type of many to many field """
instance = fields.TagField("User")
Serializable = namedtuple(
2021-03-08 16:49:10 +00:00
"Serializable", ("to_activity", "remote_id", "name_field", "name")
)
2021-03-08 16:49:10 +00:00
Queryset = namedtuple("Queryset", ("all", "instance"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c", "name", "Name")
another_item = Serializable(lambda: {}, "example.com", "", "")
items = Queryset(lambda: [item], another_item)
result = instance.field_to_activity(items)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 1)
2021-03-08 16:49:10 +00:00
self.assertEqual(result[0].href, "https://e.b/c")
self.assertEqual(result[0].name, "Name")
self.assertEqual(result[0].type, "Serializable")
def test_tag_field_from_activity(self):
2021-03-08 16:49:10 +00:00
""" loadin' a list of items from Links """
# TODO
@responses.activate
2021-03-08 16:49:10 +00:00
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
2021-02-07 00:13:59 +00:00
def test_image_field(self, _):
2021-03-08 16:49:10 +00:00
""" storing images """
user = User.objects.create_user(
2021-03-08 16:49:10 +00:00
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
image_file = pathlib.Path(__file__).parent.joinpath(
2021-03-08 16:49:10 +00:00
"../../static/images/default_avi.jpg"
)
image = Image.open(image_file)
output = BytesIO()
image.save(output, format=image.format)
2021-03-08 16:49:10 +00:00
user.avatar.save("test.jpg", ContentFile(output.getvalue()))
2021-03-08 16:49:10 +00:00
output = fields.image_serializer(user.avatar, alt="alt text")
self.assertIsNotNone(
re.match(
2021-03-08 16:49:10 +00:00
r".*\.jpg",
output.url,
)
)
2021-03-08 16:49:10 +00:00
self.assertEqual(output.name, "alt text")
2021-03-15 22:45:18 +00:00
self.assertEqual(output.type, "Document")
instance = fields.ImageField()
2020-12-17 21:01:08 +00:00
output = fields.image_serializer(user.avatar, alt=None)
self.assertEqual(instance.field_to_activity(user.avatar), output)
responses.add(
responses.GET,
2021-03-08 16:49:10 +00:00
"http://www.example.com/image.jpg",
body=user.avatar.file.read(),
2021-03-08 16:49:10 +00:00
status=200,
)
loaded_image = instance.field_from_activity("http://www.example.com/image.jpg")
self.assertIsInstance(loaded_image, list)
self.assertIsInstance(loaded_image[1], ContentFile)
def test_datetime_field(self):
2021-03-08 16:49:10 +00:00
""" this one is pretty simple, it just has to use isoformat """
instance = fields.DateTimeField()
now = timezone.now()
self.assertEqual(instance.field_to_activity(now), now.isoformat())
2021-03-08 16:49:10 +00:00
self.assertEqual(instance.field_from_activity(now.isoformat()), now)
self.assertEqual(instance.field_from_activity("bip"), None)
def test_array_field(self):
2021-03-08 16:49:10 +00:00
""" idk why it makes them strings but probably for a good reason """
instance = fields.ArrayField(fields.IntegerField)
2021-03-08 16:49:10 +00:00
self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"])
2020-12-17 02:39:18 +00:00
def test_html_field(self):
2021-03-08 16:49:10 +00:00
""" sanitizes html, the sanitizer has its own tests """
2020-12-17 02:39:18 +00:00
instance = fields.HtmlField()
self.assertEqual(
2021-03-08 16:49:10 +00:00
instance.field_from_activity("<marquee><p>hi</p></marquee>"), "<p>hi</p>"
2020-12-17 02:39:18 +00:00
)