delivery: switch from threads to tasks, one per send

for #652
pull/706/head
Ryan Barrett 2023-10-31 12:49:15 -07:00
rodzic 8bbcf2d6bd
commit db646909e2
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
12 zmienionych plików z 282 dodań i 208 usunięć

Wyświetl plik

@ -194,7 +194,7 @@ class ActivityPub(User, Protocol):
return actor.get('publicInbox') or actor.get('inbox')
@classmethod
def send(to_cls, obj, url, orig_obj=None, log_data=True):
def send(to_cls, obj, url, orig_obj=None):
"""Delivers an activity to an inbox URL.
If ``obj.recipient_obj`` is set, it's interpreted as the receiving actor
@ -208,7 +208,7 @@ class ActivityPub(User, Protocol):
if not activity.get('actor'):
logger.warning('Outgoing AP activity has no actor!')
return signed_post(url, log_data=log_data, data=activity).ok
return signed_post(url, data=activity).ok
@classmethod
def fetch(cls, obj, **kwargs):
@ -447,7 +447,7 @@ def signed_post(url, **kwargs):
return signed_request(util.requests_post, url, **kwargs)
def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs):
def signed_request(fn, url, data=None, headers=None, **kwargs):
"""Wraps ``requests.*`` and adds HTTP Signature.
If the current session has a user (ie in ``g.user``), signs with that user's
@ -457,7 +457,6 @@ def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs):
fn (callable): :func:`util.requests_get` or :func:`util.requests_post`
url (str):
data (dict): optional AS2 object
log_data (bool): whether to log full data object
kwargs: passed through to requests
Returns:
@ -473,8 +472,7 @@ def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs):
user = default_signature_user()
if data:
if log_data:
logger.info(f'Sending AS2 object: {json_dumps(data, indent=2)}')
logger.info(f'Sending AS2 object: {json_dumps(data, indent=2)}')
data = json_dumps(data).encode()
headers = {
@ -511,7 +509,7 @@ def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs):
if resp.is_redirect and fn == util.requests_get:
new_url = urljoin(url, resp.headers['Location'])
return signed_request(fn, new_url, data=data, headers=headers,
log_data=log_data, **kwargs)
**kwargs)
type = common.content_type(resp)
if (type and type != 'text/html' and

Wyświetl plik

@ -257,7 +257,7 @@ class ATProto(User, Protocol):
user.put()
@classmethod
def send(to_cls, obj, url, orig_obj=None, log_data=True):
def send(to_cls, obj, url, orig_obj=None):
"""Creates a record if we own its repo.
Creates the repo first if it doesn't exist.
@ -311,11 +311,8 @@ class ATProto(User, Protocol):
ndb.transactional()
def write():
tid = next_tid()
log_msg = f'Storing ATProto app.bsky.feed.post {tid}'
if log_data:
log_msg += ': ' + json_dumps(dag_json.encode(record).decode(),
indent=2)
logger.info(log_msg)
logger.info(f'Storing ATProto app.bsky.feed.post {tid}: ' +
json_dumps(dag_json.encode(record).decode(), indent=2))
repo.apply_writes(
[Write(action=Action.CREATE, collection='app.bsky.feed.post',

Wyświetl plik

@ -14,6 +14,7 @@ from oauth_dropins.webutil import util, webmention
from oauth_dropins.webutil.appengine_config import tasks_client
from oauth_dropins.webutil import appengine_info
from oauth_dropins.webutil.appengine_info import DEBUG
from oauth_dropins.webutil import flask_util
logger = logging.getLogger(__name__)
@ -73,6 +74,7 @@ USER_AGENT = 'Bridgy Fed (https://fed.brid.gy/)'
util.set_user_agent(USER_AGENT)
TASKS_LOCATION = 'us-central1'
RUN_TASKS_INLINE = False # overridden by unit tests
def base64_to_long(x):
@ -264,7 +266,7 @@ def create_task(queue, **params):
assert queue
path = f'/queue/{queue}'
if appengine_info.LOCAL_SERVER:
if RUN_TASKS_INLINE or appengine_info.LOCAL_SERVER:
logger.info(f'Running task inline: {queue} {params}')
from flask_app import app
return app.test_client().post(
@ -283,7 +285,7 @@ def create_task(queue, **params):
'app_engine_http_request': {
'http_method': 'POST',
'relative_uri': path,
'body': urllib.parse.urlencode(params).encode(),
'body': urllib.parse.urlencode(sorted(params.items())).encode(),
'headers': {'Content-Type': 'application/x-www-form-urlencoded'},
},
})

Wyświetl plik

@ -1,16 +1,14 @@
"""Base protocol class and common code."""
from concurrent.futures import ThreadPoolExecutor
import copy
import logging
import threading
from urllib.parse import urljoin
from cachetools import LRUCache
from flask import copy_current_request_context, g, request
from flask import g, request
from google.cloud import ndb
from google.cloud.ndb import OR
from granary import as1
from oauth_dropins.webutil.appengine_config import ndb_client
from oauth_dropins.webutil.flask_util import cloud_tasks_only
from oauth_dropins.webutil import util
from oauth_dropins.webutil.util import json_dumps, json_loads
@ -40,8 +38,6 @@ SUPPORTED_TYPES = (
'video',
)
DELIVER_THREADS = 10
# activity ids that we've already handled and can now ignore.
# used in Protocol.receive
seen_ids = LRUCache(100000)
@ -364,7 +360,7 @@ class Protocol:
return g.user.key
@classmethod
def send(to_cls, obj, url, orig_obj=None, log_data=True):
def send(to_cls, obj, url, orig_obj=None):
"""Sends an outgoing activity.
To be implemented by subclasses.
@ -374,7 +370,6 @@ class Protocol:
url (str): destination URL to send to
orig_obj (models.Object): the "original object" that this object
refers to, eg replies to or reposts or likes
log_data (bool): whether to log full data object
Returns:
bool: True if the activity is sent successfully, False if it is
@ -895,7 +890,7 @@ class Protocol:
if not targets:
obj.status = 'ignored'
obj.put()
error('No targets', status=204)
error('No targets, nothing to do ¯\_(ツ)_/¯', status=204)
sorted_targets = sorted(targets.items(), key=lambda t: t[0].uri)
obj.populate(
@ -904,71 +899,18 @@ class Protocol:
failed=[],
undelivered=[t for t, _ in sorted_targets],
)
obj.put()
logger.info(f'Delivering to: {obj.undelivered}')
errors = [] # stores (target URL, code, body) tuples
# enqueue send task for each targets
for i, (target, orig_obj) in enumerate(sorted_targets):
orig_obj = orig_obj.key.urlsafe() if orig_obj else ''
user = g.user.key.urlsafe() if g.user else ''
common.create_task(queue='send', obj=obj.key.urlsafe(),
url=target.uri, protocol=target.protocol,
orig_obj=orig_obj, user=user)
# deliver to all targets, in parallel, with a thread pool
with ThreadPoolExecutor(max_workers=DELIVER_THREADS,
thread_name_prefix='deliver') as executor:
results = []
log_data = True
for target, orig_obj in sorted_targets:
@copy_current_request_context
def deliver_one(target, orig_obj, g_user, log_data):
"""Runs on a separate thread!
Note that this has to be defined *inside* the loop, once per
target, since @copy_current_request_context copies when the
function is defined, and we need a fresh copy of the request
context for each call/thread.
https://www.kingname.info/2023/01/14/nested-thread-in-flask/
"""
assert target.uri
protocol = PROTOCOLS[target.protocol]
g.user = g_user
with ndb_client.context():
try:
sent = protocol.send(obj, target.uri, orig_obj=orig_obj,
log_data=log_data)
if sent:
obj.add('delivered', target)
obj.remove('undelivered', target)
except BaseException as e:
code, body = util.interpret_http_exception(e)
if not code and not body:
raise
obj.add('failed', target)
obj.remove('undelivered', target)
errors.append((target.uri, code, body))
obj.put()
results.append(executor.submit(deliver_one, target, orig_obj,
g.user, log_data))
log_data = False
# re-raise any exception that were raised
for r in results:
r.result()
# Pass the response status code and body through as our response
if obj.delivered:
ret = 'OK'
obj.status = 'complete'
elif errors:
ret = f'Delivery failed: {errors}', 502
obj.status = 'failed'
else:
ret = r'Nothing to do ¯\_(ツ)_/¯', 204
obj.status = 'ignored'
obj.put()
logger.info(f'Returning {ret}')
return ret
return 'OK', 202
@classmethod
def targets(cls, obj):
@ -1203,9 +1145,9 @@ def receive_task():
Calls :meth:`Protocol.receive` with the form parameters.
Parameters:
obj (google.cloud.ndb.key.Key): :class:`models.Object` to handle
user (google.cloud.ndb.key.Key): :class:`models.User` this activity is on
behalf of. This user will be loaded into ``g.user``
obj (url-safe google.cloud.ndb.key.Key): :class:`models.Object` to handle
user (url-safe google.cloud.ndb.key.Key): :class:`models.User` this
activity is on behalf of. This user will be loaded into ``g.user``
authed_as (str): passed to :meth:`Protocol.receive`
TODO: migrate incoming webmentions and AP inbox deliveries to this. The
@ -1231,3 +1173,73 @@ def receive_task():
except ValueError as e:
logger.warning(e, exc_info=True)
error(e, status=304)
@app.post('/queue/send')
@cloud_tasks_only
def send_task():
"""Task handler for sending an activity to a single specific destination.
Calls :meth:`Protocol.send` with the form parameters.
Parameters:
protocol (str): :class:`Protocol` to send to
url (str): destination URL to send to
obj (url-safe google.cloud.ndb.key.Key): :class:`models.Object` to send
orig_obj (url-safe google.cloud.ndb.key.Key): optional "original object"
:class:`models.Object` that this object refers to, eg replies to or
reposts or likes
user (url-safe google.cloud.ndb.key.Key): :class:`models.User` this
activity is on behalf of. This user will be loaded into ``g.user``
"""
form = request.form.to_dict()
logger.info(f'Params: {list(form.items())}')
# prepare
url = form['url']
protocol = form['protocol']
target = Target(uri=url, protocol=protocol)
obj = ndb.Key(urlsafe=form['obj']).get()
if target not in obj.undelivered and target not in obj.failed:
logger.info(f"{url} not in {obj.key.id()} undelivered or failed, giving up")
return '¯\_(ツ)_/¯', 204
if user_key := form.get('user'):
g.user = ndb.Key(urlsafe=user_key).get()
orig_obj = (ndb.Key(urlsafe=form['orig_obj']).get()
if form.get('orig_obj') else None)
# send
sent = None
try:
sent = PROTOCOLS[protocol].send(obj, url, orig_obj=orig_obj)
except BaseException as e:
code, body = util.interpret_http_exception(e)
if not code and not body:
logger.info(str(e), exc_info=True)
# write results to Object
@ndb.transactional()
def update_object(obj_key):
obj = obj_key.get()
if target in obj.undelivered:
obj.remove('undelivered', target)
if sent is None:
obj.add('failed', target)
else:
if target in obj.failed:
obj.remove('failed', target)
if sent:
obj.add('delivered', target)
if not obj.undelivered:
obj.status = ('complete' if obj.delivered
else 'failed' if obj.failed
else 'ignored')
obj.put()
update_object(obj.key)
return '', 200 if sent else 304

Wyświetl plik

@ -27,3 +27,12 @@ queue:
task_retry_limit: 3
min_backoff_seconds: 120
max_doublings: 3
- name: send
target: default
rate: 5/s
max_concurrent_requests: 5
retry_parameters:
task_retry_limit: 3
min_backoff_seconds: 120
max_doublings: 3

Wyświetl plik

@ -525,7 +525,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post('/ap/web/user.com/inbox', json=reply)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assertEqual(202, got.status_code, got.get_data(as_text=True))
self.assert_req(mock_get, 'https://user.com/post')
convert_id = reply['id'].replace('://', ':/')
@ -555,11 +555,9 @@ class ActivityPubTest(TestCase):
}
got = self.post('/ap/fake:user/inbox', json=reply,
base_url='https://fa.brid.gy/')
self.assertEqual(200, got.status_code)
[(obj, target)] = Fake.sent
self.assertEqual('fake:my-reply#bridgy-fed-create', obj.our_as1['id'])
self.assertEqual('fake:post:target', target)
self.assertEqual(202, got.status_code)
self.assertEqual([('fake:my-reply#bridgy-fed-create', 'fake:post:target')],
Fake.sent)
def test_inbox_reply_to_self_domain(self, *mocks):
self._test_inbox_ignore_reply_to('http://localhost/mas.to', *mocks)
@ -610,7 +608,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post(path, json=NOTE)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assertEqual(202, got.status_code, got.get_data(as_text=True))
expected_obj = {
**as2.to_as1(NOTE_OBJECT),
@ -659,7 +657,7 @@ class ActivityPubTest(TestCase):
repost = copy.deepcopy(REPOST_FULL)
repost['object'] = f'http://localhost/r/{orig_url}'
got = self.post('/user.com/inbox', json=repost)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assertEqual(202, got.status_code, got.get_data(as_text=True))
convert_id = REPOST['id'].replace('://', ':/')
self.assert_req(
@ -705,7 +703,7 @@ class ActivityPubTest(TestCase):
]
got = self.post('/ap/sharedInbox', json=REPOST)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assertEqual(202, got.status_code, got.get_data(as_text=True))
mock_post.assert_not_called() # no webmention
@ -735,7 +733,7 @@ class ActivityPubTest(TestCase):
**LIKE,
'object': 'http://nope.com/post',
})
self.assertEqual(204, got.status_code)
self.assertEqual(202, got.status_code)
self.assert_object('http://mas.to/like#ok',
# no nope.com Web user key since it didn't exist
@ -778,7 +776,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=LIKE)
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
self.assertIn(self.as2_req('https://mas.to/actor'), mock_get.mock_calls)
self.assertIn(self.req('https://user.com/post'), mock_get.mock_calls)
@ -820,7 +818,7 @@ class ActivityPubTest(TestCase):
self.assertEqual(400, got.status_code)
def test_inbox_follow_accept_with_id(self, *mocks):
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, 200, *mocks)
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, *mocks)
follow = {
**FOLLOW_WITH_ACTOR,
@ -846,7 +844,7 @@ class ActivityPubTest(TestCase):
}
accept = copy.deepcopy(ACCEPT)
accept['object']['url'] = 'https://mas.to/users/swentel#followed-https://user.com/'
self._test_inbox_follow_accept(follow, accept, 200, *mocks)
self._test_inbox_follow_accept(follow, accept, *mocks)
follow.update({
'actor': ACTOR,
@ -863,7 +861,7 @@ class ActivityPubTest(TestCase):
object_ids=[FOLLOW['object']])
def test_inbox_follow_accept_shared_inbox(self, *mocks):
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, 200, *mocks,
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, *mocks,
inbox_path='/ap/sharedInbox')
url = 'https://mas.to/users/swentel#followed-user.com'
@ -883,7 +881,7 @@ class ActivityPubTest(TestCase):
requests_response(), # AP Accept
requests.ConnectionError(), # webmention
]
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, 502,
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT,
mock_head, mock_get, mock_post)
url = 'https://mas.to/users/swentel#followed-user.com'
@ -898,9 +896,8 @@ class ActivityPubTest(TestCase):
type='follow',
object_ids=[FOLLOW['object']])
def _test_inbox_follow_accept(self, follow_as2, accept_as2, expected_status,
mock_head, mock_get, mock_post,
inbox_path='/user.com/inbox'):
def _test_inbox_follow_accept(self, follow_as2, accept_as2, mock_head,
mock_get, mock_post, inbox_path='/user.com/inbox'):
# this should makes us make the follower ActivityPub as direct=True
self.user.direct = False
self.user.put()
@ -915,7 +912,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post(inbox_path, json=follow_as2)
self.assertEqual(expected_status, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.as2_req(FOLLOW['actor']),
@ -962,7 +959,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
self.assertEqual(204, got.status_code)
self.assertEqual(202, got.status_code)
follower = Follower.query().get()
self.assert_entities_equal(
@ -994,7 +991,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
# check that the Follower is now inactive
self.assertEqual('inactive', follower.key.get().status)
@ -1014,7 +1011,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
# check that the Follower is now active
self.assertEqual('active', follower.key.get().status)
@ -1028,7 +1025,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
def test_inbox_undo_follow_inactive(self, mock_head, mock_get, mock_post):
mock_head.return_value = requests_response(url='https://user.com/')
@ -1043,7 +1040,7 @@ class ActivityPubTest(TestCase):
status='inactive')
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
self.assertEqual('inactive', follower.key.get().status)
def test_inbox_undo_follow_composite_object(self, mock_head, mock_get, mock_post):
@ -1061,7 +1058,7 @@ class ActivityPubTest(TestCase):
undo_follow = copy.deepcopy(UNDO_FOLLOW_WRAPPED)
undo_follow['object']['object'] = {'id': undo_follow['object']['object']}
got = self.post('/user.com/inbox', json=undo_follow)
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
self.assertEqual('inactive', follower.key.get().status)
def test_inbox_unsupported_type(self, *_):
@ -1307,7 +1304,7 @@ class ActivityPubTest(TestCase):
]
got = self.post('/user.com/inbox', json=LIKE)
self.assertEqual(502, got.status_code)
self.assertEqual(202, got.status_code)
def test_inbox_no_webmention_endpoint(self, mock_head, mock_get, mock_post):
mock_get.side_effect = [
@ -1321,7 +1318,7 @@ class ActivityPubTest(TestCase):
]
got = self.post('/user.com/inbox', json=LIKE)
self.assertEqual(204, got.status_code)
self.assertEqual(202, got.status_code)
self.assert_object('http://mas.to/like#ok',
notify=[self.user.key],

Wyświetl plik

@ -58,6 +58,7 @@ class ATProtoTest(TestCase):
def setUp(self):
super().setUp()
self.storage = DatastoreStorage()
common.RUN_TASKS_INLINE = False
@patch('requests.get', return_value=requests_response(DID_DOC))
def test_put_validates_id(self, mock_get):

Wyświetl plik

@ -21,6 +21,7 @@ from oauth_dropins.webutil import util
from .testutil import Fake, OtherFake, TestCase
from atproto import ATProto
import common
import models
from models import Follower, Object, OBJECT_EXPIRE_AGE, Target, User
import protocol
@ -58,13 +59,14 @@ class UserTest(TestCase):
@patch('requests.post',
return_value=requests_response('OK')) # create DID on PLC
def test_get_or_create_propagate(self, mock_post, mock_create_task):
common.RUN_TASKS_INLINE = False
Fake.fetchable = {
'fake:user': {
**ACTOR_AS,
'image': None, # don't try to fetch as blob
},
}
user = Fake.get_or_create('fake:user', propagate=True)
# check that profile was fetched remotely

Wyświetl plik

@ -14,6 +14,7 @@ from oauth_dropins.webutil.testutil import requests_response
from .testutil import Fake, TestCase, ACTOR, COMMENT, MENTION, NOTE
from activitypub import ActivityPub
import common
from models import Object, Follower, Target
from web import Web
@ -386,6 +387,8 @@ class PagesTest(TestCase):
@patch('requests.post',
return_value=requests_response('OK')) # create DID on PLC
def test_bridge_user(self, mock_post, mock_create_task):
common.RUN_TASKS_INLINE = False
Fake.fetchable = {
'fake:user': {
**ACTOR_AS,

Wyświetl plik

@ -8,6 +8,7 @@ from arroba.tests.testutil import dns_answer
from flask import g
from google.cloud import ndb
from granary import as2
from oauth_dropins.webutil import appengine_info
from oauth_dropins.webutil.flask_util import CLOUD_TASKS_QUEUE_HEADER, NoContent
from oauth_dropins.webutil.testutil import requests_response
import requests
@ -18,6 +19,7 @@ from .testutil import Fake, OtherFake, TestCase
from activitypub import ActivityPub
from app import app
from atproto import ATProto
import common
from models import Follower, Object, PROTOCOLS, Target, User
import protocol
from protocol import Protocol
@ -522,7 +524,7 @@ class ProtocolReceiveTest(TestCase):
'actor': 'fake:user',
'object': post_as1,
}
self.assertEqual('OK', Fake.receive_as1(create_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(create_as1))
self.assert_object('fake:post',
our_as1=post_as1,
@ -538,7 +540,7 @@ class ProtocolReceiveTest(TestCase):
notify=[],
)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'shared:target')], Fake.sent)
def test_create_post_bare_object(self):
self.make_followers()
@ -548,7 +550,7 @@ class ProtocolReceiveTest(TestCase):
'objectType': 'note',
'author': 'fake:user',
}
self.assertEqual('OK', Fake.receive_as1(post_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(post_as1))
self.assert_object('fake:post',
our_as1=post_as1,
@ -572,7 +574,7 @@ class ProtocolReceiveTest(TestCase):
notify=[],
)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'shared:target')], Fake.sent)
def test_create_post_bare_object_existing_failed_create(self):
self.make_followers()
@ -585,7 +587,7 @@ class ProtocolReceiveTest(TestCase):
self.store_object(id='fake:post', our_as1=post_as1)
self.store_object(id='fake:post#bridgy-fed-create', status='failed')
self.assertEqual('OK', Fake.receive_as1(post_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(post_as1))
obj = self.assert_object('fake:post#bridgy-fed-create',
status='complete',
@ -595,7 +597,7 @@ class ProtocolReceiveTest(TestCase):
ignore=['our_as1'],
)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'shared:target')], Fake.sent)
def test_create_post_bare_object_no_existing_create(self):
self.make_followers()
@ -607,7 +609,7 @@ class ProtocolReceiveTest(TestCase):
}
self.store_object(id='fake:post', our_as1=post_as1)
self.assertEqual('OK', Fake.receive_as1(post_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(post_as1))
obj = self.assert_object('fake:post#bridgy-fed-create',
status='complete',
@ -617,7 +619,7 @@ class ProtocolReceiveTest(TestCase):
ignore=['our_as1'],
)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'shared:target')], Fake.sent)
def test_create_post_use_instead(self):
self.make_user('fake:instead', cls=Fake, use_instead=self.user.key, obj_mf2={
@ -636,7 +638,7 @@ class ProtocolReceiveTest(TestCase):
}
obj = self.store_object(id='fake:post', our_as1=post_as1)
self.assertEqual('OK', Fake.receive_as1(post_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(post_as1))
self.assertEqual(1, len(Fake.sent))
self.assertEqual('shared:target', Fake.sent[0][1])
@ -677,7 +679,7 @@ class ProtocolReceiveTest(TestCase):
'actor': 'fake:user',
'object': post_as1,
}
self.assertEqual('OK', Fake.receive_as1(update_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(update_as1))
self.assert_object('fake:post',
our_as1=post_as1,
@ -693,7 +695,7 @@ class ProtocolReceiveTest(TestCase):
notify=[],
)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'shared:target')], Fake.sent)
def test_update_post_bare_object(self):
self.make_followers()
@ -756,7 +758,7 @@ class ProtocolReceiveTest(TestCase):
'actor': 'fake:user',
'object': reply_as1,
}
self.assertEqual('OK', Fake.receive_as1(create_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(create_as1))
self.assert_object('fake:reply',
our_as1=reply_as1,
@ -771,7 +773,7 @@ class ProtocolReceiveTest(TestCase):
notify=[self.bob.key],
)
self.assertEqual([(obj, 'fake:post:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'fake:post:target')], Fake.sent)
def test_create_reply_bare_object(self):
self.make_followers()
@ -787,7 +789,7 @@ class ProtocolReceiveTest(TestCase):
'id': 'fake:post',
'author': 'fake:bob',
}
self.assertEqual('OK', Fake.receive_as1(reply_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(reply_as1))
self.assert_object('fake:reply',
our_as1=reply_as1,
@ -811,7 +813,7 @@ class ProtocolReceiveTest(TestCase):
notify=[self.bob.key],
)
self.assertEqual([(obj, 'fake:post:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'fake:post:target')], Fake.sent)
def test_create_reply_to_self_delivers_to_followers(self):
self.make_followers()
@ -829,18 +831,18 @@ class ProtocolReceiveTest(TestCase):
'id': 'fake:post',
'author': 'fake:user',
}
self.assertEqual('OK', Fake.receive_as1(reply_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(reply_as1))
self.assert_object('fake:reply', our_as1=reply_as1, type='note',
feed=[self.alice.key, self.bob.key, eve.key])
obj = Object.get_by_id(id='fake:reply#bridgy-fed-create')
self.assertEqual([
(obj, 'fake:post:target'),
(obj, 'shared:target'),
(obj.key.id(), 'fake:post:target'),
(obj.key.id(), 'shared:target'),
], Fake.sent)
self.assertEqual([
(obj, 'other:eve:target'),
(obj.key.id(), 'other:eve:target'),
], OtherFake.sent)
def test_update_reply(self):
@ -865,7 +867,7 @@ class ProtocolReceiveTest(TestCase):
'actor': 'fake:user',
'object': reply_as1,
}
self.assertEqual('OK', Fake.receive_as1(update_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(update_as1))
self.assert_object('fake:reply',
our_as1=reply_as1,
@ -879,7 +881,7 @@ class ProtocolReceiveTest(TestCase):
users=[self.user.key, self.alice.key],
notify=[self.bob.key],
)
self.assertEqual([(obj, 'fake:post:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'fake:post:target')], Fake.sent)
def test_repost(self):
self.make_followers()
@ -895,7 +897,7 @@ class ProtocolReceiveTest(TestCase):
'actor': 'fake:user',
'object': 'fake:post',
}
self.assertEqual('OK', Fake.receive_as1(repost_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(repost_as1))
obj = self.assert_object('fake:repost',
status='complete',
@ -914,8 +916,8 @@ class ProtocolReceiveTest(TestCase):
feed=[self.alice.key, self.bob.key],
)
self.assertEqual([
(obj, 'fake:post:target'),
(obj, 'shared:target'),
(obj.key.id(), 'fake:post:target'),
(obj.key.id(), 'shared:target'),
], Fake.sent)
def test_repost_twitter_blocklisted(self):
@ -952,7 +954,7 @@ class ProtocolReceiveTest(TestCase):
'actor': 'fake:user',
'object': 'fake:post',
}
self.assertEqual('OK', Fake.receive_as1(like_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(like_as1))
like_obj = self.assert_object('fake:like',
users=[self.user.key],
@ -963,7 +965,7 @@ class ProtocolReceiveTest(TestCase):
type='like',
object_ids=['fake:post'])
self.assertEqual([(like_obj, 'fake:post:target')], Fake.sent)
self.assertEqual([(like_obj.key.id(), 'fake:post:target')], Fake.sent)
def test_like_no_object_error(self):
with self.assertRaises(BadRequest):
@ -1003,7 +1005,7 @@ class ProtocolReceiveTest(TestCase):
'actor': 'fake:user',
'object': 'fake:post',
}
self.assertEqual('OK', Fake.receive_as1(delete_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(delete_as1))
self.assert_object('fake:post',
our_as1=post_as1,
@ -1020,7 +1022,7 @@ class ProtocolReceiveTest(TestCase):
users=[self.user.key],
notify=[],
)
self.assertEqual([(obj, 'shared:target')], Fake.sent)
self.assertEqual([(obj.key.id(), 'shared:target')], Fake.sent)
def test_delete_no_followers_no_stored_object(self):
delete_as1 = {
@ -1097,7 +1099,7 @@ class ProtocolReceiveTest(TestCase):
}
sent = []
def send(obj, url, orig_obj=None, log_data=True):
def send(obj, url, orig_obj=None):
self.assertEqual(create_as1, obj.as1)
if not sent:
self.assertEqual('target:1', url)
@ -1110,7 +1112,7 @@ class ProtocolReceiveTest(TestCase):
mock_send.side_effect = send
self.assertEqual('OK', Fake.receive_as1(create_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(create_as1))
self.assert_object('fake:post',
our_as1=post_as1,
@ -1167,7 +1169,7 @@ class ProtocolReceiveTest(TestCase):
object_ids=['fake:user'],
)
self.assertEqual([(update_obj, 'shared:target')], Fake.sent)
self.assertEqual([(update_obj.key.id(), 'shared:target')], Fake.sent)
def test_mention_object(self, *mocks):
self.alice.obj.our_as1 = {'id': 'fake:alice', 'objectType': 'person'}
@ -1188,7 +1190,7 @@ class ProtocolReceiveTest(TestCase):
'url': 'fake:bob',
}],
}
self.assertEqual('OK', Fake.receive_as1(mention_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(mention_as1))
self.assert_object('fake:mention',
our_as1=mention_as1,
@ -1211,8 +1213,10 @@ class ProtocolReceiveTest(TestCase):
notify=[self.alice.key, self.bob.key],
)
self.assertEqual([(obj, 'fake:alice:target'), (obj, 'fake:bob:target')],
Fake.sent)
self.assertEqual([
(obj.key.id(), 'fake:alice:target'),
(obj.key.id(), 'fake:bob:target'),
], Fake.sent)
def test_follow(self):
self._test_follow()
@ -1237,7 +1241,7 @@ class ProtocolReceiveTest(TestCase):
'object': 'fake:user',
**extra,
}
self.assertEqual('OK', Fake.receive_as1(follow_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(follow_as1))
user = Fake.get_by_id('fake:user')
follow_obj = self.assert_object('fake:follow',
@ -1269,8 +1273,8 @@ class ProtocolReceiveTest(TestCase):
)
self.assertEqual([
(accept_obj, 'fake:alice:target'),
(follow_obj, 'fake:user:target'),
(accept_obj.key.id(), 'fake:alice:target'),
(follow_obj.key.id(), 'fake:user:target'),
], Fake.sent)
self.assert_entities_equal(
@ -1330,7 +1334,7 @@ class ProtocolReceiveTest(TestCase):
'actor': 'fake:alice',
'object': 'fake:user',
}
self.assertEqual('OK', Fake.receive_as1(stop_as1))
self.assertEqual(('OK', 202), Fake.receive_as1(stop_as1))
stop_obj = self.assert_object('fake:stop-following',
our_as1=stop_as1,
@ -1343,13 +1347,13 @@ class ProtocolReceiveTest(TestCase):
)
self.assertEqual('inactive', follower.key.get().status)
self.assertEqual([(stop_obj, 'fake:user:target')], Fake.sent)
self.assertEqual([(stop_obj.key.id(), 'fake:user:target')], Fake.sent)
def test_stop_following_doesnt_exist(self):
self.user.obj.our_as1 = {'id': 'fake:user'}
self.user.obj.put()
self.assertEqual('OK', Fake.receive_as1({
self.assertEqual(('OK', 202), Fake.receive_as1({
'id': 'fake:stop-following',
'objectType': 'activity',
'verb': 'stop-following',
@ -1358,11 +1362,7 @@ class ProtocolReceiveTest(TestCase):
}))
self.assertEqual(0, Follower.query().count())
self.assertEqual(1, len(Fake.sent))
obj, target = Fake.sent[0]
self.assertEqual('fake:stop-following', obj.key.id())
self.assertEqual('fake:user:target', target)
self.assertEqual([('fake:stop-following', 'fake:user:target')], Fake.sent)
def test_stop_following_inactive(self):
follower = Follower.get_or_create(to=self.user, from_=self.alice,
@ -1371,7 +1371,7 @@ class ProtocolReceiveTest(TestCase):
self.user.obj.our_as1 = {'id': 'fake:user'}
self.user.obj.put()
self.assertEqual('OK', Fake.receive_as1({
self.assertEqual(('OK', 202), Fake.receive_as1({
'id': 'fake:stop-following',
'objectType': 'activity',
'verb': 'stop-following',
@ -1380,11 +1380,7 @@ class ProtocolReceiveTest(TestCase):
}))
self.assertEqual('inactive', follower.key.get().status)
self.assertEqual(1, len(Fake.sent))
obj, target = Fake.sent[0]
self.assertEqual('fake:stop-following', obj.key.id())
self.assertEqual('fake:user:target', target)
self.assertEqual([('fake:stop-following', 'fake:user:target')], Fake.sent)
@skip
def test_receive_from_bridgy_fed_domain_fails(self):
@ -1420,13 +1416,15 @@ class ProtocolReceiveTest(TestCase):
'object': ['other:dan', 'fake:alice'],
}
self.assertEqual('OK', OtherFake.receive_as1(follow_as1))
self.assertEqual(('OK', 202), OtherFake.receive_as1(follow_as1))
self.assertEqual(1, len(OtherFake.sent))
self.assertEqual('accept', OtherFake.sent[0][0].type)
self.assertEqual(
'https://fa.brid.gy/ap/fake:alice/followers#accept-http://x.com/follow',
OtherFake.sent[0][0])
self.assertEqual(1, len(Fake.sent))
self.assertEqual('follow', Fake.sent[0][0].type)
self.assertEqual('http://x.com/follow', Fake.sent[0][0])
followers = Follower.query().fetch()
self.assertEqual(1, len(followers))
@ -1450,13 +1448,12 @@ class ProtocolReceiveTest(TestCase):
with self.assertRaises(NoContent):
Fake.receive_as1(follow_as1)
(bob_obj, bob_target), (eve_obj, eve_target) = Fake.sent
self.assertEqual('https://fa.brid.gy/ap/http://x.com/bob/followers#accept-http://x.com/follow',
bob_obj.key.id())
self.assertEqual('http://x.com/alice:target', bob_target)
self.assertEqual('https://fa.brid.gy/ap/http://x.com/eve/followers#accept-http://x.com/follow',
eve_obj.key.id())
self.assertEqual('http://x.com/alice:target', eve_target)
self.assertEqual([
('https://fa.brid.gy/ap/http://x.com/bob/followers#accept-http://x.com/follow',
'http://x.com/alice:target'),
('https://fa.brid.gy/ap/http://x.com/eve/followers#accept-http://x.com/follow',
'http://x.com/alice:target'),
], Fake.sent)
self.assert_object('http://x.com/follow',
our_as1=follow_as1,
@ -1601,3 +1598,60 @@ class ProtocolReceiveTest(TestCase):
self.assertIn(
"WARNING:protocol:actor fake:user isn't authed user fake:other",
logs.output)
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
def test_post_create_send_tasks(self, mock_create_task):
common.RUN_TASKS_INLINE = False
self.make_followers()
eve = self.make_user('other:eve', cls=OtherFake, obj_id='other:eve')
Follower.get_or_create(to=self.user, from_=eve)
note_as1 = {
'id': 'fake:post',
'objectType': 'note',
'author': 'fake:user',
}
self.assertEqual(('OK', 202), Fake.receive_as1(note_as1))
create_key = Object.get_by_id('fake:post#bridgy-fed-create').key.urlsafe()
self.assert_task(mock_create_task, 'send', '/queue/send', protocol='other',
obj=create_key, orig_obj='', url='other:eve:target',
user=self.user.key.urlsafe())
self.assert_task(mock_create_task, 'send', '/queue/send', protocol='fake',
obj=create_key, orig_obj='', url='shared:target',
user=self.user.key.urlsafe())
self.assertEqual([], OtherFake.sent)
self.assertEqual([], Fake.sent)
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
def test_reply_send_tasks_orig_obj(self, mock_create_task):
common.RUN_TASKS_INLINE = False
reply_as1 = {
'id': 'fake:reply',
'objectType': 'note',
'inReplyTo': 'fake:post',
'author': 'fake:user',
}
Fake.fetchable['fake:post'] = {
'objectType': 'note',
'id': 'fake:post',
'author': 'fake:bob',
}
self.assertEqual(('OK', 202), Fake.receive_as1(reply_as1))
self.assert_object('fake:reply',
our_as1=reply_as1,
type='note',
)
create_key = Object(id='fake:reply#bridgy-fed-create').key.urlsafe()
orig_obj_key = Object(id='fake:post').key.urlsafe()
self.assert_task(mock_create_task, 'send', '/queue/send', protocol='fake',
obj=create_key, orig_obj=orig_obj_key, url='fake:post:target',
user=self.user.key.urlsafe())
self.assertEqual([], OtherFake.sent)
self.assertEqual([], Fake.sent)

Wyświetl plik

@ -16,6 +16,7 @@ from werkzeug.exceptions import BadGateway, BadRequest
from . import testutil
from activitypub import ActivityPub, postprocess_as2
import common
from common import CONTENT_TYPE_HTML
from models import Follower, Object
from web import TASKS_LOCATION, Web
@ -520,6 +521,7 @@ class WebTest(TestCase):
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
def test_make_task(self, mock_create_task, mock_get, mock_post):
common.RUN_TASKS_INLINE = False
mock_get.side_effect = [NOTE, ACTOR]
params = {
@ -700,7 +702,7 @@ class WebTest(TestCase):
'source': 'https://user.com/reply',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/reply'),
@ -745,7 +747,7 @@ class WebTest(TestCase):
'source': 'https://user.com/reply',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
self.assertEqual(1, mock_post.call_count)
args, kwargs = mock_post.call_args
@ -763,7 +765,7 @@ class WebTest(TestCase):
'source': 'https://user.com/repost',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
self.assert_deliveries(mock_post, ['https://mas.to/inbox'], REPOST_AS2,
ignore=['cc'])
@ -793,7 +795,7 @@ class WebTest(TestCase):
'target': 'https://fed.brid.gy/',
'force': '',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
args, kwargs = mock_post.call_args
self.assertEqual(('https://mas.to/inbox',), args)
@ -824,7 +826,7 @@ class WebTest(TestCase):
'source': 'https://user.com/reply',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/reply'),
@ -858,7 +860,7 @@ class WebTest(TestCase):
'source': 'https://user.com/repost',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/repost'),
@ -905,7 +907,7 @@ class WebTest(TestCase):
'source': 'https://user.com/reply',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/reply'),
@ -961,7 +963,7 @@ class WebTest(TestCase):
'source': 'https://user.com/multiple',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
inboxes = ['https://inbox/', 'https://public/inbox', 'https://shared/inbox']
expected = {
@ -997,7 +999,7 @@ class WebTest(TestCase):
'source': 'https://user.com/repost',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
args, kwargs = mock_post.call_args
self.assertEqual(('https://mas.to/inbox',), args)
@ -1024,7 +1026,7 @@ class WebTest(TestCase):
'source': 'https://user.com/repost',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
args, kwargs = mock_post.call_args
self.assertEqual(('https://mas.to/inbox',), args)
@ -1051,7 +1053,7 @@ class WebTest(TestCase):
'source': 'https://user.com/repost',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
repost_mf2 = copy.deepcopy(REPOST_MF2)
repost_mf2['properties']['author'] = ['https://user.com/']
@ -1084,7 +1086,7 @@ class WebTest(TestCase):
'source': 'https://user.com/post',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
inboxes = ['https://inbox/', 'https://public/inbox', 'https://shared/inbox']
self.assert_object('https://user.com/post#bridgy-fed-create',
@ -1121,7 +1123,7 @@ class WebTest(TestCase):
'source': 'https://www.user.com/post',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://www.user.com/post'),
@ -1145,7 +1147,7 @@ class WebTest(TestCase):
'source': 'https://user.com/post',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/post'),
@ -1182,7 +1184,7 @@ class WebTest(TestCase):
'source': 'https://user.com/post',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/post'),
@ -1229,7 +1231,7 @@ class WebTest(TestCase):
'source': 'https://user.com/post',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
self.assertEqual(('https://inbox/',), mock_post.call_args[0])
create = copy.deepcopy(CREATE_AS2)
@ -1247,7 +1249,7 @@ class WebTest(TestCase):
'source': 'https://user.com/follow',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/follow'),
@ -1301,7 +1303,7 @@ class WebTest(TestCase):
'source': 'https://user.com/follow',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
args, kwargs = mock_post.call_args
self.assertEqual(('https://mas.to/inbox',), args)
@ -1333,7 +1335,7 @@ class WebTest(TestCase):
'source': 'https://user.com/follow#2',
'target': 'https://fed.brid.gy/',
})
self.assert_equals(200, got.status_code)
self.assert_equals(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/follow'),
@ -1385,7 +1387,7 @@ class WebTest(TestCase):
'source': 'https://user.com/follow',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/follow'),
@ -1458,7 +1460,7 @@ class WebTest(TestCase):
'source': 'https://user.com/post',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code, got.text)
self.assertEqual(202, got.status_code, got.text)
inboxes = ('https://inbox/', 'https://public/inbox', 'https://shared/inbox')
self.assert_deliveries(mock_post, inboxes, DELETE_AS2)
@ -1514,11 +1516,7 @@ class WebTest(TestCase):
'source': 'https://user.com/follow',
'target': 'https://fed.brid.gy/',
})
body = got.get_data(as_text=True)
self.assertEqual(502, got.status_code, body)
self.assertIn(
'405 Client Error: None for url: https://mas.to/inbox ; abc xyz',
body)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/follow'),
@ -1574,7 +1572,7 @@ class WebTest(TestCase):
'source': 'https://user.com/',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.req('https://user.com/'),
))
@ -1661,7 +1659,7 @@ class WebTest(TestCase):
'source': 'https://user.com/like',
'target': 'https://fed.brid.gy/',
})
self.assertEqual(200, got.status_code)
self.assertEqual(202, got.status_code)
self.assertIn(
"WARNING:models:actor https://user.com/ isn't https://user.com/like's author or actor ['https://eve.com/']",

Wyświetl plik

@ -106,9 +106,9 @@ class Fake(User, protocol.Protocol):
return url.startswith(f'{cls.LABEL}:blocklisted')
@classmethod
def send(cls, obj, url, orig_obj=None, log_data=True):
def send(cls, obj, url, orig_obj=None):
logger.info(f'{cls.__name__}.send {url}')
cls.sent.append((obj, url))
cls.sent.append((obj.key.id(), url))
return True
@classmethod
@ -185,6 +185,7 @@ class TestCase(unittest.TestCase, testutil.Asserts):
appengine_info.APP_ID = 'my-app'
appengine_info.LOCAL_SERVER = False
common.RUN_TASKS_INLINE = True
app.testing = True
cache.clear()
protocol.seen_ids.clear()
@ -477,7 +478,7 @@ class TestCase(unittest.TestCase, testutil.Asserts):
'app_engine_http_request': {
'http_method': 'POST',
'relative_uri': path,
'body': urlencode(params).encode(),
'body': urlencode(sorted(params.items())).encode(),
'headers': {'Content-Type': 'application/x-www-form-urlencoded'},
},
},