Merge branch 'main' into progress_update

This commit is contained in:
Joel Bradshaw
2021-01-13 19:45:36 -08:00
179 changed files with 9338 additions and 3228 deletions

View File

@ -12,8 +12,6 @@ class Author(TestCase):
)
self.author = models.Author.objects.create(
name='Author fullname',
first_name='Auth',
last_name='Or',
aliases=['One', 'Two'],
bio='bio bio bio',
)

View File

@ -0,0 +1,263 @@
''' 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
from bookwyrm.activitypub import ActivitySerializerError
from bookwyrm import models
class BaseActivity(TestCase):
''' the super class for model-linked activitypub dataclasses '''
def setUp(self):
''' we're probably going to re-use this so why copy/paste '''
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()
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_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(models.User, 'http://example.com/a/b')
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(
models.User, 'https://example.com/user/mouse')
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(models.User)
def test_to_model_simple_fields(self):
''' test setting simple fields '''
self.assertIsNone(self.user.name)
activity = activitypub.Person(
id=self.user.remote_id,
name='New Name',
preferredUsername='mouse',
inbox='http://www.com/',
outbox='http://www.com/',
followers='',
summary='',
publicKey=None,
endpoints={},
)
activity.to_model(models.User, self.user)
self.assertEqual(self.user.name, 'New Name')
def test_to_model_foreign_key(self):
''' test setting one to one/foreign key '''
activity = activitypub.Person(
id=self.user.remote_id,
name='New Name',
preferredUsername='mouse',
inbox='http://www.com/',
outbox='http://www.com/',
followers='',
summary='',
publicKey=self.user.key_pair.to_activity(),
endpoints={},
)
activity.publicKey['publicKeyPem'] = 'hi im secure'
activity.to_model(models.User, self.user)
self.assertEqual(self.user.key_pair.public_key, 'hi im secure')
@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=None,
endpoints={},
icon={'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
activity.to_model(models.User, self.user)
self.assertIsNotNone(self.user.avatar.name)
self.assertIsNotNone(self.user.avatar.file)
def test_to_model_many_to_many(self):
''' annoying that these all need special handling '''
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(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 '''
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': 'Image',
}],
)
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'):
update_data.to_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 '''
status = models.Status.objects.create(
content='test status',
user=self.user,
)
data = {
'url': 'http://www.example.com/image.jpg',
'name': 'alt text',
'type': 'Image',
}
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)

View File

@ -1,5 +1,7 @@
# pylint: disable=missing-module-docstring, missing-class-docstring, missing-function-docstring
import json
import pathlib
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import activitypub, models
@ -7,9 +9,6 @@ from bookwyrm import activitypub, models
class Person(TestCase):
def setUp(self):
self.user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json'
)
@ -21,3 +20,12 @@ class Person(TestCase):
self.assertEqual(activity.id, 'https://example.com/user/mouse')
self.assertEqual(activity.preferredUsername, 'mouse')
self.assertEqual(activity.type, 'Person')
def test_user_to_model(self):
activity = activitypub.Person(**self.user_data)
with patch('bookwyrm.models.user.set_remote_server.delay'):
user = activity.to_model(models.User)
self.assertEqual(user.username, 'mouse@example.com')
self.assertEqual(user.remote_id, 'https://example.com/user/mouse')
self.assertFalse(user.local)

View File

@ -1,5 +1,7 @@
''' quotation activty object serializer class '''
import json
import pathlib
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import activitypub, models
@ -8,13 +10,15 @@ from bookwyrm import activitypub, models
class Quotation(TestCase):
''' we have hecka ways to create statuses '''
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=False,
inbox='https://example.com/user/mouse/inbox',
outbox='https://example.com/user/mouse/outbox',
remote_id='https://example.com/user/mouse',
)
''' model objects we'll need '''
with patch('bookwyrm.models.user.set_remote_server.delay'):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=False,
inbox='https://example.com/user/mouse/inbox',
outbox='https://example.com/user/mouse/outbox',
remote_id='https://example.com/user/mouse',
)
self.book = models.Edition.objects.create(
title='Example Edition',
remote_id='https://example.com/book/1',
@ -26,6 +30,7 @@ class Quotation(TestCase):
def test_quotation_activity(self):
''' create a Quoteation ap object from json '''
quotation = activitypub.Quotation(**self.status_data)
self.assertEqual(quotation.type, 'Quotation')
@ -39,6 +44,7 @@ class Quotation(TestCase):
def test_activity_to_model(self):
''' create a model instance from an activity object '''
activity = activitypub.Quotation(**self.status_data)
quotation = activity.to_model(models.Quotation)

View File

@ -1,98 +1,114 @@
''' testing book data connectors '''
from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import models
from bookwyrm.connectors import abstract_connector
from bookwyrm.connectors.abstract_connector import Mapping
from bookwyrm.connectors.bookwyrm_connector import Connector
from bookwyrm.settings import DOMAIN
class AbstractConnector(TestCase):
''' generic code for connecting to outside data sources '''
def setUp(self):
self.book = models.Edition.objects.create(title='Example Edition')
models.Connector.objects.create(
''' we need an example connector '''
self.connector_info = models.Connector.objects.create(
identifier='example.com',
connector_file='bookwyrm_connector',
connector_file='openlibrary',
base_url='https://example.com',
books_url='https:/example.com',
covers_url='https://example.com',
books_url='https://example.com/books',
covers_url='https://example.com/covers',
search_url='https://example.com/search?q=',
)
self.connector = Connector('example.com')
self.data = {
'title': 'Unused title',
'ASIN': 'A00BLAH',
'isbn_10': '1234567890',
'isbn_13': 'blahhh',
'blah': 'bip',
'format': 'hardcover',
'series': ['one', 'two'],
work_data = {
'id': 'abc1',
'title': 'Test work',
'type': 'work',
'openlibraryKey': 'OL1234W',
}
self.connector.key_mappings = [
Mapping('isbn_10', model=models.Edition),
Mapping('isbn_13'),
Mapping('lccn', model=models.Work),
Mapping('asin', remote_field='ASIN'),
self.work_data = work_data
edition_data = {
'id': 'abc2',
'title': 'Test edition',
'type': 'edition',
'openlibraryKey': 'OL1234M',
}
self.edition_data = edition_data
class TestConnector(abstract_connector.AbstractConnector):
''' nothing added here '''
def format_search_result(self, search_result):
return search_result
def parse_search_data(self, data):
return data
def is_work_data(self, data):
return data['type'] == 'work'
def get_edition_from_work_data(self, data):
return edition_data
def get_work_from_edition_data(self, data):
return work_data
def get_authors_from_data(self, data):
return []
def expand_book_data(self, book):
pass
self.connector = TestConnector('example.com')
self.connector.book_mappings = [
Mapping('id'),
Mapping('title'),
Mapping('openlibraryKey'),
]
def test_create_mapping(self):
mapping = Mapping('isbn')
self.assertEqual(mapping.local_field, 'isbn')
self.assertEqual(mapping.remote_field, 'isbn')
self.assertEqual(mapping.model, None)
self.assertEqual(mapping.formatter('bb'), 'bb')
self.book = models.Edition.objects.create(
title='Test Book', remote_id='https://example.com/book/1234',
openlibrary_key='OL1234M')
def test_create_mapping_with_remote(self):
mapping = Mapping('isbn', remote_field='isbn13')
self.assertEqual(mapping.local_field, 'isbn')
self.assertEqual(mapping.remote_field, 'isbn13')
self.assertEqual(mapping.model, None)
self.assertEqual(mapping.formatter('bb'), 'bb')
def test_abstract_connector_init(self):
''' barebones connector for search with defaults '''
self.assertIsInstance(self.connector.book_mappings, list)
def test_create_mapping_with_formatter(self):
formatter = lambda x: 'aa' + x
mapping = Mapping('isbn', formatter=formatter)
self.assertEqual(mapping.local_field, 'isbn')
self.assertEqual(mapping.remote_field, 'isbn')
self.assertEqual(mapping.formatter, formatter)
self.assertEqual(mapping.model, None)
self.assertEqual(mapping.formatter('bb'), 'aabb')
def test_is_available(self):
''' this isn't used.... '''
self.assertTrue(self.connector.is_available())
self.connector.max_query_count = 1
self.connector.connector.query_count = 2
self.assertFalse(self.connector.is_available())
def test_match_from_mappings(self):
edition = models.Edition.objects.create(
title='Blah',
isbn_13='blahhh',
def test_get_or_create_book_existing(self):
''' find an existing book by remote/origin id '''
self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(
self.book.remote_id, 'https://%s/book/%d' % (DOMAIN, self.book.id))
self.assertEqual(
self.book.origin_id, 'https://example.com/book/1234')
# dedupe by origin id
result = self.connector.get_or_create_book(
'https://example.com/book/1234')
self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(result, self.book)
# dedupe by remote id
result = self.connector.get_or_create_book(
'https://%s/book/%d' % (DOMAIN, self.book.id))
self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(result, self.book)
@responses.activate
def test_get_or_create_book_deduped(self):
''' load remote data and deduplicate '''
responses.add(
responses.GET,
'https://example.com/book/abcd',
json=self.edition_data
)
match = self.connector.match_from_mappings(self.data, models.Edition)
self.assertEqual(match, edition)
def test_match_from_mappings_with_model(self):
edition = models.Edition.objects.create(
title='Blah',
isbn_10='1234567890',
)
match = self.connector.match_from_mappings(self.data, models.Edition)
self.assertEqual(match, edition)
def test_match_from_mappings_with_remote(self):
edition = models.Edition.objects.create(
title='Blah',
asin='A00BLAH',
)
match = self.connector.match_from_mappings(self.data, models.Edition)
self.assertEqual(match, edition)
def test_match_from_mappings_no_match(self):
edition = models.Edition.objects.create(
title='Blah',
)
match = self.connector.match_from_mappings(self.data, models.Edition)
self.assertEqual(match, None)
with patch(
'bookwyrm.connectors.abstract_connector.load_more_data.delay'):
result = self.connector.get_or_create_book(
'https://example.com/book/abcd')
self.assertEqual(result, self.book)
self.assertEqual(models.Edition.objects.count(), 1)
self.assertEqual(models.Edition.objects.count(), 1)

View File

@ -0,0 +1,100 @@
''' testing book data connectors '''
from django.test import TestCase
import responses
from bookwyrm import models
from bookwyrm.connectors import abstract_connector
from bookwyrm.connectors.abstract_connector import Mapping, SearchResult
class AbstractConnector(TestCase):
''' generic code for connecting to outside data sources '''
def setUp(self):
''' we need an example connector '''
self.connector_info = models.Connector.objects.create(
identifier='example.com',
connector_file='openlibrary',
base_url='https://example.com',
books_url='https://example.com/books',
covers_url='https://example.com/covers',
search_url='https://example.com/search?q=',
)
class TestConnector(abstract_connector.AbstractMinimalConnector):
''' nothing added here '''
def format_search_result(self, search_result):
return search_result
def get_or_create_book(self, remote_id):
pass
def parse_search_data(self, data):
return data
self.test_connector = TestConnector('example.com')
def test_abstract_minimal_connector_init(self):
''' barebones connector for search with defaults '''
connector = self.test_connector
self.assertEqual(connector.connector, self.connector_info)
self.assertEqual(connector.base_url, 'https://example.com')
self.assertEqual(connector.books_url, 'https://example.com/books')
self.assertEqual(connector.covers_url, 'https://example.com/covers')
self.assertEqual(connector.search_url, 'https://example.com/search?q=')
self.assertIsNone(connector.name)
self.assertEqual(connector.identifier, 'example.com')
self.assertIsNone(connector.max_query_count)
self.assertFalse(connector.local)
@responses.activate
def test_search(self):
''' makes an http request to the outside service '''
responses.add(
responses.GET,
'https://example.com/search?q=a%20book%20title',
json=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'],
status=200)
results = self.test_connector.search('a book title')
self.assertEqual(len(results), 10)
self.assertEqual(results[0], 'a')
self.assertEqual(results[1], 'b')
self.assertEqual(results[2], 'c')
def test_search_result(self):
''' a class that stores info about a search result '''
result = SearchResult(
title='Title',
key='https://example.com/book/1',
author='Author Name',
year='1850',
connector=self.test_connector,
)
# there's really not much to test here, it's just a dataclass
self.assertEqual(result.confidence, 1)
self.assertEqual(result.title, 'Title')
def test_create_mapping(self):
''' maps remote fields for book data to bookwyrm activitypub fields '''
mapping = Mapping('isbn')
self.assertEqual(mapping.local_field, 'isbn')
self.assertEqual(mapping.remote_field, 'isbn')
self.assertEqual(mapping.formatter('bb'), 'bb')
def test_create_mapping_with_remote(self):
''' the remote field is different than the local field '''
mapping = Mapping('isbn', remote_field='isbn13')
self.assertEqual(mapping.local_field, 'isbn')
self.assertEqual(mapping.remote_field, 'isbn13')
self.assertEqual(mapping.formatter('bb'), 'bb')
def test_create_mapping_with_formatter(self):
''' a function is provided to modify the data '''
formatter = lambda x: 'aa' + x
mapping = Mapping('isbn', formatter=formatter)
self.assertEqual(mapping.local_field, 'isbn')
self.assertEqual(mapping.remote_field, 'isbn')
self.assertEqual(mapping.formatter, formatter)
self.assertEqual(mapping.formatter('bb'), 'aabb')

View File

@ -1,16 +1,17 @@
''' testing book data connectors '''
from dateutil import parser
from django.test import TestCase
import json
import pathlib
from django.test import TestCase
from bookwyrm import models
from bookwyrm.connectors.bookwyrm_connector import Connector
from bookwyrm.connectors.abstract_connector import SearchResult, get_date
from bookwyrm.connectors.abstract_connector import SearchResult
class BookWyrmConnector(TestCase):
''' this connector doesn't do much, just search '''
def setUp(self):
''' create the connector '''
models.Connector.objects.create(
identifier='example.com',
connector_file='bookwyrm_connector',
@ -29,13 +30,10 @@ class BookWyrmConnector(TestCase):
self.edition_data = json.loads(edition_file.read_bytes())
def test_is_work_data(self):
self.assertEqual(self.connector.is_work_data(self.work_data), True)
self.assertEqual(self.connector.is_work_data(self.edition_data), False)
def test_format_search_result(self):
datafile = pathlib.Path(__file__).parent.joinpath('../data/bw_search.json')
''' create a SearchResult object from search response json '''
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/bw_search.json')
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_search_data(search_data)
self.assertIsInstance(results, list)
@ -46,9 +44,4 @@ class BookWyrmConnector(TestCase):
self.assertEqual(result.key, 'https://example.com/book/122')
self.assertEqual(result.author, 'Susanna Clarke')
self.assertEqual(result.year, 2017)
def test_get_date(self):
date = get_date(self.edition_data['published_date'])
expected = parser.parse("2020-09-15T00:00:00+00:00")
self.assertEqual(date, expected)
self.assertEqual(result.connector, self.connector)

View File

@ -1,12 +1,18 @@
''' interface between the app and various connectors '''
from django.test import TestCase
from bookwyrm import books_manager, models
from bookwyrm.connectors.bookwyrm_connector import Connector as BookWyrmConnector
from bookwyrm.connectors.self_connector import Connector as SelfConnector
from bookwyrm import models
from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.bookwyrm_connector \
import Connector as BookWyrmConnector
from bookwyrm.connectors.self_connector \
import Connector as SelfConnector
class Book(TestCase):
class ConnectorManager(TestCase):
''' interface between the app and various connectors '''
def setUp(self):
''' we'll need some books and a connector info entry '''
self.work = models.Work.objects.create(
title='Example Work'
)
@ -28,53 +34,50 @@ class Book(TestCase):
covers_url='http://test.com/',
)
def test_get_edition(self):
edition = books_manager.get_edition(self.edition.id)
self.assertEqual(edition, self.edition)
def test_get_edition_work(self):
edition = books_manager.get_edition(self.work.id)
self.assertEqual(edition, self.edition)
def test_get_or_create_connector(self):
''' loads a connector if the data source is known or creates one '''
remote_id = 'https://example.com/object/1'
connector = books_manager.get_or_create_connector(remote_id)
connector = connector_manager.get_or_create_connector(remote_id)
self.assertIsInstance(connector, BookWyrmConnector)
self.assertEqual(connector.identifier, 'example.com')
self.assertEqual(connector.base_url, 'https://example.com')
same_connector = books_manager.get_or_create_connector(remote_id)
same_connector = connector_manager.get_or_create_connector(remote_id)
self.assertEqual(connector.identifier, same_connector.identifier)
def test_get_connectors(self):
''' load all connectors '''
remote_id = 'https://example.com/object/1'
books_manager.get_or_create_connector(remote_id)
connectors = list(books_manager.get_connectors())
connector_manager.get_or_create_connector(remote_id)
connectors = list(connector_manager.get_connectors())
self.assertEqual(len(connectors), 2)
self.assertIsInstance(connectors[0], SelfConnector)
self.assertIsInstance(connectors[1], BookWyrmConnector)
def test_search(self):
results = books_manager.search('Example')
''' search all connectors '''
results = connector_manager.search('Example')
self.assertEqual(len(results), 1)
self.assertIsInstance(results[0]['connector'], SelfConnector)
self.assertEqual(len(results[0]['results']), 1)
self.assertEqual(results[0]['results'][0].title, 'Example Edition')
def test_local_search(self):
results = books_manager.local_search('Example')
''' search only the local database '''
results = connector_manager.local_search('Example')
self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, 'Example Edition')
def test_first_search_result(self):
result = books_manager.first_search_result('Example')
''' only get one search result '''
result = connector_manager.first_search_result('Example')
self.assertEqual(result.title, 'Example Edition')
no_result = books_manager.first_search_result('dkjfhg')
no_result = connector_manager.first_search_result('dkjfhg')
self.assertIsNone(no_result)
def test_load_connector(self):
connector = books_manager.load_connector(self.connector)
''' load a connector object from the database entry '''
connector = connector_manager.load_connector(self.connector)
self.assertIsInstance(connector, SelfConnector)
self.assertEqual(connector.identifier, 'test_connector')

View File

@ -1,19 +1,24 @@
''' testing book data connectors '''
from dateutil import parser
from django.test import TestCase
import json
import pathlib
import pytz
from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import models
from bookwyrm.connectors.openlibrary import Connector
from bookwyrm.connectors.openlibrary import get_languages, get_description
from bookwyrm.connectors.openlibrary import pick_default_edition, get_openlibrary_key
from bookwyrm.connectors.abstract_connector import SearchResult, get_date
from bookwyrm.connectors.openlibrary import pick_default_edition, \
get_openlibrary_key
from bookwyrm.connectors.abstract_connector import SearchResult
from bookwyrm.connectors.connector_manager import ConnectorException
class Openlibrary(TestCase):
''' test loading data from openlibrary.org '''
def setUp(self):
''' creates the connector we'll use '''
models.Connector.objects.create(
identifier='openlibrary.org',
name='OpenLibrary',
@ -36,19 +41,85 @@ class Openlibrary(TestCase):
self.edition_list_data = json.loads(edition_list_file.read_bytes())
def test_get_remote_id_from_data(self):
''' format the remote id from the data '''
data = {'key': '/work/OL1234W'}
result = self.connector.get_remote_id_from_data(data)
self.assertEqual(result, 'https://openlibrary.org/work/OL1234W')
# error handlding
with self.assertRaises(ConnectorException):
self.connector.get_remote_id_from_data({})
def test_is_work_data(self):
''' detect if the loaded json is a work '''
self.assertEqual(self.connector.is_work_data(self.work_data), True)
self.assertEqual(self.connector.is_work_data(self.edition_data), False)
def test_pick_default_edition(self):
edition = pick_default_edition(self.edition_list_data['entries'])
self.assertEqual(edition['key'], '/books/OL9952943M')
@responses.activate
def test_get_edition_from_work_data(self):
''' loads a list of editions '''
data = {'key': '/work/OL1234W'}
responses.add(
responses.GET,
'https://openlibrary.org/work/OL1234W/editions',
json={'entries': []},
status=200)
with patch('bookwyrm.connectors.openlibrary.pick_default_edition') \
as pick_edition:
pick_edition.return_value = 'hi'
result = self.connector.get_edition_from_work_data(data)
self.assertEqual(result, 'hi')
@responses.activate
def test_get_work_from_edition_data(self):
''' loads a list of editions '''
data = {'works': [{'key': '/work/OL1234W'}]}
responses.add(
responses.GET,
'https://openlibrary.org/work/OL1234W',
json={'hi': 'there'},
status=200)
result = self.connector.get_work_from_edition_data(data)
self.assertEqual(result, {'hi': 'there'})
@responses.activate
def test_get_authors_from_data(self):
''' find authors in data '''
responses.add(
responses.GET,
'https://openlibrary.org/authors/OL382982A',
json={'hi': 'there'},
status=200)
results = self.connector.get_authors_from_data(self.work_data)
for result in results:
self.assertIsInstance(result, models.Author)
def test_get_cover_url(self):
''' formats a url that should contain the cover image '''
blob = ['image']
result = self.connector.get_cover_url(blob)
self.assertEqual(
result, 'https://covers.openlibrary.org/b/id/image-L.jpg')
def test_parse_search_result(self):
''' extract the results from the search json response '''
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ol_search.json')
search_data = json.loads(datafile.read_bytes())
result = self.connector.parse_search_data(search_data)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 2)
def test_format_search_result(self):
''' translate json from openlibrary into SearchResult '''
datafile = pathlib.Path(__file__).parent.joinpath('../data/ol_search.json')
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ol_search.json')
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_search_data(search_data)
self.assertIsInstance(results, list)
@ -56,29 +127,66 @@ class Openlibrary(TestCase):
result = self.connector.format_search_result(results[0])
self.assertIsInstance(result, SearchResult)
self.assertEqual(result.title, 'This Is How You Lose the Time War')
self.assertEqual(result.key, 'https://openlibrary.org/works/OL20639540W')
self.assertEqual(
result.key, 'https://openlibrary.org/works/OL20639540W')
self.assertEqual(result.author, 'Amal El-Mohtar, Max Gladstone')
self.assertEqual(result.year, 2019)
self.assertEqual(result.connector, self.connector)
@responses.activate
def test_load_edition_data(self):
''' format url from key and make request '''
key = 'OL1234W'
responses.add(
responses.GET,
'https://openlibrary.org/works/OL1234W/editions',
json={'hi': 'there'}
)
result = self.connector.load_edition_data(key)
self.assertEqual(result, {'hi': 'there'})
@responses.activate
def test_expand_book_data(self):
''' given a book, get more editions '''
work = models.Work.objects.create(
title='Test Work', openlibrary_key='OL1234W')
edition = models.Edition.objects.create(
title='Test Edition', parent_work=work)
responses.add(
responses.GET,
'https://openlibrary.org/works/OL1234W/editions',
json={'entries': []},
)
with patch(
'bookwyrm.connectors.abstract_connector.AbstractConnector.' \
'create_edition_from_data'):
self.connector.expand_book_data(edition)
self.connector.expand_book_data(work)
def test_get_description(self):
''' should do some cleanup on the description data '''
description = get_description(self.work_data['description'])
expected = 'First in the Old Kingdom/Abhorsen series.'
self.assertEqual(description, expected)
def test_get_date(self):
date = get_date(self.work_data['first_publish_date'])
expected = pytz.utc.localize(parser.parse('1995'))
self.assertEqual(date, expected)
def test_get_openlibrary_key(self):
''' extracts the uuid '''
key = get_openlibrary_key('/books/OL27320736M')
self.assertEqual(key, 'OL27320736M')
def test_get_languages(self):
''' looks up languages from a list '''
languages = get_languages(self.edition_data['languages'])
self.assertEqual(languages, ['English'])
def test_get_ol_key(self):
key = get_openlibrary_key('/books/OL27320736M')
self.assertEqual(key, 'OL27320736M')
def test_pick_default_edition(self):
''' detect if the loaded json is an edition '''
edition = pick_default_edition(self.edition_list_data['entries'])
self.assertEqual(edition['key'], '/books/OL9788823M')

View File

@ -9,7 +9,9 @@ from bookwyrm.settings import DOMAIN
class SelfConnector(TestCase):
''' just uses local data '''
def setUp(self):
''' creating the connector '''
models.Connector.objects.create(
identifier=DOMAIN,
name='Local',
@ -22,56 +24,85 @@ class SelfConnector(TestCase):
priority=1,
)
self.connector = Connector(DOMAIN)
self.work = models.Work.objects.create(
title='Example Work',
)
self.edition = models.Edition.objects.create(
title='Edition of Example Work',
author_text='Anonymous',
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
parent_work=self.work,
)
models.Edition.objects.create(
title='Another Edition',
parent_work=self.work,
series='Anonymous'
)
models.Edition.objects.create(
title='More Editions',
subtitle='The Anonymous Edition',
parent_work=self.work,
)
models.Edition.objects.create(
title='An Edition',
author_text='Fish',
parent_work=self.work
)
def test_format_search_result(self):
''' create a SearchResult '''
author = models.Author.objects.create(name='Anonymous')
edition = models.Edition.objects.create(
title='Edition of Example Work',
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
)
edition.authors.add(author)
result = self.connector.search('Edition of Example')[0]
self.assertEqual(result.title, 'Edition of Example Work')
self.assertEqual(result.key, self.edition.remote_id)
self.assertEqual(result.key, edition.remote_id)
self.assertEqual(result.author, 'Anonymous')
self.assertEqual(result.year, 1980)
self.assertEqual(result.connector, self.connector)
def test_search_rank(self):
''' prioritize certain results '''
author = models.Author.objects.create(name='Anonymous')
edition = models.Edition.objects.create(
title='Edition of Example Work',
published_date=datetime.datetime(1980, 5, 10, tzinfo=timezone.utc),
parent_work=models.Work.objects.create(title='')
)
# author text is rank C
edition.authors.add(author)
# series is rank D
models.Edition.objects.create(
title='Another Edition',
series='Anonymous',
parent_work=models.Work.objects.create(title='')
)
# subtitle is rank B
models.Edition.objects.create(
title='More Editions',
subtitle='The Anonymous Edition',
parent_work=models.Work.objects.create(title='')
)
# title is rank A
models.Edition.objects.create(title='Anonymous')
# doesn't rank in this search
edition = models.Edition.objects.create(
title='An Edition',
parent_work=models.Work.objects.create(title='')
)
results = self.connector.search('Anonymous')
self.assertEqual(len(results), 2)
self.assertEqual(results[0].title, 'More Editions')
self.assertEqual(results[1].title, 'Edition of Example Work')
self.assertEqual(len(results), 3)
self.assertEqual(results[0].title, 'Anonymous')
self.assertEqual(results[1].title, 'More Editions')
self.assertEqual(results[2].title, 'Edition of Example Work')
def test_search_default_filter(self):
def test_search_multiple_editions(self):
''' it should get rid of duplicate editions for the same work '''
self.work.default_edition = self.edition
self.work.save()
work = models.Work.objects.create(title='Work Title')
edition_1 = models.Edition.objects.create(
title='Edition 1 Title', parent_work=work)
edition_2 = models.Edition.objects.create(
title='Edition 2 Title', parent_work=work)
edition_3 = models.Edition.objects.create(
title='Fish', parent_work=work)
work.default_edition = edition_2
work.save()
results = self.connector.search('Anonymous')
# pick the best edition
results = self.connector.search('Edition 1 Title')
self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, 'Edition of Example Work')
self.assertEqual(results[0].key, edition_1.remote_id)
# pick the default edition when no match is best
results = self.connector.search('Edition Title')
self.assertEqual(len(results), 1)
self.assertEqual(results[0].key, edition_2.remote_id)
# only matches one edition, so no deduplication takes place
results = self.connector.search('Fish')
self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, 'An Edition')
self.assertEqual(results[0].key, edition_3.remote_id)

View File

@ -0,0 +1,52 @@
{
"id": "https://example.com/users/rat/generatednote/2567/activity",
"type": "Create",
"actor": "https://example.com/users/rat",
"object": {
"id": "https://example.com/users/rat/generatednote/2567",
"type": "GeneratedNote",
"url": null,
"inReplyTo": null,
"published": "2020-12-16T01:45:19.662734+00:00",
"attributedTo": "https://example.com/users/rat",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://example.com/users/rat/followers"
],
"content": "wants to read",
"replies": {
"id": "https://example.com/users/rat/generatednote/2567/replies",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://example.com/users/rat/generatednote/2567/replies?page=true",
"last": "https://example.com/users/rat/generatednote/2567/replies?page=true",
"name": "",
"@context": "https://www.w3.org/ns/activitystreams"
},
"tag": [
{
"href": "https://bookwyrm.social/book/37292",
"name": "Female Husbands",
"type": "Book"
}
],
"attachment": [],
"sensitive": false,
"@context": "https://www.w3.org/ns/activitystreams"
},
"to": [
"https://example.com/users/rat/followers"
],
"cc": [
"https://www.w3.org/ns/activitystreams#Public"
],
"signature": {
"creator": "https://example.com/users/rat#main-key",
"created": "2020-12-16T01:45:19.662734+00:00",
"signatureValue": "R+W8nN1CQAlREjSUeaQwJXZrXTOOLvpHQi9n/3vd8QKq+l6HJEpu7eAht9fjpk8YOKEgV3OUQ7w3E42wM4t+sFiaPoQjY6Xy9IOvx/2LcOZjSOtTkiZ1XnnVb3DSbl8BOBH02+cPvoR6k4LIPHm2IHYZ1UL02WdDWaicHEwl7bw=",
"type": "RsaSignature2017"
},
"@context": "https://www.w3.org/ns/activitystreams"
}

View File

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

View File

@ -0,0 +1,4 @@
Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Average Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Recommended For,Recommended By,Owned Copies,Original Purchase Date,Original Purchase Location,Condition,Condition Description,BCID
42036538,Gideon the Ninth (The Locked Tomb #1),Tamsyn Muir,"Muir, Tamsyn",,"=""1250313198""","=""9781250313195""",0,4.20,Tor,Hardcover,448,2019,2019,2020/10/25,2020/10/21,,,read,,,,1,,,0,,,,,
52691223,Subcutanean,Aaron A. Reed,"Reed, Aaron A.",,"=""""","=""""",0,4.45,,Paperback,232,2020,,2020/03/06,2020/03/05,,,read,,,,1,,,0,,,,,
28694510,Patisserie at Home,Mélanie Dupuis,"Dupuis, Mélanie",Anne Cazor,"=""0062445316""","=""9780062445315""",2,4.60,Harper Design,Hardcover,288,2016,,,2019/07/08,,,read,"mixed feelings",,,2,,,0,,,,,
1 Book Id Title Author Author l-f Additional Authors ISBN ISBN13 My Rating Average Rating Publisher Binding Number of Pages Year Published Original Publication Year Date Read Date Added Bookshelves Bookshelves with positions Exclusive Shelf My Review Spoiler Private Notes Read Count Recommended For Recommended By Owned Copies Original Purchase Date Original Purchase Location Condition Condition Description BCID
2 42036538 Gideon the Ninth (The Locked Tomb #1) Tamsyn Muir Muir, Tamsyn ="1250313198" ="9781250313195" 0 4.20 Tor Hardcover 448 2019 2019 2020/10/25 2020/10/21 read 1 0
3 52691223 Subcutanean Aaron A. Reed Reed, Aaron A. ="" ="" 0 4.45 Paperback 232 2020 2020/03/06 2020/03/05 read 1 0
4 28694510 Patisserie at Home Mélanie Dupuis Dupuis, Mélanie Anne Cazor ="0062445316" ="9780062445315" 2 4.60 Harper Design Hardcover 288 2016 2019/07/08 read mixed feelings 2 0

View File

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

View File

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

View File

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

View File

@ -1,49 +0,0 @@
from django.test import TestCase
from bookwyrm import models, incoming
class IncomingFollowAccept(TestCase):
def setUp(self):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword')
self.local_user.remote_id = 'http://local.com/user/mouse'
self.local_user.save()
def test_handle_follow_accept(self):
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123#accepts",
"type": "Accept",
"actor": "https://example.com/users/rat",
"object": {
"id": "https://example.com/users/rat/follows/123",
"type": "Follow",
"actor": "http://local.com/user/mouse",
"object": "https://example.com/users/rat"
}
}
models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
self.assertEqual(models.UserFollowRequest.objects.count(), 1)
incoming.handle_follow_accept(activity)
# request should be deleted
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
# relationship should be created
follows = self.remote_user.followers
self.assertEqual(follows.count(), 1)
self.assertEqual(follows.first(), self.local_user)

View File

@ -1,31 +0,0 @@
''' when a remote user changes their profile '''
import json
import pathlib
from django.test import TestCase
from bookwyrm import models, incoming
class UpdateUser(TestCase):
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
remote_id='https://example.com/user/mouse',
local=False,
localname='mouse'
)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json'
)
self.user_data = json.loads(datafile.read_bytes())
def test_handle_update_user(self):
self.assertIsNone(self.user.name)
self.assertEqual(self.user.localname, 'mouse')
incoming.handle_update_user({'object': self.user_data})
self.user = models.User.objects.get(id=self.user.id)
self.assertEqual(self.user.name, 'MOUSE?? MOUSE!!')
self.assertEqual(self.user.localname, 'mouse')

View File

@ -1,25 +1,220 @@
''' testing models '''
from collections import namedtuple
from dataclasses import dataclass
import re
from django.test import TestCase
from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm import models
from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models import base_model
from bookwyrm.models.base_model import ActivitypubMixin
from bookwyrm.settings import DOMAIN
class BaseModel(TestCase):
''' functionality shared across models '''
def test_remote_id(self):
instance = BookWyrmModel()
''' these should be generated '''
instance = base_model.BookWyrmModel()
instance.id = 1
expected = instance.get_remote_id()
self.assertEqual(expected, 'https://%s/bookwyrmmodel/1' % DOMAIN)
def test_remote_id_with_user(self):
''' format of remote id when there's a user object '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword')
instance = BookWyrmModel()
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
instance = base_model.BookWyrmModel()
instance.user = user
instance.id = 1
expected = instance.get_remote_id()
self.assertEqual(
expected,
'https://%s/user/mouse/bookwyrmmodel/1' % DOMAIN)
def test_execute_after_save(self):
''' this function sets remote ids after creation '''
# using Work because it BookWrymModel is abstract and this requires save
# Work is a relatively not-fancy model.
instance = models.Work.objects.create(title='work title')
instance.remote_id = None
base_model.execute_after_save(None, instance, True)
self.assertEqual(
instance.remote_id,
'https://%s/book/%d' % (DOMAIN, instance.id)
)
# shouldn't set remote_id if it's not created
instance.remote_id = None
base_model.execute_after_save(None, instance, False)
self.assertIsNone(instance.remote_id)
def test_to_create_activity(self):
''' wrapper for ActivityPub "create" action '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
object_activity = {
'to': 'to field', 'cc': 'cc field',
'content': 'hi',
'published': '2020-12-04T17:52:22.623807+00:00',
}
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: object_activity
)
activity = ActivitypubMixin.to_create_activity(mock_self, user)
self.assertEqual(
activity['id'],
'https://example.com/status/1/activity'
)
self.assertEqual(activity['actor'], user.remote_id)
self.assertEqual(activity['type'], 'Create')
self.assertEqual(activity['to'], 'to field')
self.assertEqual(activity['cc'], 'cc field')
self.assertEqual(activity['object'], object_activity)
self.assertEqual(
activity['signature'].creator,
'%s#main-key' % user.remote_id
)
def test_to_delete_activity(self):
''' wrapper for Delete activity '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {}
)
activity = ActivitypubMixin.to_delete_activity(mock_self, user)
self.assertEqual(
activity['id'],
'https://example.com/status/1/activity'
)
self.assertEqual(activity['actor'], user.remote_id)
self.assertEqual(activity['type'], 'Delete')
self.assertEqual(
activity['to'],
['%s/followers' % user.remote_id])
self.assertEqual(
activity['cc'],
['https://www.w3.org/ns/activitystreams#Public'])
def test_to_update_activity(self):
''' ditto above but for Update '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {}
)
activity = ActivitypubMixin.to_update_activity(mock_self, user)
self.assertIsNotNone(
re.match(
r'^https:\/\/example\.com\/status\/1#update\/.*',
activity['id']
)
)
self.assertEqual(activity['actor'], user.remote_id)
self.assertEqual(activity['type'], 'Update')
self.assertEqual(
activity['to'],
['https://www.w3.org/ns/activitystreams#Public'])
self.assertEqual(activity['object'], {})
def test_to_undo_activity(self):
''' and again, for Undo '''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
MockSelf = namedtuple('Self', ('remote_id', 'to_activity'))
mock_self = MockSelf(
'https://example.com/status/1',
lambda *args: {}
)
activity = ActivitypubMixin.to_undo_activity(mock_self, user)
self.assertEqual(
activity['id'],
'https://example.com/status/1#undo'
)
self.assertEqual(activity['actor'], user.remote_id)
self.assertEqual(activity['type'], 'Undo')
self.assertEqual(activity['object'], {})
def test_to_activity(self):
''' model to ActivityPub json '''
@dataclass(init=False)
class TestActivity(ActivityObject):
''' real simple mock '''
type: str = 'Test'
class TestModel(ActivitypubMixin, base_model.BookWyrmModel):
''' real simple mock model because BookWyrmModel is abstract '''
instance = TestModel()
instance.remote_id = 'https://www.example.com/test'
instance.activity_serializer = TestActivity
activity = instance.to_activity()
self.assertIsInstance(activity, dict)
self.assertEqual(activity['id'], 'https://www.example.com/test')
self.assertEqual(activity['type'], 'Test')
def test_find_existing_by_remote_id(self):
''' attempt to match a remote id to an object in the db '''
# uses a different remote id scheme
# this isn't really part of this test directly but it's helpful to state
book = models.Edition.objects.create(
title='Test Edition', remote_id='http://book.com/book')
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
user.remote_id = 'http://example.com/a/b'
user.save()
self.assertEqual(book.origin_id, 'http://book.com/book')
self.assertNotEqual(book.remote_id, 'http://book.com/book')
# uses subclasses
models.Comment.objects.create(
user=user, content='test status', book=book, \
remote_id='https://comment.net')
result = models.User.find_existing_by_remote_id('hi')
self.assertIsNone(result)
result = models.User.find_existing_by_remote_id(
'http://example.com/a/b')
self.assertEqual(result, user)
# test using origin id
result = models.Edition.find_existing_by_remote_id(
'http://book.com/book')
self.assertEqual(result, book)
# test subclass match
result = models.Status.find_existing_by_remote_id(
'https://comment.net')
def test_find_existing(self):
''' match a blob of data to a model '''
book = models.Edition.objects.create(
title='Test edition',
openlibrary_key='OL1234',
)
result = models.Edition.find_existing(
{'openlibraryKey': 'OL1234'})
self.assertEqual(result, book)

View File

@ -1,5 +1,7 @@
''' testing models '''
from dateutil.parser import parse
from django.test import TestCase
from django.utils import timezone
from bookwyrm import models, settings
from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10
@ -8,6 +10,7 @@ from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10
class Book(TestCase):
''' not too much going on in the books model but here we are '''
def setUp(self):
''' we'll need some books '''
self.work = models.Work.objects.create(
title='Example Work',
remote_id='https://example.com/book/1'
@ -22,9 +25,10 @@ class Book(TestCase):
)
def test_remote_id(self):
''' fanciness with remote/origin ids '''
remote_id = 'https://%s/book/%d' % (settings.DOMAIN, self.work.id)
self.assertEqual(self.work.get_remote_id(), remote_id)
self.assertEqual(self.work.remote_id, 'https://example.com/book/1')
self.assertEqual(self.work.remote_id, remote_id)
def test_create_book(self):
''' you shouldn't be able to create Books (only editions and works) '''
@ -56,15 +60,41 @@ class Book(TestCase):
self.assertEqual(isbn_10, '178816167X')
class Shelf(TestCase):
def setUp(self):
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
models.Shelf.objects.create(
name='Test Shelf', identifier='test-shelf', user=user)
def test_get_edition_info(self):
''' text slug about an edition '''
book = models.Edition.objects.create(title='Test Edition')
self.assertEqual(book.edition_info, '')
def test_remote_id(self):
''' editions and works use the same absolute id syntax '''
shelf = models.Shelf.objects.get(identifier='test-shelf')
expected_id = 'https://%s/user/mouse/shelf/test-shelf' % settings.DOMAIN
self.assertEqual(shelf.get_remote_id(), expected_id)
book.physical_format = 'worm'
book.save()
self.assertEqual(book.edition_info, 'worm')
book.languages = ['English']
book.save()
self.assertEqual(book.edition_info, 'worm')
book.languages = ['Glorbish', 'English']
book.save()
self.assertEqual(book.edition_info, 'worm, Glorbish language')
book.published_date = timezone.make_aware(parse('2020'))
book.save()
self.assertEqual(book.edition_info, 'worm, Glorbish language, 2020')
self.assertEqual(
book.alt_text, 'Test Edition cover (worm, Glorbish language, 2020)')
def test_get_rank(self):
''' sets the data quality index for the book '''
# basic rank
self.assertEqual(self.first_edition.edition_rank, 0)
self.first_edition.description = 'hi'
self.first_edition.save()
self.assertEqual(self.first_edition.edition_rank, 1)
# default edition
self.work.default_edition = self.first_edition
self.work.save()
self.first_edition.refresh_from_db()
self.assertEqual(self.first_edition.edition_rank, 20)

View File

@ -0,0 +1,460 @@
''' testing models '''
from io import BytesIO
from collections import namedtuple
from dataclasses import dataclass
import json
import pathlib
import re
from typing import List
from unittest.mock import patch
from PIL import Image
import responses
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
from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm.models import fields, User, Status
from bookwyrm.models.base_model import ActivitypubMixin, BookWyrmModel
#pylint: disable=too-many-public-methods
class ActivitypubFields(TestCase):
''' overwrites standard model feilds to work with activitypub '''
def test_validate_remote_id(self):
''' 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(
ValidationError, fields.validate_remote_id,
'http:/example.com/dlfjg-23/x')
self.assertRaises(
ValidationError, fields.validate_remote_id,
'www.example.com/dlfjg-23/x')
self.assertRaises(
ValidationError, fields.validate_remote_id,
'http://www.example.com/dlfjg 23/x')
def test_activitypub_field_mixin(self):
''' generic mixin with super basic to and from functionality '''
instance = fields.ActivitypubFieldMixin()
self.assertEqual(instance.field_to_activity('fish'), 'fish')
self.assertEqual(instance.field_from_activity('fish'), 'fish')
self.assertFalse(instance.deduplication_field)
instance = fields.ActivitypubFieldMixin(
activitypub_wrapper='endpoints', activitypub_field='outbox'
)
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()
instance.name = 'snake_case_name'
self.assertEqual(instance.get_activitypub_field(), 'snakeCaseName')
def test_set_field_from_activity(self):
''' setter from entire json blob '''
@dataclass
class TestModel:
''' real simple mock '''
field_name: str
mock_model = TestModel(field_name='bip')
TestActivity = namedtuple('test', ('fieldName', 'unrelated'))
data = TestActivity(fieldName='hi', unrelated='bfkjh')
instance = fields.ActivitypubFieldMixin()
instance.name = 'field_name'
instance.set_field_from_activity(mock_model, data)
self.assertEqual(mock_model.field_name, 'hi')
def test_set_activity_from_field(self):
''' set json field given entire model '''
@dataclass
class TestModel:
''' real simple mock '''
field_name: str
unrelated: str
mock_model = TestModel(field_name='bip', unrelated='field')
instance = fields.ActivitypubFieldMixin()
instance.name = 'field_name'
data = {}
instance.set_activity_from_field(data, mock_model)
self.assertEqual(data['fieldName'], 'bip')
def test_remote_id_field(self):
''' just sets some defaults on charfield '''
instance = fields.RemoteIdField()
self.assertEqual(instance.max_length, 255)
self.assertTrue(instance.deduplication_field)
with self.assertRaises(ValidationError):
instance.run_validators('http://www.example.com/dlfjg 23/x')
def test_username_field(self):
''' again, just setting defaults on username field '''
instance = fields.UsernameField()
self.assertEqual(instance.activitypub_field, 'preferredUsername')
self.assertEqual(instance.max_length, 150)
self.assertEqual(instance.unique, True)
with self.assertRaises(ValidationError):
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')
def test_privacy_field_defaults(self):
''' post privacy field's many default values '''
instance = fields.PrivacyField()
self.assertEqual(instance.max_length, 255)
self.assertEqual(
[c[0] for c in instance.choices],
['public', 'unlisted', 'followers', 'direct'])
self.assertEqual(instance.default, 'public')
self.assertEqual(
instance.public, 'https://www.w3.org/ns/activitystreams#Public')
def test_privacy_field_set_field_from_activity(self):
''' translate between to/cc fields and privacy '''
@dataclass(init=False)
class TestActivity(ActivityObject):
''' real simple mock '''
to: List[str]
cc: List[str]
id: str = 'http://hi.com'
type: str = 'Test'
class TestPrivacyModel(ActivitypubMixin, BookWyrmModel):
''' real simple mock model because BookWyrmModel is abstract '''
privacy_field = fields.PrivacyField()
mention_users = fields.TagField(User)
user = fields.ForeignKey(User, on_delete=models.CASCADE)
public = 'https://www.w3.org/ns/activitystreams#Public'
data = TestActivity(
to=[public],
cc=['bleh'],
)
model_instance = TestPrivacyModel(privacy_field='direct')
self.assertEqual(model_instance.privacy_field, 'direct')
instance = fields.PrivacyField()
instance.name = 'privacy_field'
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, 'public')
data.to = ['bleh']
data.cc = []
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, 'direct')
data.to = ['bleh']
data.cc = [public, 'waah']
instance.set_field_from_activity(model_instance, data)
self.assertEqual(model_instance.privacy_field, 'unlisted')
def test_privacy_field_set_activity_from_field(self):
''' translate between to/cc fields and privacy '''
user = User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
public = 'https://www.w3.org/ns/activitystreams#Public'
followers = '%s/followers' % user.remote_id
instance = fields.PrivacyField()
instance.name = 'privacy_field'
model_instance = Status.objects.create(user=user, content='hi')
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [public])
self.assertEqual(activity['cc'], [followers])
model_instance = Status.objects.create(user=user, privacy='unlisted')
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [followers])
self.assertEqual(activity['cc'], [public])
model_instance = Status.objects.create(user=user, privacy='followers')
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [followers])
self.assertEqual(activity['cc'], [])
model_instance = Status.objects.create(
user=user,
privacy='direct',
)
model_instance.mention_users.set([user])
activity = {}
instance.set_activity_from_field(activity, model_instance)
self.assertEqual(activity['to'], [user.remote_id])
self.assertEqual(activity['cc'], [])
def test_foreign_key(self):
''' 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
self.assertEqual(instance.field_to_activity(item), 'https://e.b/c')
@responses.activate
def test_foreign_key_from_activity_str(self):
''' create a new object from a foreign key '''
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json')
userdata = json.loads(datafile.read_bytes())
# don't try to load the user icon
del userdata['icon']
# it shouldn't match with this unrelated user:
unrelated_user = User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
# test receiving an unknown remote id and loading data
responses.add(
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')
self.assertIsInstance(value, User)
self.assertNotEqual(value, unrelated_user)
self.assertEqual(value.remote_id, 'https://example.com/user/mouse')
self.assertEqual(value.name, 'MOUSE?? MOUSE!!')
def test_foreign_key_from_activity_dict(self):
''' test recieving activity json '''
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json')
userdata = json.loads(datafile.read_bytes())
# don't try to load the user icon
del userdata['icon']
# it shouldn't match with this unrelated user:
unrelated_user = User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
with patch('bookwyrm.models.user.set_remote_server.delay'):
value = instance.field_from_activity(userdata)
self.assertIsInstance(value, User)
self.assertNotEqual(value, unrelated_user)
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
def test_foreign_key_from_activity_dict_existing(self):
''' test receiving a dict of an existing object in the db '''
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json'
)
userdata = json.loads(datafile.read_bytes())
user = User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
user.remote_id = 'https://example.com/user/mouse'
user.save()
User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword',
local=True, localname='rat')
value = instance.field_from_activity(userdata)
self.assertEqual(value, user)
def test_foreign_key_from_activity_str_existing(self):
''' test receiving a remote id of an existing object in the db '''
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
user = User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
User.objects.create_user(
'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):
''' 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):
''' lists! '''
instance = fields.ManyToManyField('User')
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)
self.assertEqual(instance.field_to_activity(items), ['https://e.b/c'])
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):
''' resolve related fields for a list, takes a list of remote ids '''
instance = fields.ManyToManyField(User)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ap_user.json'
)
userdata = json.loads(datafile.read_bytes())
# don't try to load the user icon
del userdata['icon']
# test receiving an unknown remote id and loading data
responses.add(
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', 'bleh']
)
self.assertIsInstance(value, list)
self.assertEqual(len(value), 1)
self.assertIsInstance(value[0], User)
def test_tag_field(self):
''' a special type of many to many field '''
instance = fields.TagField('User')
Serializable = namedtuple(
'Serializable',
('to_activity', 'remote_id', 'name_field', 'name')
)
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)
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):
''' loadin' a list of items from Links '''
# TODO
@responses.activate
def test_image_field(self):
''' storing images '''
user = User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
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)
user.avatar.save(
'test.jpg',
ContentFile(output.getvalue())
)
output = fields.image_serializer(user.avatar, alt='alt text')
self.assertIsNotNone(
re.match(
r'.*\.jpg',
output.url,
)
)
self.assertEqual(output.name, 'alt text')
self.assertEqual(output.type, 'Image')
instance = fields.ImageField()
output = fields.image_serializer(user.avatar, alt=None)
self.assertEqual(instance.field_to_activity(user.avatar), output)
responses.add(
responses.GET,
'http://www.example.com/image.jpg',
body=user.avatar.file.read(),
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):
''' 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())
self.assertEqual(
instance.field_from_activity(now.isoformat()), now
)
self.assertEqual(instance.field_from_activity('bip'), None)
def test_array_field(self):
''' idk why it makes them strings but probably for a good reason '''
instance = fields.ArrayField(fields.IntegerField)
self.assertEqual(instance.field_to_activity([0, 1]), ['0', '1'])
def test_html_field(self):
''' sanitizes html, the sanitizer has its own tests '''
instance = fields.HtmlField()
self.assertEqual(
instance.field_from_activity('<marquee><p>hi</p></marquee>'),
'<p>hi</p>'
)

View File

@ -1,9 +1,16 @@
''' testing models '''
import datetime
import json
import pathlib
from unittest.mock import patch
from django.utils import timezone
from django.test import TestCase
import responses
from bookwyrm import models
from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.abstract_connector import SearchResult
class ImportJob(TestCase):
@ -52,13 +59,14 @@ class ImportJob(TestCase):
unknown_read_data['Date Read'] = ''
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
job = models.ImportJob.objects.create(user=user)
models.ImportItem.objects.create(
self.item_1 = models.ImportItem.objects.create(
job=job, index=1, data=currently_reading_data)
models.ImportItem.objects.create(
self.item_2 = models.ImportItem.objects.create(
job=job, index=2, data=read_data)
models.ImportItem.objects.create(
self.item_3 = models.ImportItem.objects.create(
job=job, index=3, data=unknown_read_data)
@ -72,8 +80,7 @@ class ImportJob(TestCase):
def test_shelf(self):
''' converts to the local shelf typology '''
expected = 'reading'
item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.shelf, expected)
self.assertEqual(self.item_1.shelf, expected)
def test_date_added(self):
@ -91,21 +98,79 @@ class ImportJob(TestCase):
def test_currently_reading_reads(self):
''' infer currently reading dates where available '''
expected = [models.ReadThrough(
start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc))]
start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc)
)]
actual = models.ImportItem.objects.get(index=1)
self.assertEqual(actual.reads[0].start_date, expected[0].start_date)
self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date)
def test_read_reads(self):
actual = models.ImportItem.objects.get(index=2)
self.assertEqual(actual.reads[0].start_date, datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc))
self.assertEqual(actual.reads[0].finish_date, datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc))
''' infer read dates where available '''
actual = self.item_2
self.assertEqual(
actual.reads[0].start_date,
datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc))
self.assertEqual(
actual.reads[0].finish_date,
datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc))
def test_unread_reads(self):
''' handle books with no read dates '''
expected = []
actual = models.ImportItem.objects.get(index=3)
self.assertEqual(actual.reads, expected)
@responses.activate
def test_get_book_from_isbn(self):
''' search and load books by isbn (9780356506999) '''
connector_info = models.Connector.objects.create(
identifier='openlibrary.org',
name='OpenLibrary',
connector_file='openlibrary',
base_url='https://openlibrary.org',
books_url='https://openlibrary.org',
covers_url='https://covers.openlibrary.org',
search_url='https://openlibrary.org/search?q=',
priority=3,
)
connector = connector_manager.load_connector(connector_info)
result = SearchResult(
title='Test Result',
key='https://openlibrary.org/works/OL1234W',
author='An Author',
year='1980',
connector=connector,
)
datafile = pathlib.Path(__file__).parent.joinpath(
'../data/ol_edition.json')
bookdata = json.loads(datafile.read_bytes())
responses.add(
responses.GET,
'https://openlibrary.org/works/OL1234W',
json=bookdata,
status=200)
responses.add(
responses.GET,
'https://openlibrary.org/works/OL15832982W',
json=bookdata,
status=200)
responses.add(
responses.GET,
'https://openlibrary.org/authors/OL382982A',
json={'name': 'test author'},
status=200)
with patch(
'bookwyrm.connectors.abstract_connector.load_more_data.delay'):
with patch(
'bookwyrm.connectors.connector_manager.first_search_result'
) as search:
search.return_value = result
book = self.item_1.get_book_from_isbn()
self.assertEqual(book.title, 'Sabriel')

View File

@ -1,4 +1,5 @@
''' testing models '''
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models
@ -6,15 +7,17 @@ from bookwyrm import models
class Relationship(TestCase):
def setUp(self):
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.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword')
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'http://local.com/user/mouse'
self.local_user.save()

View File

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

View File

@ -1,52 +1,275 @@
''' testing models '''
from io import BytesIO
import pathlib
from PIL import Image
from django.core.files.base import ContentFile
from django.db import IntegrityError
from django.test import TestCase
from django.utils import timezone
from bookwyrm import models, settings
class Status(TestCase):
''' lotta types of statuses '''
def setUp(self):
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
book = models.Edition.objects.create(title='Example Edition')
''' useful things for creating a status '''
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
self.book = models.Edition.objects.create(title='Test Edition')
models.Status.objects.create(user=user, content='Blah blah')
models.Comment.objects.create(user=user, content='content', book=book)
models.Quotation.objects.create(
user=user, content='content', book=book, quote='blah')
models.Review.objects.create(
user=user, content='content', book=book, rating=3)
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.book.cover.save(
'test.jpg',
ContentFile(output.getvalue())
)
def test_status(self):
status = models.Status.objects.first()
def test_status_generated_fields(self):
''' setting remote id '''
status = models.Status.objects.create(content='bleh', user=self.user)
expected_id = 'https://%s/user/mouse/status/%d' % \
(settings.DOMAIN, status.id)
self.assertEqual(status.remote_id, expected_id)
self.assertEqual(status.privacy, 'public')
def test_comment(self):
comment = models.Comment.objects.first()
expected_id = 'https://%s/user/mouse/comment/%d' % \
(settings.DOMAIN, comment.id)
self.assertEqual(comment.remote_id, expected_id)
def test_replies(self):
''' get a list of replies '''
parent = models.Status.objects.create(content='hi', user=self.user)
child = models.Status.objects.create(
content='hello', reply_parent=parent, user=self.user)
models.Review.objects.create(
content='hey', reply_parent=parent, user=self.user, book=self.book)
models.Status.objects.create(
content='hi hello', reply_parent=child, user=self.user)
def test_quotation(self):
quotation = models.Quotation.objects.first()
expected_id = 'https://%s/user/mouse/quotation/%d' % \
(settings.DOMAIN, quotation.id)
self.assertEqual(quotation.remote_id, expected_id)
replies = models.Status.replies(parent)
self.assertEqual(replies.count(), 2)
self.assertEqual(replies.first(), child)
# should select subclasses
self.assertIsInstance(replies.last(), models.Review)
def test_review(self):
review = models.Review.objects.first()
expected_id = 'https://%s/user/mouse/review/%d' % \
(settings.DOMAIN, review.id)
self.assertEqual(review.remote_id, expected_id)
def test_status_type(self):
''' class name '''
self.assertEqual(models.Status().status_type, 'Note')
self.assertEqual(models.Review().status_type, 'Review')
self.assertEqual(models.Quotation().status_type, 'Quotation')
self.assertEqual(models.Comment().status_type, 'Comment')
self.assertEqual(models.Boost().status_type, 'Boost')
def test_boostable(self):
''' can a status be boosted, based on privacy '''
self.assertTrue(models.Status(privacy='public').boostable)
self.assertTrue(models.Status(privacy='unlisted').boostable)
self.assertFalse(models.Status(privacy='followers').boostable)
self.assertFalse(models.Status(privacy='direct').boostable)
class Tag(TestCase):
def test_tag(self):
book = models.Edition.objects.create(title='Example Edition')
user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
tag = models.Tag.objects.create(user=user, book=book, name='t/est tag')
self.assertEqual(tag.identifier, 't%2Fest+tag')
def test_to_replies(self):
''' activitypub replies collection '''
parent = models.Status.objects.create(content='hi', user=self.user)
child = models.Status.objects.create(
content='hello', reply_parent=parent, user=self.user)
models.Review.objects.create(
content='hey', reply_parent=parent, user=self.user, book=self.book)
models.Status.objects.create(
content='hi hello', reply_parent=child, user=self.user)
replies = parent.to_replies()
self.assertEqual(replies['id'], '%s/replies' % parent.remote_id)
self.assertEqual(replies['totalItems'], 2)
def test_status_to_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Status.objects.create(
content='test content', user=self.user)
activity = status.to_activity()
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Note')
self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['sensitive'], False)
def test_status_to_activity_tombstone(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Status.objects.create(
content='test content', user=self.user,
deleted=True, deleted_date=timezone.now())
activity = status.to_activity()
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Tombstone')
self.assertFalse(hasattr(activity, 'content'))
def test_status_to_pure_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Status.objects.create(
content='test content', user=self.user)
activity = status.to_activity(pure=True)
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Note')
self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['sensitive'], False)
self.assertEqual(activity['attachment'], [])
def test_generated_note_to_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.GeneratedNote.objects.create(
content='test content', user=self.user)
status.mention_books.set([self.book])
status.mention_users.set([self.user])
activity = status.to_activity()
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'GeneratedNote')
self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['sensitive'], False)
self.assertEqual(len(activity['tag']), 2)
def test_generated_note_to_pure_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.GeneratedNote.objects.create(
content='test content', user=self.user)
status.mention_books.set([self.book])
status.mention_users.set([self.user])
activity = status.to_activity(pure=True)
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(
activity['content'],
'mouse test content <a href="%s">"Test Edition"</a>' % \
self.book.remote_id)
self.assertEqual(len(activity['tag']), 2)
self.assertEqual(activity['type'], 'Note')
self.assertEqual(activity['sensitive'], False)
self.assertIsInstance(activity['attachment'], list)
self.assertEqual(activity['attachment'][0].type, 'Image')
self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \
(settings.DOMAIN, self.book.cover.url))
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
def test_comment_to_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Comment.objects.create(
content='test content', user=self.user, book=self.book)
activity = status.to_activity()
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Comment')
self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['inReplyToBook'], self.book.remote_id)
def test_comment_to_pure_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Comment.objects.create(
content='test content', user=self.user, book=self.book)
activity = status.to_activity(pure=True)
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Note')
self.assertEqual(
activity['content'],
'test content<p>(comment on <a href="%s">"Test Edition"</a>)</p>' %
self.book.remote_id)
self.assertEqual(activity['attachment'][0].type, 'Image')
self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \
(settings.DOMAIN, self.book.cover.url))
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
def test_quotation_to_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Quotation.objects.create(
quote='a sickening sense', content='test content',
user=self.user, book=self.book)
activity = status.to_activity()
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Quotation')
self.assertEqual(activity['quote'], 'a sickening sense')
self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['inReplyToBook'], self.book.remote_id)
def test_quotation_to_pure_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Quotation.objects.create(
quote='a sickening sense', content='test content',
user=self.user, book=self.book)
activity = status.to_activity(pure=True)
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Note')
self.assertEqual(
activity['content'],
'a sickening sense <p>-- <a href="%s">"Test Edition"</a></p>' \
'test content' % self.book.remote_id)
self.assertEqual(activity['attachment'][0].type, 'Image')
self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \
(settings.DOMAIN, self.book.cover.url))
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
def test_review_to_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Review.objects.create(
name='Review name', content='test content', rating=3,
user=self.user, book=self.book)
activity = status.to_activity()
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Review')
self.assertEqual(activity['rating'], 3)
self.assertEqual(activity['name'], 'Review name')
self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['inReplyToBook'], self.book.remote_id)
def test_review_to_pure_activity(self):
''' subclass of the base model version with a "pure" serializer '''
status = models.Review.objects.create(
name='Review name', content='test content', rating=3,
user=self.user, book=self.book)
activity = status.to_activity(pure=True)
self.assertEqual(activity['id'], status.remote_id)
self.assertEqual(activity['type'], 'Article')
self.assertEqual(
activity['name'], 'Review of "%s" (3 stars): Review name' \
% self.book.title)
self.assertEqual(activity['content'], 'test content')
self.assertEqual(activity['attachment'][0].type, 'Image')
self.assertEqual(activity['attachment'][0].url, 'https://%s%s' % \
(settings.DOMAIN, self.book.cover.url))
self.assertEqual(
activity['attachment'][0].name, 'Test Edition cover')
def test_favorite(self):
''' fav a status '''
status = models.Status.objects.create(
content='test content', user=self.user)
fav = models.Favorite.objects.create(status=status, user=self.user)
# can't fav a status twice
with self.assertRaises(IntegrityError):
models.Favorite.objects.create(status=status, user=self.user)
activity = fav.to_activity()
self.assertEqual(activity['type'], 'Like')
self.assertEqual(activity['actor'], self.user.remote_id)
self.assertEqual(activity['object'], status.remote_id)
def test_boost(self):
''' boosting, this one's a bit fussy '''
status = models.Status.objects.create(
content='test content', user=self.user)
boost = models.Boost.objects.create(
boosted_status=status, user=self.user)
activity = boost.to_activity()
self.assertEqual(activity['actor'], self.user.remote_id)
self.assertEqual(activity['object'], status.remote_id)
self.assertEqual(activity['type'], 'Announce')
self.assertEqual(activity, boost.to_activity(pure=True))
def test_notification(self):
''' a simple model '''
notification = models.Notification.objects.create(
user=self.user, notification_type='FAVORITE')
self.assertFalse(notification.read)
with self.assertRaises(IntegrityError):
models.Notification.objects.create(
user=self.user, notification_type='GLORB')

View File

@ -1,4 +1,5 @@
''' testing models '''
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models
@ -8,7 +9,8 @@ from bookwyrm.settings import DOMAIN
class User(TestCase):
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
'mouse@%s' % DOMAIN, 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
def test_computed_fields(self):
''' username instead of id here '''
@ -19,8 +21,15 @@ class User(TestCase):
self.assertEqual(self.user.shared_inbox, 'https://%s/inbox' % DOMAIN)
self.assertEqual(self.user.inbox, '%s/inbox' % expected_id)
self.assertEqual(self.user.outbox, '%s/outbox' % expected_id)
self.assertIsNotNone(self.user.private_key)
self.assertIsNotNone(self.user.public_key)
self.assertIsNotNone(self.user.key_pair.private_key)
self.assertIsNotNone(self.user.key_pair.public_key)
def test_remote_user(self):
with patch('bookwyrm.models.user.set_remote_server.delay'):
user = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', local=False,
remote_id='https://example.com/dfjkg')
self.assertEqual(user.username, 'rat@example.com')
def test_user_shelves(self):
@ -53,7 +62,6 @@ class User(TestCase):
self.assertEqual(activity['name'], self.user.name)
self.assertEqual(activity['inbox'], self.user.inbox)
self.assertEqual(activity['outbox'], self.user.outbox)
self.assertEqual(activity['followers'], self.user.ap_followers)
self.assertEqual(activity['bookwyrmUser'], True)
self.assertEqual(activity['discoverable'], True)
self.assertEqual(activity['type'], 'Person')

View File

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

View File

@ -1,78 +0,0 @@
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, outgoing
class Following(TestCase):
def setUp(self):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True,
remote_id='http://local.com/users/mouse',
)
def test_handle_follow(self):
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_follow(self.local_user, self.remote_user)
rel = models.UserFollowRequest.objects.get()
self.assertEqual(rel.user_subject, self.local_user)
self.assertEqual(rel.user_object, self.remote_user)
self.assertEqual(rel.status, 'follow_request')
def test_handle_unfollow(self):
self.remote_user.followers.add(self.local_user)
self.assertEqual(self.remote_user.followers.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_unfollow(self.local_user, self.remote_user)
self.assertEqual(self.remote_user.followers.count(), 0)
def test_handle_accept(self):
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
rel_id = rel.id
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_accept(rel)
# request should be deleted
self.assertEqual(
models.UserFollowRequest.objects.filter(id=rel_id).count(), 0
)
# follow relationship should exist
self.assertEqual(self.remote_user.followers.first(), self.local_user)
def test_handle_reject(self):
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
rel_id = rel.id
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_reject(rel)
# request should be deleted
self.assertEqual(
models.UserFollowRequest.objects.filter(id=rel_id).count(), 0
)
# follow relationship should not exist
self.assertEqual(
models.UserFollows.objects.filter(id=rel_id).count(), 0
)

View File

@ -1,69 +0,0 @@
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, outgoing
class Shelving(TestCase):
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True,
remote_id='http://local.com/users/mouse',
)
work = models.Work.objects.create(
title='Example work',
)
self.book = models.Edition.objects.create(
title='Example Edition',
remote_id='https://example.com/book/1',
parent_work=work,
)
self.shelf = models.Shelf.objects.create(
name='Test Shelf',
identifier='test-shelf',
user=self.user
)
def test_handle_shelve(self):
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_shelve(self.user, self.book, self.shelf)
# make sure the book is on the shelf
self.assertEqual(self.shelf.books.get(), self.book)
def test_handle_shelve_to_read(self):
shelf = models.Shelf.objects.get(identifier='to-read')
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_shelve(self.user, self.book, shelf)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
def test_handle_shelve_reading(self):
shelf = models.Shelf.objects.get(identifier='reading')
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_shelve(self.user, self.book, shelf)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
def test_handle_shelve_read(self):
shelf = models.Shelf.objects.get(identifier='read')
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_shelve(self.user, self.book, shelf)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
def test_handle_unshelve(self):
self.shelf.books.add(self.book)
self.shelf.save()
self.assertEqual(self.shelf.books.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay') as _:
outgoing.handle_unshelve(self.user, self.book, self.shelf)
self.assertEqual(self.shelf.books.count(), 0)

View File

@ -1,3 +1,4 @@
from unittest.mock import patch
from django.test import TestCase
from bookwyrm import models, broadcast
@ -6,44 +7,47 @@ from bookwyrm import models, broadcast
class Book(TestCase):
def setUp(self):
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'mouseword')
follower = models.User.objects.create_user(
'rat', 'rat@mouse.mouse', 'ratword', local=False,
remote_id='http://example.com/u/1',
outbox='http://example.com/u/1/o',
shared_inbox='http://example.com/inbox',
inbox='http://example.com/u/1/inbox')
self.user.followers.add(follower)
no_inbox_follower = models.User.objects.create_user(
'hamster', 'hamster@mouse.mouse', 'hamword',
shared_inbox=None, local=False,
remote_id='http://example.com/u/2',
outbox='http://example.com/u/2/o',
inbox='http://example.com/u/2/inbox')
self.user.followers.add(no_inbox_follower)
non_bw_follower = models.User.objects.create_user(
'gerbil', 'gerb@mouse.mouse', 'gerbword',
remote_id='http://example.com/u/3',
outbox='http://example2.com/u/3/o',
inbox='http://example2.com/u/3/inbox',
shared_inbox='http://example2.com/inbox',
bookwyrm_user=False, local=False)
self.user.followers.add(non_bw_follower)
'mouse', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
local_follower = models.User.objects.create_user(
'joe', 'joe@mouse.mouse', 'jeoword')
'joe', 'joe@mouse.mouse', 'jeoword',
local=True, localname='joe')
self.user.followers.add(local_follower)
models.User.objects.create_user(
'nutria', 'nutria@mouse.mouse', 'nuword',
remote_id='http://example.com/u/4',
outbox='http://example.com/u/4/o',
shared_inbox='http://example.com/inbox',
inbox='http://example.com/u/4/inbox',
local=False)
with patch('bookwyrm.models.user.set_remote_server.delay'):
follower = models.User.objects.create_user(
'rat', 'rat@mouse.mouse', 'ratword', local=False,
remote_id='http://example.com/u/1',
outbox='http://example.com/u/1/o',
shared_inbox='http://example.com/inbox',
inbox='http://example.com/u/1/inbox')
self.user.followers.add(follower)
no_inbox_follower = models.User.objects.create_user(
'hamster', 'hamster@mouse.mouse', 'hamword',
shared_inbox=None, local=False,
remote_id='http://example.com/u/2',
outbox='http://example.com/u/2/o',
inbox='http://example.com/u/2/inbox')
self.user.followers.add(no_inbox_follower)
non_bw_follower = models.User.objects.create_user(
'gerbil', 'gerb@mouse.mouse', 'gerbword',
remote_id='http://example.com/u/3',
outbox='http://example2.com/u/3/o',
inbox='http://example2.com/u/3/inbox',
shared_inbox='http://example2.com/inbox',
bookwyrm_user=False, local=False)
self.user.followers.add(non_bw_follower)
models.User.objects.create_user(
'nutria', 'nutria@mouse.mouse', 'nuword',
remote_id='http://example.com/u/4',
outbox='http://example.com/u/4/o',
shared_inbox='http://example.com/inbox',
inbox='http://example.com/u/4/inbox',
local=False)
def test_get_public_recipients(self):

View File

@ -0,0 +1,104 @@
''' testing import '''
from collections import namedtuple
import pathlib
from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import goodreads_import, models
from bookwyrm.settings import DOMAIN
class GoodreadsImport(TestCase):
''' importing from goodreads csv '''
def setUp(self):
''' use a test csv '''
datafile = pathlib.Path(__file__).parent.joinpath(
'data/goodreads.csv')
self.csv = open(datafile, 'r')
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'password', local=True)
models.Connector.objects.create(
identifier=DOMAIN,
name='Local',
local=True,
connector_file='self_connector',
base_url='https://%s' % DOMAIN,
books_url='https://%s/book' % DOMAIN,
covers_url='https://%s/images/covers' % DOMAIN,
search_url='https://%s/search?q=' % DOMAIN,
priority=1,
)
def test_create_job(self):
''' creates the import job entry and checks csv '''
import_job = goodreads_import.create_job(
self.user, self.csv, False, 'public')
self.assertEqual(import_job.user, self.user)
self.assertEqual(import_job.include_reviews, False)
self.assertEqual(import_job.privacy, 'public')
import_items = models.ImportItem.objects.filter(job=import_job).all()
self.assertEqual(len(import_items), 3)
self.assertEqual(import_items[0].index, 0)
self.assertEqual(import_items[0].data['Book Id'], '42036538')
self.assertEqual(import_items[1].index, 1)
self.assertEqual(import_items[1].data['Book Id'], '52691223')
self.assertEqual(import_items[2].index, 2)
self.assertEqual(import_items[2].data['Book Id'], '28694510')
def test_create_retry_job(self):
''' trying again with items that didn't import '''
import_job = goodreads_import.create_job(
self.user, self.csv, False, 'unlisted')
import_items = models.ImportItem.objects.filter(
job=import_job
).all()[:2]
retry = goodreads_import.create_retry_job(
self.user, import_job, import_items)
self.assertNotEqual(import_job, retry)
self.assertEqual(retry.user, self.user)
self.assertEqual(retry.include_reviews, False)
self.assertEqual(retry.privacy, 'unlisted')
retry_items = models.ImportItem.objects.filter(job=retry).all()
self.assertEqual(len(retry_items), 2)
self.assertEqual(retry_items[0].index, 0)
self.assertEqual(retry_items[0].data['Book Id'], '42036538')
self.assertEqual(retry_items[1].index, 1)
self.assertEqual(retry_items[1].data['Book Id'], '52691223')
def test_start_import(self):
''' begin loading books '''
import_job = goodreads_import.create_job(
self.user, self.csv, False, 'unlisted')
MockTask = namedtuple('Task', ('id'))
mock_task = MockTask(7)
with patch('bookwyrm.goodreads_import.import_data.delay') as start:
start.return_value = mock_task
goodreads_import.start_import(import_job)
import_job.refresh_from_db()
self.assertEqual(import_job.task_id, '7')
@responses.activate
def test_import_data(self):
''' resolve entry '''
import_job = goodreads_import.create_job(
self.user, self.csv, False, 'unlisted')
book = models.Edition.objects.create(title='Test Book')
with patch(
'bookwyrm.models.import_job.ImportItem.get_book_from_isbn'
) as resolve:
resolve.return_value = book
with patch('bookwyrm.outgoing.handle_imported_book'):
goodreads_import.import_data(import_job.id)
import_item = models.ImportItem.objects.get(job=import_job, index=0)
self.assertEqual(import_item.book.id, book.id)

View File

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

View File

@ -0,0 +1,705 @@
''' sending out activities '''
import csv
import json
import pathlib
from unittest.mock import patch
from django.http import JsonResponse
from django.test import TestCase
from django.test.client import RequestFactory
import responses
from bookwyrm import forms, models, outgoing
from bookwyrm.settings import DOMAIN
# pylint: disable=too-many-public-methods
class Outgoing(TestCase):
''' sends out activities '''
def setUp(self):
''' we'll need some data '''
self.factory = RequestFactory()
with patch('bookwyrm.models.user.set_remote_server'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@email.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
self.local_user = models.User.objects.create_user(
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse',
remote_id='https://example.com/users/mouse',
)
datafile = pathlib.Path(__file__).parent.joinpath(
'data/ap_user.json'
)
self.userdata = json.loads(datafile.read_bytes())
del self.userdata['icon']
work = models.Work.objects.create(title='Test Work')
self.book = models.Edition.objects.create(
title='Example Edition',
remote_id='https://example.com/book/1',
parent_work=work
)
self.shelf = models.Shelf.objects.create(
name='Test Shelf',
identifier='test-shelf',
user=self.local_user
)
def test_outbox(self):
''' returns user's statuses '''
request = self.factory.get('')
result = outgoing.outbox(request, 'mouse')
self.assertIsInstance(result, JsonResponse)
def test_outbox_bad_method(self):
''' can't POST to outbox '''
request = self.factory.post('')
result = outgoing.outbox(request, 'mouse')
self.assertEqual(result.status_code, 405)
def test_outbox_unknown_user(self):
''' should 404 for unknown and remote users '''
request = self.factory.post('')
result = outgoing.outbox(request, 'beepboop')
self.assertEqual(result.status_code, 405)
result = outgoing.outbox(request, 'rat')
self.assertEqual(result.status_code, 405)
def test_outbox_privacy(self):
''' don't show dms et cetera in outbox '''
models.Status.objects.create(
content='PRIVATE!!', user=self.local_user, privacy='direct')
models.Status.objects.create(
content='bffs ONLY', user=self.local_user, privacy='followers')
models.Status.objects.create(
content='unlisted status', user=self.local_user, privacy='unlisted')
models.Status.objects.create(
content='look at this', user=self.local_user, privacy='public')
request = self.factory.get('')
result = outgoing.outbox(request, 'mouse')
self.assertIsInstance(result, JsonResponse)
data = json.loads(result.content)
self.assertEqual(data['type'], 'OrderedCollection')
self.assertEqual(data['totalItems'], 2)
def test_outbox_filter(self):
''' if we only care about reviews, only get reviews '''
models.Review.objects.create(
content='look at this', name='hi', rating=1,
book=self.book, user=self.local_user)
models.Status.objects.create(
content='look at this', user=self.local_user)
request = self.factory.get('', {'type': 'bleh'})
result = outgoing.outbox(request, 'mouse')
self.assertIsInstance(result, JsonResponse)
data = json.loads(result.content)
self.assertEqual(data['type'], 'OrderedCollection')
self.assertEqual(data['totalItems'], 2)
request = self.factory.get('', {'type': 'Review'})
result = outgoing.outbox(request, 'mouse')
self.assertIsInstance(result, JsonResponse)
data = json.loads(result.content)
self.assertEqual(data['type'], 'OrderedCollection')
self.assertEqual(data['totalItems'], 1)
def test_handle_follow(self):
''' send a follow request '''
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_follow(self.local_user, self.remote_user)
rel = models.UserFollowRequest.objects.get()
self.assertEqual(rel.user_subject, self.local_user)
self.assertEqual(rel.user_object, self.remote_user)
self.assertEqual(rel.status, 'follow_request')
def test_handle_unfollow(self):
''' send an unfollow '''
self.remote_user.followers.add(self.local_user)
self.assertEqual(self.remote_user.followers.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_unfollow(self.local_user, self.remote_user)
self.assertEqual(self.remote_user.followers.count(), 0)
def test_handle_accept(self):
''' accept a follow request '''
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
rel_id = rel.id
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_accept(rel)
# request should be deleted
self.assertEqual(
models.UserFollowRequest.objects.filter(id=rel_id).count(), 0
)
# follow relationship should exist
self.assertEqual(self.remote_user.followers.first(), self.local_user)
def test_handle_reject(self):
''' reject a follow request '''
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user,
user_object=self.remote_user
)
rel_id = rel.id
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_reject(rel)
# request should be deleted
self.assertEqual(
models.UserFollowRequest.objects.filter(id=rel_id).count(), 0
)
# follow relationship should not exist
self.assertEqual(
models.UserFollows.objects.filter(id=rel_id).count(), 0
)
def test_existing_user(self):
''' simple database lookup by username '''
result = outgoing.handle_remote_webfinger('@mouse@local.com')
self.assertEqual(result, self.local_user)
result = outgoing.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 '''
username = 'mouse@example.com'
wellknown = {
"subject": "acct:mouse@example.com",
"links": [{
"rel": "self",
"type": "application/activity+json",
"href": "https://example.com/user/mouse"
}]
}
responses.add(
responses.GET,
'https://example.com/.well-known/webfinger?resource=acct:%s' \
% username,
json=wellknown,
status=200)
responses.add(
responses.GET,
'https://example.com/user/mouse',
json=self.userdata,
status=200)
with patch('bookwyrm.models.user.set_remote_server.delay'):
result = outgoing.handle_remote_webfinger('@mouse@example.com')
self.assertIsInstance(result, models.User)
self.assertEqual(result.username, 'mouse@example.com')
def test_handle_shelve(self):
''' shelve a book '''
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_shelve(self.local_user, self.book, self.shelf)
# make sure the book is on the shelf
self.assertEqual(self.shelf.books.get(), self.book)
def test_handle_shelve_to_read(self):
''' special behavior for the to-read shelf '''
shelf = models.Shelf.objects.get(identifier='to-read')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_shelve(self.local_user, self.book, shelf)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
def test_handle_shelve_reading(self):
''' special behavior for the reading shelf '''
shelf = models.Shelf.objects.get(identifier='reading')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_shelve(self.local_user, self.book, shelf)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
def test_handle_shelve_read(self):
''' special behavior for the read shelf '''
shelf = models.Shelf.objects.get(identifier='read')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_shelve(self.local_user, self.book, shelf)
# make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book)
def test_handle_unshelve(self):
''' remove a book from a shelf '''
self.shelf.books.add(self.book)
self.shelf.save()
self.assertEqual(self.shelf.books.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_unshelve(self.local_user, self.book, self.shelf)
self.assertEqual(self.shelf.books.count(), 0)
def test_handle_reading_status_to_read(self):
''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='to-read')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_reading_status(
self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get()
self.assertEqual(status.user, self.local_user)
self.assertEqual(status.mention_books.first(), self.book)
self.assertEqual(status.content, 'wants to read')
def test_handle_reading_status_reading(self):
''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='reading')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_reading_status(
self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get()
self.assertEqual(status.user, self.local_user)
self.assertEqual(status.mention_books.first(), self.book)
self.assertEqual(status.content, 'started reading')
def test_handle_reading_status_read(self):
''' posts shelve activities '''
shelf = self.local_user.shelf_set.get(identifier='read')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_reading_status(
self.local_user, shelf, self.book, 'public')
status = models.GeneratedNote.objects.get()
self.assertEqual(status.user, self.local_user)
self.assertEqual(status.mention_books.first(), self.book)
self.assertEqual(status.content, 'finished reading')
def test_handle_reading_status_other(self):
''' posts shelve activities '''
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_reading_status(
self.local_user, self.shelf, self.book, 'public')
self.assertFalse(models.GeneratedNote.objects.exists())
def test_handle_imported_book(self):
''' goodreads import added a book, this adds related connections '''
shelf = self.local_user.shelf_set.filter(identifier='read').first()
self.assertIsNone(shelf.books.first())
import_job = models.ImportJob.objects.create(user=self.local_user)
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
csv_file = open(datafile, 'r')
for index, entry in enumerate(list(csv.DictReader(csv_file))):
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_imported_book(
self.local_user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
readthrough = models.ReadThrough.objects.get(user=self.local_user)
self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up.
self.assertEqual(readthrough.start_date.year, 2020)
self.assertEqual(readthrough.start_date.month, 10)
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_imported_book_already_shelved(self):
''' goodreads import added a book, this adds related connections '''
shelf = self.local_user.shelf_set.filter(identifier='to-read').first()
models.ShelfBook.objects.create(
shelf=shelf, added_by=self.local_user, book=self.book)
import_job = models.ImportJob.objects.create(user=self.local_user)
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
csv_file = open(datafile, 'r')
for index, entry in enumerate(list(csv.DictReader(csv_file))):
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_imported_book(
self.local_user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
self.assertIsNone(
self.local_user.shelf_set.get(identifier='read').books.first())
readthrough = models.ReadThrough.objects.get(user=self.local_user)
self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date.year, 2020)
self.assertEqual(readthrough.start_date.month, 10)
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_import_twice(self):
''' re-importing books '''
shelf = self.local_user.shelf_set.filter(identifier='read').first()
import_job = models.ImportJob.objects.create(user=self.local_user)
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
csv_file = open(datafile, 'r')
for index, entry in enumerate(list(csv.DictReader(csv_file))):
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_imported_book(
self.local_user, import_item, False, 'public')
outgoing.handle_imported_book(
self.local_user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
readthrough = models.ReadThrough.objects.get(user=self.local_user)
self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up.
self.assertEqual(readthrough.start_date.year, 2020)
self.assertEqual(readthrough.start_date.month, 10)
self.assertEqual(readthrough.start_date.day, 21)
self.assertEqual(readthrough.finish_date.year, 2020)
self.assertEqual(readthrough.finish_date.month, 10)
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_imported_book_review(self):
''' goodreads review import '''
import_job = models.ImportJob.objects.create(user=self.local_user)
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
csv_file = open(datafile, 'r')
entry = list(csv.DictReader(csv_file))[2]
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_imported_book(
self.local_user, import_item, True, 'unlisted')
review = models.Review.objects.get(book=self.book, user=self.local_user)
self.assertEqual(review.content, 'mixed feelings')
self.assertEqual(review.rating, 2)
self.assertEqual(review.published_date.year, 2019)
self.assertEqual(review.published_date.month, 7)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, 'unlisted')
def test_handle_imported_book_reviews_disabled(self):
''' goodreads review import '''
import_job = models.ImportJob.objects.create(user=self.local_user)
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
csv_file = open(datafile, 'r')
entry = list(csv.DictReader(csv_file))[2]
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_imported_book(
self.local_user, import_item, False, 'unlisted')
self.assertFalse(models.Review.objects.filter(
book=self.book, user=self.local_user
).exists())
def test_handle_delete_status(self):
''' marks a status as deleted '''
status = models.Status.objects.create(
user=self.local_user, content='hi')
self.assertFalse(status.deleted)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_delete_status(self.local_user, status)
status.refresh_from_db()
self.assertTrue(status.deleted)
def test_handle_status(self):
''' create a status '''
form = forms.CommentForm({
'content': 'hi',
'user': self.local_user.id,
'book': self.book.id,
'privacy': 'public',
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(self.local_user, form)
status = models.Comment.objects.get()
self.assertEqual(status.content, '<p>hi</p>')
self.assertEqual(status.user, self.local_user)
self.assertEqual(status.book, self.book)
def test_handle_status_reply(self):
''' create a status in reply to an existing status '''
user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'password', local=True)
parent = models.Status.objects.create(
content='parent status', user=self.local_user)
form = forms.ReplyForm({
'content': 'hi',
'user': user.id,
'reply_parent': parent.id,
'privacy': 'public',
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(user, form)
status = models.Status.objects.get(user=user)
self.assertEqual(status.content, '<p>hi</p>')
self.assertEqual(status.user, user)
self.assertEqual(
models.Notification.objects.get().user, self.local_user)
def test_handle_status_mentions(self):
''' @mention a user in a post '''
user = models.User.objects.create_user(
'rat@%s' % DOMAIN, 'rat@rat.com', 'password',
local=True, localname='rat')
form = forms.CommentForm({
'content': 'hi @rat',
'user': self.local_user.id,
'book': self.book.id,
'privacy': 'public',
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(self.local_user, form)
status = models.Status.objects.get()
self.assertEqual(list(status.mention_users.all()), [user])
self.assertEqual(models.Notification.objects.get().user, user)
self.assertEqual(
status.content,
'<p>hi <a href="%s">@rat</a></p>' % user.remote_id)
def test_handle_status_reply_with_mentions(self):
''' reply to a post with an @mention'ed user '''
user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'password',
local=True, localname='rat')
form = forms.CommentForm({
'content': 'hi @rat@example.com',
'user': self.local_user.id,
'book': self.book.id,
'privacy': 'public',
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(self.local_user, form)
status = models.Status.objects.get()
form = forms.ReplyForm({
'content': 'right',
'user': user,
'privacy': 'public',
'reply_parent': status.id
})
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_status(user, form)
reply = models.Status.replies(status).first()
self.assertEqual(reply.content, '<p>right</p>')
self.assertEqual(reply.user, user)
self.assertTrue(self.remote_user in reply.mention_users.all())
self.assertTrue(self.local_user in reply.mention_users.all())
def test_find_mentions(self):
''' detect and look up @ mentions of users '''
user = models.User.objects.create_user(
'nutria@%s' % DOMAIN, 'nutria@nutria.com', 'password',
local=True, localname='nutria')
self.assertEqual(user.username, 'nutria@%s' % DOMAIN)
self.assertEqual(
list(outgoing.find_mentions('@nutria'))[0],
('@nutria', user)
)
self.assertEqual(
list(outgoing.find_mentions('leading text @nutria'))[0],
('@nutria', user)
)
self.assertEqual(
list(outgoing.find_mentions('leading @nutria trailing text'))[0],
('@nutria', user)
)
self.assertEqual(
list(outgoing.find_mentions('@rat@example.com'))[0],
('@rat@example.com', self.remote_user)
)
multiple = list(outgoing.find_mentions('@nutria and @rat@example.com'))
self.assertEqual(multiple[0], ('@nutria', user))
self.assertEqual(multiple[1], ('@rat@example.com', self.remote_user))
with patch('bookwyrm.outgoing.handle_remote_webfinger') as rw:
rw.return_value = self.local_user
self.assertEqual(
list(outgoing.find_mentions('@beep@beep.com'))[0],
('@beep@beep.com', self.local_user)
)
with patch('bookwyrm.outgoing.handle_remote_webfinger') as rw:
rw.return_value = None
self.assertEqual(list(outgoing.find_mentions('@beep@beep.com')), [])
self.assertEqual(
list(outgoing.find_mentions('@nutria@%s' % DOMAIN))[0],
('@nutria@%s' % DOMAIN, user)
)
def test_format_links(self):
''' find and format urls into a tags '''
url = 'http://www.fish.com/'
self.assertEqual(
outgoing.format_links(url),
'<a href="%s">www.fish.com/</a>' % url)
self.assertEqual(
outgoing.format_links('(%s)' % url),
'(<a href="%s">www.fish.com/</a>)' % url)
url = 'https://archive.org/details/dli.granth.72113/page/n25/mode/2up'
self.assertEqual(
outgoing.format_links(url),
'<a href="%s">' \
'archive.org/details/dli.granth.72113/page/n25/mode/2up</a>' \
% url)
url = 'https://openlibrary.org/search' \
'?q=arkady+strugatsky&mode=everything'
self.assertEqual(
outgoing.format_links(url),
'<a href="%s">openlibrary.org/search' \
'?q=arkady+strugatsky&mode=everything</a>' % url)
def test_to_markdown(self):
''' this is mostly handled in other places, but nonetheless '''
text = '_hi_ and http://fish.com is <marquee>rad</marquee>'
result = outgoing.to_markdown(text)
self.assertEqual(
result,
'<p><em>hi</em> and <a href="http://fish.com">fish.com</a> ' \
'is rad</p>')
def test_handle_favorite(self):
''' create and broadcast faving a status '''
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_favorite(self.remote_user, status)
fav = models.Favorite.objects.get()
self.assertEqual(fav.status, status)
self.assertEqual(fav.user, self.remote_user)
notification = models.Notification.objects.get()
self.assertEqual(notification.notification_type, 'FAVORITE')
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user)
def test_handle_unfavorite(self):
''' unfav a status '''
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_favorite(self.remote_user, status)
self.assertEqual(models.Favorite.objects.count(), 1)
self.assertEqual(models.Notification.objects.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_unfavorite(self.remote_user, status)
self.assertEqual(models.Favorite.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0)
def test_handle_boost(self):
''' boost a status '''
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_boost(self.remote_user, status)
boost = models.Boost.objects.get()
self.assertEqual(boost.boosted_status, status)
self.assertEqual(boost.user, self.remote_user)
self.assertEqual(boost.privacy, 'public')
notification = models.Notification.objects.get()
self.assertEqual(notification.notification_type, 'BOOST')
self.assertEqual(notification.user, self.local_user)
self.assertEqual(notification.related_user, self.remote_user)
self.assertEqual(notification.related_status, status)
def test_handle_boost_unlisted(self):
''' boost a status '''
status = models.Status.objects.create(
user=self.local_user, content='hi', privacy='unlisted')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_boost(self.remote_user, status)
boost = models.Boost.objects.get()
self.assertEqual(boost.privacy, 'unlisted')
def test_handle_boost_private(self):
''' boost a status '''
status = models.Status.objects.create(
user=self.local_user, content='hi', privacy='followers')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_boost(self.remote_user, status)
self.assertFalse(models.Boost.objects.exists())
def test_handle_boost_twice(self):
''' boost a status '''
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_boost(self.remote_user, status)
outgoing.handle_boost(self.remote_user, status)
self.assertEqual(models.Boost.objects.count(), 1)
def test_handle_unboost(self):
''' undo a boost '''
status = models.Status.objects.create(
user=self.local_user, content='hi')
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_boost(self.remote_user, status)
self.assertEqual(models.Boost.objects.count(), 1)
self.assertEqual(models.Notification.objects.count(), 1)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
outgoing.handle_unboost(self.remote_user, status)
self.assertEqual(models.Boost.objects.count(), 0)
self.assertEqual(models.Notification.objects.count(), 0)

View File

@ -1,27 +0,0 @@
import json
import pathlib
from django.test import TestCase
from bookwyrm import models, remote_user
class RemoteUser(TestCase):
''' not too much going on in the books model but here we are '''
def setUp(self):
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',
)
datafile = pathlib.Path(__file__).parent.joinpath(
'data/ap_user.json'
)
self.user_data = json.loads(datafile.read_bytes())
def test_get_remote_user(self):
actor = 'https://example.com/users/rat'
user = remote_user.get_or_create_remote_user(actor)
self.assertEqual(user, self.remote_user)

View File

@ -1,34 +1,36 @@
''' make sure only valid html gets to the app '''
from django.test import TestCase
from bookwyrm.sanitize_html import InputHtmlParser
class Sanitizer(TestCase):
''' sanitizer tests '''
def test_no_html(self):
''' just text '''
input_text = 'no html '
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual(input_text, output)
def test_valid_html(self):
''' leave the html untouched '''
input_text = '<b>yes </b> <i>html</i>'
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual(input_text, output)
def test_valid_html_attrs(self):
''' and don't remove attributes '''
input_text = '<a href="fish.com">yes </a> <i>html</i>'
parser = InputHtmlParser()
parser.feed(input_text)
output = parser.get_output()
self.assertEqual(input_text, output)
def test_invalid_html(self):
''' remove all html when the html is malformed '''
input_text = '<b>yes <i>html</i>'
parser = InputHtmlParser()
parser.feed(input_text)
@ -41,8 +43,8 @@ class Sanitizer(TestCase):
output = parser.get_output()
self.assertEqual('yes html ', output)
def test_disallowed_html(self):
''' remove disallowed html but keep allowed html '''
input_text = '<div> yes <i>html</i></div>'
parser = InputHtmlParser()
parser.feed(input_text)

View File

@ -25,20 +25,26 @@ def get_follow_data(follower, followee):
).serialize()
return json.dumps(follow_activity)
Sender = namedtuple('Sender', ('remote_id', 'private_key', 'public_key'))
KeyPair = namedtuple('KeyPair', ('private_key', 'public_key'))
Sender = namedtuple('Sender', ('remote_id', 'key_pair'))
class Signature(TestCase):
def setUp(self):
self.mouse = User.objects.create_user('mouse', 'mouse@example.com', '')
self.rat = User.objects.create_user('rat', 'rat@example.com', '')
self.cat = User.objects.create_user('cat', 'cat@example.com', '')
self.mouse = User.objects.create_user(
'mouse@%s' % DOMAIN, 'mouse@example.com', '',
local=True, localname='mouse')
self.rat = User.objects.create_user(
'rat@%s' % DOMAIN, 'rat@example.com', '',
local=True, localname='rat')
self.cat = User.objects.create_user(
'cat@%s' % DOMAIN, 'cat@example.com', '',
local=True, localname='cat')
private_key, public_key = create_key_pair()
self.fake_remote = Sender(
'http://localhost/user/remote',
private_key,
public_key,
KeyPair(private_key, public_key)
)
def send(self, signature, now, data, digest):
@ -70,8 +76,9 @@ class Signature(TestCase):
digest = digest or make_digest(data)
signature = make_signature(
signer or sender, self.rat.inbox, now, digest)
with patch('bookwyrm.incoming.handle_follow.delay') as _:
return self.send(signature, now, send_data or data, digest)
with patch('bookwyrm.incoming.handle_follow.delay'):
with patch('bookwyrm.models.user.set_remote_server.delay'):
return self.send(signature, now, send_data or data, digest)
def test_correct_signature(self):
response = self.send_test_request(sender=self.mouse)
@ -89,7 +96,7 @@ class Signature(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath('data/ap_user.json')
data = json.loads(datafile.read_bytes())
data['id'] = self.fake_remote.remote_id
data['publicKey']['publicKeyPem'] = self.fake_remote.public_key
data['publicKey']['publicKeyPem'] = self.fake_remote.key_pair.public_key
del data['icon'] # Avoid having to return an avatar.
responses.add(
responses.GET,
@ -107,7 +114,7 @@ class Signature(TestCase):
status=200
)
with patch('bookwyrm.remote_user.get_remote_reviews.delay') as _:
with patch('bookwyrm.models.user.get_remote_reviews.delay'):
response = self.send_test_request(sender=self.fake_remote)
self.assertEqual(response.status_code, 200)
@ -116,7 +123,7 @@ class Signature(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath('data/ap_user.json')
data = json.loads(datafile.read_bytes())
data['id'] = self.fake_remote.remote_id
data['publicKey']['publicKeyPem'] = self.fake_remote.public_key
data['publicKey']['publicKeyPem'] = self.fake_remote.key_pair.public_key
del data['icon'] # Avoid having to return an avatar.
responses.add(
responses.GET,
@ -127,25 +134,18 @@ class Signature(TestCase):
responses.GET,
'https://localhost/.well-known/nodeinfo',
status=404)
responses.add(
responses.GET,
'https://example.com/user/mouse/outbox?page=true',
json={'orderedItems': []},
status=200
)
# Second and subsequent fetches get a different key:
new_private_key, new_public_key = create_key_pair()
new_sender = Sender(
self.fake_remote.remote_id, new_private_key, new_public_key)
data['publicKey']['publicKeyPem'] = new_public_key
key_pair = KeyPair(*create_key_pair())
new_sender = Sender(self.fake_remote.remote_id, key_pair)
data['publicKey']['publicKeyPem'] = key_pair.public_key
responses.add(
responses.GET,
self.fake_remote.remote_id,
json=data,
status=200)
with patch('bookwyrm.remote_user.get_remote_reviews.delay') as _:
with patch('bookwyrm.models.user.get_remote_reviews.delay'):
# Key correct:
response = self.send_test_request(sender=self.fake_remote)
self.assertEqual(response.status_code, 200)
@ -177,7 +177,7 @@ class Signature(TestCase):
@pytest.mark.integration
def test_changed_data(self):
'''Message data must match the digest header.'''
with patch('bookwyrm.remote_user.fetch_user_data') as _:
with patch('bookwyrm.activitypub.resolve_remote_id'):
response = self.send_test_request(
self.mouse,
send_data=get_follow_data(self.mouse, self.cat))
@ -185,7 +185,7 @@ class Signature(TestCase):
@pytest.mark.integration
def test_invalid_digest(self):
with patch('bookwyrm.remote_user.fetch_user_data') as _:
with patch('bookwyrm.activitypub.resolve_remote_id'):
response = self.send_test_request(
self.mouse,
digest='SHA-256=AAAAAAAAAAAAAAAAAA')
@ -194,7 +194,7 @@ class Signature(TestCase):
@pytest.mark.integration
def test_old_message(self):
'''Old messages should be rejected to prevent replay attacks.'''
with patch('bookwyrm.remote_user.fetch_user_data') as _:
with patch('bookwyrm.activitypub.resolve_remote_id'):
response = self.send_test_request(
self.mouse,
date=http_date(time.time() - 301)

View File

@ -0,0 +1,215 @@
''' style fixes and lookups for templates '''
import re
from unittest.mock import patch
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from django.test import TestCase
from django.utils import timezone
from bookwyrm import models
from bookwyrm.templatetags import bookwyrm_tags
class TemplateTags(TestCase):
''' lotta different things here '''
def setUp(self):
''' create some filler objects '''
self.user = models.User.objects.create_user(
'mouse@example.com', 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse')
with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword',
remote_id='http://example.com/rat', local=False)
self.book = models.Edition.objects.create(title='Test Book')
def test_dict_key(self):
''' just getting a value out of a dict '''
test_dict = {'a': 1, 'b': 3}
self.assertEqual(
bookwyrm_tags.dict_key(test_dict, 'a'), 1)
self.assertEqual(
bookwyrm_tags.dict_key(test_dict, 'c'), 0)
def test_get_rating(self):
''' get a user's most recent rating of a book '''
models.Review.objects.create(
user=self.user, book=self.book, rating=3)
self.assertEqual(
bookwyrm_tags.get_rating(self.book, self.user), 3)
def test_get_rating_doesnt_exist(self):
''' there is no rating available '''
self.assertEqual(
bookwyrm_tags.get_rating(self.book, self.user), 0)
def test_get_user_identifer_local(self):
''' fall back to the simplest uid available '''
self.assertNotEqual(self.user.username, self.user.localname)
self.assertEqual(
bookwyrm_tags.get_user_identifier(self.user), 'mouse')
def test_get_user_identifer_remote(self):
''' for a remote user, should be their full username '''
self.assertEqual(
bookwyrm_tags.get_user_identifier(self.remote_user),
'rat@example.com')
def test_get_notification_count(self):
''' just countin' '''
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0)
models.Notification.objects.create(
user=self.user, notification_type='FOLLOW')
models.Notification.objects.create(
user=self.user, notification_type='FOLLOW')
models.Notification.objects.create(
user=self.remote_user, notification_type='FOLLOW')
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 2)
def test_get_replies(self):
''' direct replies to a status '''
parent = models.Review.objects.create(
user=self.user, book=self.book)
first_child = models.Status.objects.create(
reply_parent=parent, user=self.user)
second_child = models.Status.objects.create(
reply_parent=parent, user=self.user)
third_child = models.Status.objects.create(
reply_parent=parent, user=self.user, deleted=True)
replies = bookwyrm_tags.get_replies(parent)
self.assertEqual(len(replies), 2)
self.assertTrue(first_child in replies)
self.assertTrue(second_child in replies)
self.assertFalse(third_child in replies)
def test_get_parent(self):
''' get the reply parent of a status '''
parent = models.Review.objects.create(
user=self.user, book=self.book)
child = models.Status.objects.create(
reply_parent=parent, user=self.user)
result = bookwyrm_tags.get_parent(child)
self.assertEqual(result, parent)
self.assertIsInstance(result, models.Review)
def test_get_user_liked(self):
''' did a user like a status '''
status = models.Review.objects.create(
user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status))
models.Favorite.objects.create(
user=self.user,
status=status
)
self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status))
def test_get_user_boosted(self):
''' did a user boost a status '''
status = models.Review.objects.create(
user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status))
models.Boost.objects.create(
user=self.user,
boosted_status=status
)
self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status))
def test_follow_request_exists(self):
''' does a user want to follow '''
self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user))
models.UserFollowRequest.objects.create(
user_subject=self.user,
user_object=self.remote_user)
self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user))
self.assertTrue(
bookwyrm_tags.follow_request_exists(self.remote_user, self.user))
def test_get_boosted(self):
''' load a boosted status '''
status = models.Review.objects.create(
user=self.remote_user, book=self.book)
boost = models.Boost.objects.create(
user=self.user,
boosted_status=status
)
boosted = bookwyrm_tags.get_boosted(boost)
self.assertIsInstance(boosted, models.Review)
self.assertEqual(boosted, status)
def test_get_book_description(self):
''' grab it from the edition or the parent '''
work = models.Work.objects.create(title='Test Work')
self.book.parent_work = work
self.book.save()
self.assertIsNone(bookwyrm_tags.get_book_description(self.book))
work.description = 'hi'
work.save()
self.assertEqual(bookwyrm_tags.get_book_description(self.book), 'hi')
self.book.description = 'hello'
self.book.save()
self.assertEqual(bookwyrm_tags.get_book_description(self.book), 'hello')
def test_get_uuid(self):
''' uuid functionality '''
uuid = bookwyrm_tags.get_uuid('hi')
self.assertTrue(re.match(r'hi[A-Za-z0-9\-]', uuid))
def test_time_since(self):
''' ultraconcise timestamps '''
self.assertEqual(bookwyrm_tags.time_since('bleh'), '')
now = timezone.now()
self.assertEqual(bookwyrm_tags.time_since(now), '0s')
seconds_ago = now - relativedelta(seconds=4)
self.assertEqual(bookwyrm_tags.time_since(seconds_ago), '4s')
minutes_ago = now - relativedelta(minutes=8)
self.assertEqual(bookwyrm_tags.time_since(minutes_ago), '8m')
hours_ago = now - relativedelta(hours=9)
self.assertEqual(bookwyrm_tags.time_since(hours_ago), '9h')
days_ago = now - relativedelta(days=3)
self.assertEqual(bookwyrm_tags.time_since(days_ago), '3d')
# I am not going to figure out how to mock dates tonight.
months_ago = now - relativedelta(months=5)
self.assertTrue(re.match(
r'[A-Z][a-z]{2} \d?\d',
bookwyrm_tags.time_since(months_ago)
))
years_ago = now - relativedelta(years=10)
self.assertTrue(re.match(
r'[A-Z][a-z]{2} \d?\d \d{4}',
bookwyrm_tags.time_since(years_ago)
))

View File

@ -0,0 +1,534 @@
''' test for app action functionality '''
from unittest.mock import patch
import dateutil
from django.core.exceptions import PermissionDenied
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.http.response import Http404
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import timezone
from bookwyrm import forms, models, view_actions as actions
from bookwyrm.settings import DOMAIN
#pylint: disable=too-many-public-methods
class ViewActions(TestCase):
''' a lot here: all handlers for receiving activitypub requests '''
def setUp(self):
''' we need basic things, like users '''
self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'https://example.com/user/mouse'
self.local_user.save()
self.group = Group.objects.create(name='editor')
self.group.permissions.add(
Permission.objects.create(
name='edit_book',
codename='edit_book',
content_type=ContentType.objects.get_for_model(models.User)).id
)
with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
self.status = models.Status.objects.create(
user=self.local_user,
content='Test status',
remote_id='https://example.com/status/1',
)
self.work = models.Work.objects.create(title='Test Work')
self.book = models.Edition.objects.create(
title='Test Book', parent_work=self.work)
self.settings = models.SiteSettings.objects.create(id=1)
self.factory = RequestFactory()
def test_register(self):
''' create a user '''
self.assertEqual(models.User.objects.count(), 2)
request = self.factory.post(
'register/',
{
'localname': 'nutria-user.user_nutria',
'password': 'mouseword',
'email': 'aa@bb.cccc'
})
with patch('bookwyrm.view_actions.login'):
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 3)
self.assertEqual(response.status_code, 302)
nutria = models.User.objects.last()
self.assertEqual(nutria.username, 'nutria-user.user_nutria@%s' % DOMAIN)
self.assertEqual(nutria.localname, 'nutria-user.user_nutria')
self.assertEqual(nutria.local, True)
def test_register_trailing_space(self):
''' django handles this so weirdly '''
request = self.factory.post(
'register/',
{
'localname': 'nutria ',
'password': 'mouseword',
'email': 'aa@bb.ccc'
})
with patch('bookwyrm.view_actions.login'):
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 3)
self.assertEqual(response.status_code, 302)
nutria = models.User.objects.last()
self.assertEqual(nutria.username, 'nutria@%s' % DOMAIN)
self.assertEqual(nutria.localname, 'nutria')
self.assertEqual(nutria.local, True)
def test_register_invalid_email(self):
''' gotta have an email '''
self.assertEqual(models.User.objects.count(), 2)
request = self.factory.post(
'register/',
{
'localname': 'nutria',
'password': 'mouseword',
'email': 'aa'
})
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 2)
self.assertEqual(response.template_name, 'login.html')
def test_register_invalid_username(self):
''' gotta have an email '''
self.assertEqual(models.User.objects.count(), 2)
request = self.factory.post(
'register/',
{
'localname': 'nut@ria',
'password': 'mouseword',
'email': 'aa@bb.ccc'
})
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 2)
self.assertEqual(response.template_name, 'login.html')
request = self.factory.post(
'register/',
{
'localname': 'nutr ia',
'password': 'mouseword',
'email': 'aa@bb.ccc'
})
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 2)
self.assertEqual(response.template_name, 'login.html')
request = self.factory.post(
'register/',
{
'localname': 'nut@ria',
'password': 'mouseword',
'email': 'aa@bb.ccc'
})
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 2)
self.assertEqual(response.template_name, 'login.html')
def test_register_closed_instance(self):
''' you can't just register '''
self.settings.allow_registration = False
self.settings.save()
request = self.factory.post(
'register/',
{
'localname': 'nutria ',
'password': 'mouseword',
'email': 'aa@bb.ccc'
})
with self.assertRaises(PermissionDenied):
actions.register(request)
def test_register_invite(self):
''' you can't just register '''
self.settings.allow_registration = False
self.settings.save()
models.SiteInvite.objects.create(
code='testcode', user=self.local_user, use_limit=1)
self.assertEqual(models.SiteInvite.objects.get().times_used, 0)
request = self.factory.post(
'register/',
{
'localname': 'nutria',
'password': 'mouseword',
'email': 'aa@bb.ccc',
'invite_code': 'testcode'
})
with patch('bookwyrm.view_actions.login'):
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 3)
self.assertEqual(response.status_code, 302)
self.assertEqual(models.SiteInvite.objects.get().times_used, 1)
# invite already used to max capacity
request = self.factory.post(
'register/',
{
'localname': 'nutria2',
'password': 'mouseword',
'email': 'aa@bb.ccc',
'invite_code': 'testcode'
})
with self.assertRaises(PermissionDenied):
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 3)
# bad invite code
request = self.factory.post(
'register/',
{
'localname': 'nutria3',
'password': 'mouseword',
'email': 'aa@bb.ccc',
'invite_code': 'dkfkdjgdfkjgkdfj'
})
with self.assertRaises(Http404):
response = actions.register(request)
self.assertEqual(models.User.objects.count(), 3)
def test_password_reset_request(self):
''' send 'em an email '''
request = self.factory.post('', {'email': 'aa@bb.ccc'})
resp = actions.password_reset_request(request)
self.assertEqual(resp.status_code, 302)
request = self.factory.post(
'', {'email': 'mouse@mouse.com'})
with patch('bookwyrm.emailing.send_email.delay'):
resp = actions.password_reset_request(request)
self.assertEqual(resp.template_name, 'password_reset_request.html')
self.assertEqual(
models.PasswordReset.objects.get().user, self.local_user)
def test_password_reset(self):
''' reset from code '''
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'reset-code': code.code,
'password': 'hi',
'confirm-password': 'hi'
})
with patch('bookwyrm.view_actions.login'):
resp = actions.password_reset(request)
self.assertEqual(resp.status_code, 302)
self.assertFalse(models.PasswordReset.objects.exists())
def test_password_reset_wrong_code(self):
''' reset from code '''
models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'reset-code': 'jhgdkfjgdf',
'password': 'hi',
'confirm-password': 'hi'
})
resp = actions.password_reset(request)
self.assertEqual(resp.template_name, 'password_reset.html')
self.assertTrue(models.PasswordReset.objects.exists())
def test_password_reset_mismatch(self):
''' reset from code '''
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.post('', {
'reset-code': code.code,
'password': 'hi',
'confirm-password': 'hihi'
})
resp = actions.password_reset(request)
self.assertEqual(resp.template_name, 'password_reset.html')
self.assertTrue(models.PasswordReset.objects.exists())
def test_password_change(self):
''' change password '''
password_hash = self.local_user.password
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hi'
})
request.user = self.local_user
with patch('bookwyrm.view_actions.login'):
actions.password_change(request)
self.assertNotEqual(self.local_user.password, password_hash)
def test_password_change_mismatch(self):
''' change password '''
password_hash = self.local_user.password
request = self.factory.post('', {
'password': 'hi',
'confirm-password': 'hihi'
})
request.user = self.local_user
actions.password_change(request)
self.assertEqual(self.local_user.password, password_hash)
def test_edit_user(self):
''' use a form to update a user '''
form = forms.EditUserForm(instance=self.local_user)
form.data['name'] = 'New Name'
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
actions.edit_profile(request)
self.assertEqual(self.local_user.name, 'New Name')
def test_edit_book(self):
''' lets a user edit a book '''
self.local_user.groups.add(self.group)
form = forms.EditionForm(instance=self.book)
form.data['title'] = 'New Title'
form.data['last_edited_by'] = self.local_user.id
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
actions.edit_book(request, self.book.id)
self.book.refresh_from_db()
self.assertEqual(self.book.title, 'New Title')
def test_switch_edition(self):
''' updates user's relationships to a book '''
work = models.Work.objects.create(title='test work')
edition1 = models.Edition.objects.create(
title='first ed', parent_work=work)
edition2 = models.Edition.objects.create(
title='second ed', parent_work=work)
shelf = models.Shelf.objects.create(
name='Test Shelf', user=self.local_user)
shelf.books.add(edition1)
models.ReadThrough.objects.create(
user=self.local_user, book=edition1)
self.assertEqual(models.ShelfBook.objects.get().book, edition1)
self.assertEqual(models.ReadThrough.objects.get().book, edition1)
request = self.factory.post('', {
'edition': edition2.id
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
actions.switch_edition(request)
self.assertEqual(models.ShelfBook.objects.get().book, edition2)
self.assertEqual(models.ReadThrough.objects.get().book, edition2)
def test_edit_author(self):
''' edit an author '''
author = models.Author.objects.create(name='Test Author')
self.local_user.groups.add(self.group)
form = forms.AuthorForm(instance=author)
form.data['name'] = 'New Name'
form.data['last_edited_by'] = self.local_user.id
request = self.factory.post('', form.data)
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
actions.edit_author(request, author.id)
author.refresh_from_db()
self.assertEqual(author.name, 'New Name')
self.assertEqual(author.last_edited_by, self.local_user)
def test_edit_author_non_editor(self):
''' edit an author with invalid post data'''
author = models.Author.objects.create(name='Test Author')
form = forms.AuthorForm(instance=author)
form.data['name'] = 'New Name'
form.data['last_edited_by'] = self.local_user.id
request = self.factory.post('', form.data)
request.user = self.local_user
with self.assertRaises(PermissionDenied):
actions.edit_author(request, author.id)
author.refresh_from_db()
self.assertEqual(author.name, 'Test Author')
def test_edit_author_invalid_form(self):
''' edit an author with invalid post data'''
author = models.Author.objects.create(name='Test Author')
self.local_user.groups.add(self.group)
form = forms.AuthorForm(instance=author)
form.data['name'] = ''
form.data['last_edited_by'] = self.local_user.id
request = self.factory.post('', form.data)
request.user = self.local_user
resp = actions.edit_author(request, author.id)
author.refresh_from_db()
self.assertEqual(author.name, 'Test Author')
self.assertEqual(resp.template_name, 'edit_author.html')
def test_edit_shelf_privacy(self):
''' set name or privacy on shelf '''
shelf = self.local_user.shelf_set.get(identifier='to-read')
self.assertEqual(shelf.privacy, 'public')
request = self.factory.post(
'', {
'privacy': 'unlisted',
'user': self.local_user.id,
'name': 'To Read',
})
request.user = self.local_user
actions.edit_shelf(request, shelf.id)
shelf.refresh_from_db()
self.assertEqual(shelf.privacy, 'unlisted')
def test_edit_shelf_name(self):
''' change the name of an editable shelf '''
shelf = models.Shelf.objects.create(
name='Test Shelf', user=self.local_user)
self.assertEqual(shelf.privacy, 'public')
request = self.factory.post(
'', {
'privacy': 'public',
'user': self.local_user.id,
'name': 'cool name'
})
request.user = self.local_user
actions.edit_shelf(request, shelf.id)
shelf.refresh_from_db()
self.assertEqual(shelf.name, 'cool name')
self.assertEqual(shelf.identifier, 'testshelf-%d' % shelf.id)
def test_edit_shelf_name_not_editable(self):
''' can't change the name of an non-editable shelf '''
shelf = self.local_user.shelf_set.get(identifier='to-read')
self.assertEqual(shelf.privacy, 'public')
request = self.factory.post(
'', {
'privacy': 'public',
'user': self.local_user.id,
'name': 'cool name'
})
request.user = self.local_user
actions.edit_shelf(request, shelf.id)
self.assertEqual(shelf.name, 'To Read')
def test_edit_readthrough(self):
''' adding dates to an ongoing readthrough '''
start = timezone.make_aware(dateutil.parser.parse('2021-01-03'))
readthrough = models.ReadThrough.objects.create(
book=self.book, user=self.local_user, start_date=start)
request = self.factory.post(
'', {
'start_date': '2017-01-01',
'finish_date': '2018-03-07',
'book': '',
'id': readthrough.id,
})
request.user = self.local_user
actions.edit_readthrough(request)
readthrough.refresh_from_db()
self.assertEqual(readthrough.start_date.year, 2017)
self.assertEqual(readthrough.start_date.month, 1)
self.assertEqual(readthrough.start_date.day, 1)
self.assertEqual(readthrough.finish_date.year, 2018)
self.assertEqual(readthrough.finish_date.month, 3)
self.assertEqual(readthrough.finish_date.day, 7)
self.assertEqual(readthrough.book, self.book)
def test_delete_readthrough(self):
''' remove a readthrough '''
readthrough = models.ReadThrough.objects.create(
book=self.book, user=self.local_user)
models.ReadThrough.objects.create(
book=self.book, user=self.local_user)
request = self.factory.post(
'', {
'id': readthrough.id,
})
request.user = self.local_user
actions.delete_readthrough(request)
self.assertFalse(
models.ReadThrough.objects.filter(id=readthrough.id).exists())
def test_create_readthrough(self):
''' adding new read dates '''
request = self.factory.post(
'', {
'start_date': '2017-01-01',
'finish_date': '2018-03-07',
'book': self.book.id,
'id': '',
})
request.user = self.local_user
actions.create_readthrough(request)
readthrough = models.ReadThrough.objects.get()
self.assertEqual(readthrough.start_date.year, 2017)
self.assertEqual(readthrough.start_date.month, 1)
self.assertEqual(readthrough.start_date.day, 1)
self.assertEqual(readthrough.finish_date.year, 2018)
self.assertEqual(readthrough.finish_date.month, 3)
self.assertEqual(readthrough.finish_date.day, 7)
self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.user, self.local_user)
def test_tag(self):
''' add a tag to a book '''
request = self.factory.post(
'', {
'name': 'A Tag!?',
'book': self.book.id,
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
actions.tag(request)
tag = models.Tag.objects.get()
user_tag = models.UserTag.objects.get()
self.assertEqual(tag.name, 'A Tag!?')
self.assertEqual(tag.identifier, 'A+Tag%21%3F')
self.assertEqual(user_tag.user, self.local_user)
self.assertEqual(user_tag.book, self.book)
def test_untag(self):
''' remove a tag from a book '''
tag = models.Tag.objects.create(name='A Tag!?')
models.UserTag.objects.create(
user=self.local_user, book=self.book, tag=tag)
request = self.factory.post(
'', {
'user': self.local_user.id,
'book': self.book.id,
'name': tag.name,
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
actions.untag(request)
self.assertTrue(models.Tag.objects.filter(name='A Tag!?').exists())
self.assertFalse(models.UserTag.objects.exists())

View File

@ -0,0 +1,597 @@
''' test for app action functionality '''
import json
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.http import JsonResponse
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import abstract_connector
from bookwyrm.settings import DOMAIN, USER_AGENT
# pylint: disable=too-many-public-methods
class Views(TestCase):
''' every response to a get request, html or json '''
def setUp(self):
''' we need basic test data and mocks '''
self.factory = RequestFactory()
self.work = models.Work.objects.create(title='Test Work')
self.book = models.Edition.objects.create(
title='Test Book', parent_work=self.work)
models.Connector.objects.create(
identifier='self',
connector_file='self_connector',
local=True
)
self.local_user = models.User.objects.create_user(
'mouse@local.com', 'mouse@mouse.mouse', 'password',
local=True, localname='mouse')
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',
)
def test_get_edition(self):
''' given an edition or a work, returns an edition '''
self.assertEqual(
views.get_edition(self.book.id), self.book)
self.assertEqual(
views.get_edition(self.work.id), self.book)
def test_get_user_from_username(self):
''' works for either localname or username '''
self.assertEqual(
views.get_user_from_username('mouse'), self.local_user)
self.assertEqual(
views.get_user_from_username('mouse@local.com'), self.local_user)
with self.assertRaises(models.User.DoesNotExist):
views.get_user_from_username('mojfse@example.com')
def test_is_api_request(self):
''' should it return html or json '''
request = self.factory.get('/path')
request.headers = {'Accept': 'application/json'}
self.assertTrue(views.is_api_request(request))
request = self.factory.get('/path.json')
request.headers = {'Accept': 'Praise'}
self.assertTrue(views.is_api_request(request))
request = self.factory.get('/path')
request.headers = {'Accept': 'Praise'}
self.assertFalse(views.is_api_request(request))
def test_home_tab(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
result = views.home_tab(request, 'local')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'feed.html')
self.assertEqual(result.status_code, 200)
def test_direct_messages_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
result = views.direct_messages_page(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'direct_messages.html')
self.assertEqual(result.status_code, 200)
def test_get_activity_feed(self):
''' loads statuses '''
rat = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'password', local=True)
public_status = models.Comment.objects.create(
content='public status', book=self.book, user=self.local_user)
direct_status = models.Status.objects.create(
content='direct', user=self.local_user, privacy='direct')
rat_public = models.Status.objects.create(
content='blah blah', user=rat)
rat_unlisted = models.Status.objects.create(
content='blah blah', user=rat, privacy='unlisted')
remote_status = models.Status.objects.create(
content='blah blah', user=self.remote_user)
followers_status = models.Status.objects.create(
content='blah', user=rat, privacy='followers')
rat_mention = models.Status.objects.create(
content='blah blah blah', user=rat, privacy='followers')
rat_mention.mention_users.set([self.local_user])
statuses = views.get_activity_feed(
self.local_user,
['public', 'unlisted', 'followers'],
following_only=True,
queryset=models.Comment.objects
)
self.assertEqual(len(statuses), 1)
self.assertEqual(statuses[0], public_status)
statuses = views.get_activity_feed(
self.local_user,
['public', 'followers'],
local_only=True
)
self.assertEqual(len(statuses), 2)
self.assertEqual(statuses[1], public_status)
self.assertEqual(statuses[0], rat_public)
statuses = views.get_activity_feed(self.local_user, 'direct')
self.assertEqual(len(statuses), 1)
self.assertEqual(statuses[0], direct_status)
statuses = views.get_activity_feed(
self.local_user,
['public', 'followers'],
)
self.assertEqual(len(statuses), 3)
self.assertEqual(statuses[2], public_status)
self.assertEqual(statuses[1], rat_public)
self.assertEqual(statuses[0], remote_status)
statuses = views.get_activity_feed(
self.local_user,
['public', 'unlisted', 'followers'],
following_only=True
)
self.assertEqual(len(statuses), 2)
self.assertEqual(statuses[1], public_status)
self.assertEqual(statuses[0], rat_mention)
rat.followers.add(self.local_user)
statuses = views.get_activity_feed(
self.local_user,
['public', 'unlisted', 'followers'],
following_only=True
)
self.assertEqual(len(statuses), 5)
self.assertEqual(statuses[4], public_status)
self.assertEqual(statuses[3], rat_public)
self.assertEqual(statuses[2], rat_unlisted)
self.assertEqual(statuses[1], followers_status)
self.assertEqual(statuses[0], rat_mention)
def test_search_json_response(self):
''' searches local data only and returns book data in json format '''
# we need a connector for this, sorry
request = self.factory.get('', {'q': 'Test Book'})
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
response = views.search(request)
self.assertIsInstance(response, JsonResponse)
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]['title'], 'Test Book')
self.assertEqual(
data[0]['key'], 'https://%s/book/%d' % (DOMAIN, self.book.id))
def test_search_html_response(self):
''' searches remote connectors '''
class TestConnector(abstract_connector.AbstractMinimalConnector):
''' nothing added here '''
def format_search_result(self, search_result):
pass
def get_or_create_book(self, remote_id):
pass
def parse_search_data(self, data):
pass
models.Connector.objects.create(
identifier='example.com',
connector_file='openlibrary',
base_url='https://example.com',
books_url='https://example.com/books',
covers_url='https://example.com/covers',
search_url='https://example.com/search?q=',
)
connector = TestConnector('example.com')
search_result = abstract_connector.SearchResult(
key='http://www.example.com/book/1',
title='Gideon the Ninth',
author='Tamsyn Muir',
year='2019',
connector=connector
)
request = self.factory.get('', {'q': 'Test Book'})
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
with patch(
'bookwyrm.connectors.connector_manager.search') as manager:
manager.return_value = [search_result]
response = views.search(request)
self.assertIsInstance(response, TemplateResponse)
self.assertEqual(response.template_name, 'search_results.html')
self.assertEqual(
response.context_data['book_results'][0].title, 'Gideon the Ninth')
def test_search_html_response_users(self):
''' searches remote connectors '''
request = self.factory.get('', {'q': 'mouse'})
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
with patch('bookwyrm.connectors.connector_manager.search'):
response = views.search(request)
self.assertIsInstance(response, TemplateResponse)
self.assertEqual(response.template_name, 'search_results.html')
self.assertEqual(
response.context_data['user_results'][0], self.local_user)
def test_import_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
result = views.import_page(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'import.html')
self.assertEqual(result.status_code, 200)
def test_import_status(self):
''' there are so many views, this just makes sure it LOADS '''
import_job = models.ImportJob.objects.create(user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.tasks.app.AsyncResult') as async_result:
async_result.return_value = []
result = views.import_status(request, import_job.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'import_status.html')
self.assertEqual(result.status_code, 200)
def test_login_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = AnonymousUser
result = views.login_page(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'login.html')
self.assertEqual(result.status_code, 200)
request.user = self.local_user
result = views.login_page(request)
self.assertEqual(result.url, '/')
self.assertEqual(result.status_code, 302)
def test_about_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
result = views.about_page(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'about.html')
self.assertEqual(result.status_code, 200)
def test_password_reset_request(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
result = views.password_reset_request(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'password_reset_request.html')
self.assertEqual(result.status_code, 200)
def test_password_reset(self):
''' there are so many views, this just makes sure it LOADS '''
code = models.PasswordReset.objects.create(user=self.local_user)
request = self.factory.get('')
request.user = AnonymousUser
result = views.password_reset(request, code.code)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'password_reset.html')
self.assertEqual(result.status_code, 200)
def test_invite_page(self):
''' there are so many views, this just makes sure it LOADS '''
models.SiteInvite.objects.create(code='hi', user=self.local_user)
request = self.factory.get('')
request.user = AnonymousUser
# why?? this is annoying.
request.user.is_authenticated = False
with patch('bookwyrm.models.site.SiteInvite.valid') as invite:
invite.return_value = True
result = views.invite_page(request, 'hi')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'invite.html')
self.assertEqual(result.status_code, 200)
def test_manage_invites(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
request.user.is_superuser = True
result = views.manage_invites(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'manage_invites.html')
self.assertEqual(result.status_code, 200)
def test_notifications_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
result = views.notifications_page(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'notifications.html')
self.assertEqual(result.status_code, 200)
def test_user_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.user_page(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'user.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.user_page(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_followers_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.followers_page(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'followers.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.followers_page(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_following_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.following_page(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'following.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.following_page(request, 'mouse')
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_status_page(self):
''' there are so many views, this just makes sure it LOADS '''
status = models.Status.objects.create(
content='hi', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.status_page(request, 'mouse', status.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'status.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.status_page(request, 'mouse', status.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_replies_page(self):
''' there are so many views, this just makes sure it LOADS '''
status = models.Status.objects.create(
content='hi', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.replies_page(request, 'mouse', status.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'status.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.replies_page(request, 'mouse', status.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_edit_profile_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
result = views.edit_profile_page(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'edit_user.html')
self.assertEqual(result.status_code, 200)
def test_book_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.book_page(request, self.book.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'book.html')
self.assertEqual(result.status_code, 200)
request = self.factory.get('')
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.book_page(request, self.book.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_edit_book_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
request.user = self.local_user
request.user.is_superuser = True
result = views.edit_book_page(request, self.book.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'edit_book.html')
self.assertEqual(result.status_code, 200)
def test_edit_author_page(self):
''' there are so many views, this just makes sure it LOADS '''
author = models.Author.objects.create(name='Test Author')
request = self.factory.get('')
request.user = self.local_user
request.user.is_superuser = True
result = views.edit_author_page(request, author.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'edit_author.html')
self.assertEqual(result.status_code, 200)
def test_editions_page(self):
''' there are so many views, this just makes sure it LOADS '''
request = self.factory.get('')
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.editions_page(request, self.work.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'editions.html')
self.assertEqual(result.status_code, 200)
request = self.factory.get('')
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.editions_page(request, self.work.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_author_page(self):
''' there are so many views, this just makes sure it LOADS '''
author = models.Author.objects.create(name='Jessica')
request = self.factory.get('')
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.author_page(request, author.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'author.html')
self.assertEqual(result.status_code, 200)
request = self.factory.get('')
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.author_page(request, author.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_tag_page(self):
''' there are so many views, this just makes sure it LOADS '''
tag = models.Tag.objects.create(name='hi there')
models.UserTag.objects.create(
tag=tag, user=self.local_user, book=self.book)
request = self.factory.get('')
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.tag_page(request, tag.identifier)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'tag.html')
self.assertEqual(result.status_code, 200)
request = self.factory.get('')
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.tag_page(request, tag.identifier)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_shelf_page(self):
''' there are so many views, this just makes sure it LOADS '''
shelf = self.local_user.shelf_set.first()
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = False
result = views.shelf_page(
request, self.local_user.username, shelf.identifier)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'shelf.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.shelf_page(
request, self.local_user.username, shelf.identifier)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
request = self.factory.get('/?page=1')
request.user = self.local_user
with patch('bookwyrm.views.is_api_request') as is_api:
is_api.return_value = True
result = views.shelf_page(
request, self.local_user.username, shelf.identifier)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_is_bookwyrm_request(self):
''' checks if a request came from a bookwyrm instance '''
request = self.factory.get('', {'q': 'Test Book'})
self.assertFalse(views.is_bookworm_request(request))
request = self.factory.get(
'', {'q': 'Test Book'},
HTTP_USER_AGENT=\
"http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)"
)
self.assertFalse(views.is_bookworm_request(request))
request = self.factory.get(
'', {'q': 'Test Book'}, HTTP_USER_AGENT=USER_AGENT)
self.assertTrue(views.is_bookworm_request(request))