2023-03-08 21:10:41 +00:00
""" Common test utility code. """
2023-10-16 17:36:43 +00:00
import contextlib
2017-10-20 14:13:04 +00:00
import copy
2023-04-30 19:20:58 +00:00
from datetime import datetime
import logging
2023-08-31 03:59:37 +00:00
import os
2023-04-20 22:04:00 +00:00
import random
2023-06-07 23:40:31 +00:00
import re
2017-08-24 06:24:47 +00:00
import unittest
2019-12-26 06:20:57 +00:00
from unittest . mock import ANY , call
2023-09-13 21:36:24 +00:00
from urllib . parse import urlencode
2023-06-09 04:21:40 +00:00
import warnings
2017-08-24 06:24:47 +00:00
2024-04-10 22:20:28 +00:00
from arroba import did
2023-05-06 21:37:23 +00:00
import arroba . util
from arroba . util import datetime_to_tid
2023-06-09 04:21:40 +00:00
from bs4 import MarkupResemblesLocatorWarning
2023-04-20 22:04:00 +00:00
import dag_cbor . random
2023-03-27 21:12:06 +00:00
from google . cloud import ndb
2024-06-01 14:07:00 +00:00
from google . cloud . ndb . global_cache import _InProcessGlobalCache
2024-01-01 22:47:03 +00:00
from google . protobuf . timestamp_pb2 import Timestamp
2024-06-03 21:11:23 +00:00
from granary import as1 , as2
2023-02-09 04:22:16 +00:00
from granary . tests . test_as1 import (
2023-07-28 22:49:29 +00:00
ACTOR ,
2023-02-09 04:22:16 +00:00
COMMENT ,
MENTION ,
NOTE ,
)
2023-10-18 04:50:19 +00:00
from oauth_dropins . webutil import flask_util , testutil , util
2019-12-26 06:20:57 +00:00
from oauth_dropins . webutil . appengine_config import ndb_client
2023-09-07 19:01:43 +00:00
from oauth_dropins . webutil import appengine_info
2023-01-07 17:18:11 +00:00
from oauth_dropins . webutil . testutil import requests_response
2019-12-26 06:20:57 +00:00
import requests
2017-08-24 06:24:47 +00:00
2023-07-13 21:19:01 +00:00
# other modules are imported _after_ Fake etc classes is defined so that it's in
2023-05-30 23:36:18 +00:00
# PROTOCOLS when URL routes are registered.
2024-06-04 22:13:53 +00:00
from common import add , global_cache_timeout_policy , long_to_base64 , TASKS_LOCATION
2024-04-21 18:27:23 +00:00
import ids
2023-05-26 23:07:36 +00:00
import models
2023-10-19 20:00:46 +00:00
from models import KEY_BITS , Object , PROTOCOLS , Target , User
2023-03-08 21:10:41 +00:00
import protocol
2024-05-24 03:40:54 +00:00
import router
2023-04-19 00:17:48 +00:00
2023-03-27 21:12:06 +00:00
logger = logging . getLogger ( __name__ )
2023-11-13 21:30:15 +00:00
ATPROTO_KEY = arroba . util . new_key ( 2349823483510 ) # deterministic seed
2023-07-28 22:49:29 +00:00
NOTE = {
* * NOTE ,
# bare string author id
' author ' : ACTOR [ ' id ' ] ,
}
MENTION = {
* * MENTION ,
# author object with just id
' author ' : { ' id ' : ACTOR [ ' id ' ] } ,
2023-10-10 22:43:29 +00:00
' content ' : ' a mention ' ,
2023-07-28 22:49:29 +00:00
}
COMMENT = {
* * COMMENT ,
# full author object
' author ' : {
* * ACTOR ,
' displayName ' : ' Dr. Eve ' ,
} ,
2023-10-10 22:43:29 +00:00
' content ' : ' a comment ' ,
2023-07-28 22:49:29 +00:00
}
2023-03-14 00:23:57 +00:00
2023-05-26 23:07:36 +00:00
class Fake ( User , protocol . Protocol ) :
2023-06-11 15:14:17 +00:00
ABBREV = ' fa '
2024-01-25 03:20:54 +00:00
PHRASE = ' fake-phrase '
2023-10-24 23:09:28 +00:00
CONTENT_TYPE = ' fa/ke '
2024-04-23 23:52:53 +00:00
HAS_COPIES = True
2024-04-29 19:26:54 +00:00
LOGO_HTML = ' <img src= " fake-logo " > '
2023-03-14 00:23:57 +00:00
2023-06-27 16:48:47 +00:00
# maps string ids to dict AS1 objects that can be fetched
fetchable = { }
2023-03-27 21:12:06 +00:00
# in-order list of (Object, str URL)
sent = [ ]
2024-04-21 18:27:23 +00:00
# in-order lists of ids
2023-03-27 21:12:06 +00:00
fetched = [ ]
2024-04-21 18:27:23 +00:00
created_for = [ ]
2023-03-27 21:12:06 +00:00
2023-09-25 22:08:14 +00:00
@ndb.ComputedProperty
2023-09-25 17:27:08 +00:00
def handle ( self ) :
2023-10-27 00:18:01 +00:00
return self . key . id ( ) . replace ( f ' { self . LABEL } : ' , f ' { self . LABEL } :handle: ' )
2023-09-25 17:27:08 +00:00
2023-06-01 01:34:33 +00:00
def web_url ( self ) :
2024-05-29 23:46:16 +00:00
return f ' web: { self . key . id ( ) } '
2023-06-01 01:34:33 +00:00
2024-04-21 18:27:23 +00:00
@classmethod
def create_for ( cls , user ) :
2024-04-21 19:18:12 +00:00
assert not user . get_copy ( cls )
id = user . key . id ( )
2024-05-07 17:54:18 +00:00
logger . info ( f ' { cls . __name__ } .create_for { id } ' )
2024-04-21 19:18:12 +00:00
cls . created_for . append ( id )
add ( user . copies , Target ( uri = ids . translate_user_id ( id = id , from_ = user , to = cls ) ,
protocol = cls . LABEL ) )
2024-04-21 18:27:23 +00:00
2023-06-13 20:17:11 +00:00
@classmethod
def owns_id ( cls , id ) :
2023-10-27 00:18:01 +00:00
if id . startswith ( ' nope ' ) or id == f ' { cls . LABEL } :nope ' :
2023-06-15 17:52:11 +00:00
return False
2023-10-27 00:18:01 +00:00
return ( ( id . startswith ( f ' { cls . LABEL } : ' )
and not id . startswith ( f ' { cls . LABEL } :handle: ' ) )
2023-09-22 21:53:36 +00:00
or id in cls . fetchable )
2023-06-13 20:17:11 +00:00
2023-09-22 21:53:36 +00:00
@classmethod
2024-05-03 22:18:16 +00:00
def owns_handle ( cls , handle , allow_internal = False ) :
2023-10-27 00:18:01 +00:00
return handle . startswith ( f ' { cls . LABEL } :handle: ' )
2023-09-22 19:14:50 +00:00
2023-09-22 20:11:15 +00:00
@classmethod
def handle_to_id ( cls , handle ) :
2023-10-27 00:18:01 +00:00
if handle == f ' { cls . LABEL } :handle:nope ' :
2023-09-23 20:53:17 +00:00
return None
2023-10-27 00:18:01 +00:00
return handle . replace ( f ' { cls . LABEL } :handle: ' , f ' { cls . LABEL } : ' )
2023-09-22 20:11:15 +00:00
2023-09-09 22:11:52 +00:00
@classmethod
2024-04-22 20:24:24 +00:00
def is_blocklisted ( cls , url , allow_internal = False ) :
2023-10-27 00:18:01 +00:00
return url . startswith ( f ' { cls . LABEL } :blocklisted ' )
2023-09-09 22:11:52 +00:00
2023-03-14 00:23:57 +00:00
@classmethod
2024-06-25 20:37:14 +00:00
def send ( to , obj , url , from_user = None , orig_obj = None ) :
logger . info ( f ' { to . __name__ } .send { url } { obj . as1 } ' )
to . sent . append ( ( obj . key . id ( ) , url ) )
from_ = PROTOCOLS . get ( obj . source_protocol )
if ( from_ and from_ != to and to . HAS_COPIES
and obj . type not in ( ' update ' , ' delete ' ) ) :
if obj . type == ' post ' :
obj = Object . get_by_id ( as1 . get_object ( obj . as1 ) [ ' id ' ] )
copy_id = ids . translate_object_id (
id = obj . key . id ( ) , from_ = from_ , to = to )
add ( obj . copies , Target ( uri = copy_id , protocol = to . LABEL ) )
obj . put ( )
2023-06-27 18:39:57 +00:00
return True
2023-03-14 00:23:57 +00:00
@classmethod
2023-06-12 22:50:47 +00:00
def fetch ( cls , obj , * * kwargs ) :
2023-04-03 14:53:15 +00:00
id = obj . key . id ( )
2023-10-18 04:51:20 +00:00
logger . info ( f ' { cls . __name__ } .fetch { id } ' )
2023-03-27 21:12:06 +00:00
cls . fetched . append ( id )
2023-06-27 16:48:47 +00:00
if id in cls . fetchable :
obj . our_as1 = cls . fetchable [ id ]
2023-07-14 19:45:47 +00:00
return True
2023-03-27 21:12:06 +00:00
2023-07-14 19:45:47 +00:00
return False
2023-03-08 21:10:41 +00:00
2023-05-24 04:30:57 +00:00
@classmethod
2024-05-14 22:58:53 +00:00
def _convert ( cls , obj , from_user = None ) :
2023-11-26 04:07:14 +00:00
logger . info ( f ' { cls . __name__ } .convert { obj . key . id ( ) } { from_user } ' )
2023-11-07 22:02:29 +00:00
return cls . translate_ids ( obj . as1 )
2023-05-24 04:30:57 +00:00
2023-06-17 21:12:43 +00:00
@classmethod
def target_for ( cls , obj , shared = False ) :
2024-05-07 17:54:18 +00:00
assert obj . source_protocol in ( cls . LABEL , cls . ABBREV , ' ui ' , None ) , \
obj . source_protocol
2024-06-13 18:26:55 +00:00
return f ' { cls . LABEL } :shared:target ' if shared else f ' { obj . key . id ( ) } :target '
2023-06-17 21:12:43 +00:00
2023-06-30 18:33:03 +00:00
@classmethod
2024-06-03 21:11:23 +00:00
def receive ( cls , obj , authed_as = None , * * kwargs ) :
2023-09-19 02:19:59 +00:00
assert isinstance ( obj , Object )
2024-06-03 21:11:23 +00:00
if not authed_as :
authed_as = as1 . get_owner ( obj . as1 ) or obj . as1 [ ' id ' ]
return super ( ) . receive ( obj = obj , authed_as = authed_as , * * kwargs )
2023-09-19 02:19:59 +00:00
@classmethod
2023-10-16 18:13:38 +00:00
def receive_as1 ( cls , our_as1 , * * kwargs ) :
2023-07-02 05:40:42 +00:00
assert isinstance ( our_as1 , dict )
2024-06-03 21:11:23 +00:00
return cls . receive ( Object ( id = our_as1 [ ' id ' ] , our_as1 = our_as1 ,
source_protocol = cls . LABEL ) ,
* * kwargs )
2023-06-30 18:33:03 +00:00
2023-03-10 23:13:45 +00:00
2023-07-13 21:19:01 +00:00
class OtherFake ( Fake ) :
2023-10-06 21:57:36 +00:00
""" Different class because the same-protocol check special cases Fake. """
2023-10-27 00:18:01 +00:00
LABEL = ABBREV = ' other '
2023-11-07 22:02:29 +00:00
CONTENT_TYPE = ' ot/her '
2023-11-27 22:44:05 +00:00
HAS_FOLLOW_ACCEPTS = True
2023-07-13 21:19:01 +00:00
2023-09-20 04:46:41 +00:00
fetchable = { }
sent = [ ]
fetched = [ ]
2024-04-21 18:27:23 +00:00
created_for = [ ]
2023-09-20 04:46:41 +00:00
2023-10-06 21:57:36 +00:00
@classmethod
def target_for ( cls , obj , shared = False ) :
""" No shared target. """
return f ' { obj . key . id ( ) } :target '
2023-07-13 21:19:01 +00:00
2024-04-17 23:43:10 +00:00
class ExplicitEnableFake ( Fake ) :
LABEL = ABBREV = ' eefake '
CONTENT_TYPE = ' un/known '
fetchable = { }
sent = [ ]
fetched = [ ]
2024-04-21 18:27:23 +00:00
created_for = [ ]
2024-04-17 23:43:10 +00:00
2023-05-30 23:36:18 +00:00
# import other modules that register Flask handlers *after* Fake is defined
2023-05-26 23:07:36 +00:00
models . reset_protocol_properties ( )
2023-05-30 23:36:18 +00:00
import app
2023-10-19 20:00:46 +00:00
import activitypub
2023-05-31 20:17:17 +00:00
from activitypub import ActivityPub , CONNEG_HEADERS_AS2_HTML
2024-05-03 03:22:35 +00:00
import atproto
2023-08-31 20:49:45 +00:00
from atproto import ATProto
2023-05-30 23:36:18 +00:00
import common
2024-06-13 20:54:37 +00:00
from common import (
global_cache ,
LOCAL_DOMAINS ,
memcache ,
OTHER_DOMAINS ,
PRIMARY_DOMAIN ,
PROTOCOL_DOMAINS ,
)
2023-05-30 23:36:18 +00:00
from web import Web
2024-06-01 14:07:00 +00:00
from flask_app import app
2023-05-30 23:36:18 +00:00
2023-10-19 20:00:46 +00:00
# used in TestCase.make_user() to reuse keys across Users since they're
# expensive to generate.
requests . post ( f ' http:// { ndb_client . host } /reset ' )
with ndb_client . context ( ) :
2024-01-06 21:59:31 +00:00
global_user = activitypub . _INSTANCE_ACTOR = Fake . get_or_create ( ' fake:user ' )
2023-10-19 20:00:46 +00:00
2017-10-10 14:42:10 +00:00
class TestCase ( unittest . TestCase , testutil . Asserts ) :
2017-08-24 06:24:47 +00:00
maxDiff = None
def setUp ( self ) :
2021-07-11 23:30:14 +00:00
super ( ) . setUp ( )
2023-03-27 21:12:06 +00:00
2023-09-19 18:15:49 +00:00
appengine_info . APP_ID = ' my-app '
2023-09-07 19:01:43 +00:00
appengine_info . LOCAL_SERVER = False
2023-10-31 19:49:15 +00:00
common . RUN_TASKS_INLINE = True
2021-08-18 14:59:52 +00:00
app . testing = True
2023-03-08 21:10:41 +00:00
protocol . seen_ids . clear ( )
2024-06-26 23:15:32 +00:00
2023-03-11 06:24:58 +00:00
common . webmention_discover . cache . clear ( )
2024-04-10 22:20:28 +00:00
did . resolve_handle . cache . clear ( )
did . resolve_plc . cache . clear ( )
did . resolve_web . cache . clear ( )
2024-06-26 23:15:32 +00:00
ids . web_ap_base_domain . cache . clear ( )
protocol . Protocol . for_id . cache . clear ( )
User . count_followers . cache . clear ( )
2023-02-09 04:22:16 +00:00
2024-04-21 18:27:23 +00:00
for cls in ExplicitEnableFake , Fake , OtherFake :
2023-10-06 21:57:36 +00:00
cls . fetchable = { }
cls . sent = [ ]
cls . fetched = [ ]
2024-04-21 18:27:23 +00:00
cls . created_for = [ ]
2023-03-27 21:12:06 +00:00
2023-04-30 19:20:58 +00:00
# make random test data deterministic
2023-05-06 21:37:23 +00:00
arroba . util . _clockid = 17
2023-04-30 19:20:58 +00:00
random . seed ( 1234567890 )
dag_cbor . random . set_options ( seed = 1234567890 )
2021-08-18 14:59:52 +00:00
self . client = app . test_client ( )
2023-02-09 04:22:16 +00:00
self . client . __enter__ ( )
2019-12-26 06:20:57 +00:00
2024-05-24 03:40:54 +00:00
self . router_client = router . app . test_client ( )
2024-06-13 20:54:37 +00:00
memcache . clear ( )
global_cache . clear ( )
2024-06-23 16:20:22 +00:00
activitypub . WEB_OPT_OUT_DOMAINS = set ( )
2024-06-01 14:07:00 +00:00
2019-12-26 06:20:57 +00:00
# clear datastore
2023-01-24 20:17:24 +00:00
requests . post ( f ' http:// { ndb_client . host } /reset ' )
2024-06-01 14:07:00 +00:00
self . ndb_context = ndb_client . context (
global_cache = _InProcessGlobalCache ( ) ,
2024-06-04 22:13:53 +00:00
global_cache_timeout_policy = global_cache_timeout_policy ,
2024-06-01 14:07:00 +00:00
cache_policy = lambda key : False )
2019-12-26 06:20:57 +00:00
self . ndb_context . __enter__ ( )
2017-08-24 06:24:47 +00:00
2023-01-13 03:43:12 +00:00
util . now = lambda * * kwargs : testutil . NOW
2023-06-16 04:22:20 +00:00
# used in make_user()
self . last_make_user_id = 1
2023-01-13 03:43:12 +00:00
2023-05-23 06:09:36 +00:00
self . app_context = app . app_context ( )
self . app_context . push ( )
self . request_context = app . test_request_context ( ' / ' )
2023-06-15 22:09:03 +00:00
self . request_context . push ( )
2023-05-23 06:09:36 +00:00
2023-06-09 04:21:40 +00:00
# suppress a few warnings
# local/lib/python3.9/site-packages/bs4/__init__.py:435: MarkupResemblesLocatorWarning: The input looks more like a filename than markup. You may want to open this file and pass the filehandle into Beautiful Soup.
warnings . filterwarnings ( ' ignore ' , category = MarkupResemblesLocatorWarning )
2023-08-31 03:59:37 +00:00
# arroba config
os . environ . update ( {
2024-04-26 20:59:04 +00:00
' APPVIEW_HOST ' : ' appview.local ' ,
' BGS_HOST ' : ' bgs.local ' ,
2023-08-31 03:59:37 +00:00
' PDS_HOST ' : ' pds.local ' ,
' PLC_HOST ' : ' plc.local ' ,
2024-04-26 20:59:04 +00:00
' MOD_SERVICE_HOST ' : ' mod.service.local ' ,
' MOD_SERVICE_DID ' : ' did:mod-service ' ,
2023-08-31 03:59:37 +00:00
} )
2024-05-03 03:22:35 +00:00
atproto . appview . address = ' https://appview.local '
2023-08-31 03:59:37 +00:00
2017-08-24 06:24:47 +00:00
def tearDown ( self ) :
2023-05-23 06:09:36 +00:00
self . app_context . pop ( )
2019-12-26 06:20:57 +00:00
self . ndb_context . __exit__ ( None , None , None )
2023-02-09 04:22:16 +00:00
self . client . __exit__ ( None , None , None )
2021-07-11 23:30:14 +00:00
super ( ) . tearDown ( )
2017-10-20 14:13:04 +00:00
2023-06-15 22:09:03 +00:00
# this breaks if it's before super().tearDown(). why?!
self . request_context . pop ( )
2023-06-07 23:40:31 +00:00
def run ( self , result = None ) :
""" Override to hide stdlib and virtualenv lines in tracebacks.
https : / / docs . python . org / 3.9 / library / unittest . html #unittest.TestCase.run
https : / / docs . python . org / 3.9 / library / unittest . html #unittest.TestResult
"""
result = super ( ) . run ( result = result )
def prune ( results ) :
return [
2023-07-10 18:37:40 +00:00
( tc , re . sub ( r ' \ n File " .+/(local|.venv|oauth-dropins|Python.framework)/.+ \ n.+ \ n ' ,
2023-06-07 23:40:31 +00:00
' \n ' , tb ) )
for tc , tb in results ]
result . errors = prune ( result . errors )
result . failures = prune ( result . failures )
return result
2023-10-18 04:50:19 +00:00
def post ( self , url , client = None , * * kwargs ) :
""" Adds Cloud tasks header to ``self.client.post``. """
if client is None :
2024-05-24 03:40:54 +00:00
client = self . router_client if url . startswith ( ' /queue/ ' ) else self . client
2023-10-18 04:50:19 +00:00
kwargs . setdefault ( ' headers ' , { } ) [ flask_util . CLOUD_TASKS_QUEUE_HEADER ] = ' '
return client . post ( url , * * kwargs )
2023-11-15 22:23:08 +00:00
def make_user ( self , id , cls , * * kwargs ) :
2023-03-10 23:13:45 +00:00
""" Reuse RSA key across Users because generating it is expensive. """
2024-06-23 15:38:00 +00:00
obj_as1 = copy . deepcopy ( kwargs . pop ( ' obj_as1 ' , None ) )
obj_as2 = copy . deepcopy ( kwargs . pop ( ' obj_as2 ' , None ) )
obj_bsky = copy . deepcopy ( kwargs . pop ( ' obj_bsky ' , None ) )
obj_mf2 = copy . deepcopy ( kwargs . pop ( ' obj_mf2 ' , None ) )
obj_id = copy . deepcopy ( kwargs . pop ( ' obj_id ' , None ) )
2024-01-13 03:52:49 +00:00
2024-04-11 21:24:18 +00:00
kwargs . setdefault ( ' direct ' , True )
2023-05-30 23:36:18 +00:00
user = cls ( id = id ,
2023-05-26 23:07:36 +00:00
mod = global_user . mod ,
public_exponent = global_user . public_exponent ,
private_exponent = global_user . private_exponent ,
* * kwargs )
2024-05-29 23:18:15 +00:00
user . obj_key = kwargs . pop ( ' obj_key ' , None )
if user . obj_key :
assert not ( obj_as1 or obj_as2 or obj_bsky or obj_mf2 or obj_id )
elif cls != ATProto or obj_bsky :
if not obj_id :
obj_id = ( ( obj_as2 or { } ) . get ( ' id ' )
or util . get_url ( ( obj_mf2 or { } ) , ' properties ' )
or user . profile_id ( ) )
user . obj_key = Object . get_or_create (
2024-06-03 21:11:23 +00:00
id = obj_id , authed_as = obj_id , our_as1 = obj_as1 , as2 = obj_as2 ,
bsky = obj_bsky , mf2 = obj_mf2 , source_protocol = cls . LABEL ) . key
2024-05-29 23:18:15 +00:00
2023-03-10 23:13:45 +00:00
user . put ( )
return user
2023-05-23 06:09:36 +00:00
def add_objects ( self ) :
2023-07-16 21:06:03 +00:00
user = ndb . Key ( Web , ' user.com ' )
2023-06-29 05:46:53 +00:00
# post
2023-07-16 21:06:03 +00:00
self . store_object ( id = ' a ' ,
users = [ user ] ,
notify = [ user ] ,
feed = [ user ] ,
2023-07-28 22:49:29 +00:00
our_as1 = NOTE )
# post with mention
2023-07-16 21:06:03 +00:00
self . store_object ( id = ' b ' ,
2023-07-28 22:49:29 +00:00
notify = [ user ] ,
feed = [ user ] ,
our_as1 = MENTION )
2023-06-29 05:46:53 +00:00
# reply
2023-07-16 21:06:03 +00:00
self . store_object ( id = ' d ' ,
notify = [ user ] ,
feed = [ user ] ,
2023-07-28 22:49:29 +00:00
our_as1 = COMMENT )
2023-06-29 05:46:53 +00:00
# not feed/notif
2023-07-28 22:49:29 +00:00
self . store_object ( id = ' e ' ,
users = [ user ] ,
our_as1 = NOTE )
2023-06-29 05:46:53 +00:00
# deleted
2023-07-16 21:06:03 +00:00
self . store_object ( id = ' f ' ,
notify = [ user ] ,
feed = [ user ] ,
2023-10-10 22:43:29 +00:00
our_as1 = { * * NOTE , ' content ' : ' deleted! ' } ,
2023-07-28 22:49:29 +00:00
deleted = True )
# different domain
nope = ndb . Key ( Web , ' nope.org ' )
self . store_object ( id = ' g ' ,
notify = [ nope ] ,
feed = [ nope ] ,
our_as1 = MENTION )
# actor whose id is in NOTE.author
self . store_object ( id = ACTOR [ ' id ' ] , our_as1 = ACTOR )
2023-06-29 05:46:53 +00:00
@staticmethod
def store_object ( * * kwargs ) :
obj = Object ( * * kwargs )
obj . put ( )
return obj
2023-02-09 04:22:16 +00:00
2023-04-30 19:20:58 +00:00
@staticmethod
def random_keys_and_cids ( num ) :
def tid ( ) :
ms = random . randint ( datetime ( 2020 , 1 , 1 ) . timestamp ( ) * 1000 ,
datetime ( 2024 , 1 , 1 ) . timestamp ( ) * 1000 )
return datetime_to_tid ( datetime . fromtimestamp ( float ( ms ) / 1000 ) )
return [ ( f ' com.example.record/ { tid ( ) } ' , cid )
for cid in dag_cbor . random . rand_cid ( num ) ]
def random_tid ( num ) :
ms = random . randint ( datetime ( 2020 , 1 , 1 ) . timestamp ( ) * 1000 ,
datetime ( 2024 , 1 , 1 ) . timestamp ( ) * 1000 )
tid = datetime_to_tid ( datetime . fromtimestamp ( float ( ms ) / 1000 ) )
return f ' com.example.record/ { tid } '
2023-05-30 23:36:18 +00:00
def get_as2 ( self , * args , * * kwargs ) :
2023-05-31 20:17:17 +00:00
kwargs . setdefault ( ' headers ' , { } ) [ ' Accept ' ] = CONNEG_HEADERS_AS2_HTML
2023-05-30 23:36:18 +00:00
return self . client . get ( * args , * * kwargs )
2023-06-05 20:20:07 +00:00
@classmethod
def req ( cls , url , * * kwargs ) :
2017-10-20 14:13:04 +00:00
""" Returns a mock requests call. """
2022-11-24 16:20:04 +00:00
kwargs . setdefault ( ' headers ' , { } ) . update ( {
' User-Agent ' : util . user_agent ,
} )
2017-10-20 14:13:04 +00:00
kwargs . setdefault ( ' timeout ' , util . HTTP_TIMEOUT )
2019-10-04 04:08:26 +00:00
kwargs . setdefault ( ' stream ' , True )
2017-10-20 14:13:04 +00:00
return call ( url , * * kwargs )
2022-03-17 04:11:09 +00:00
2023-06-05 20:20:07 +00:00
@classmethod
def as2_req ( cls , url , * * kwargs ) :
2023-10-26 23:56:30 +00:00
kwargs . setdefault ( ' data ' , None )
2022-11-24 16:20:04 +00:00
headers = {
2023-01-16 21:00:38 +00:00
' Date ' : ' Sun, 02 Jan 2022 03:04:05 GMT ' ,
2022-11-24 16:20:04 +00:00
' Host ' : util . domain_from_link ( url , minimize = False ) ,
2024-02-27 19:38:00 +00:00
' Content-Type ' : as2 . CONTENT_TYPE_LD_PROFILE ,
2022-11-24 17:39:01 +00:00
' Digest ' : ANY ,
2023-05-31 20:17:17 +00:00
* * CONNEG_HEADERS_AS2_HTML ,
2022-11-24 16:20:04 +00:00
* * kwargs . pop ( ' headers ' , { } ) ,
}
2023-10-26 23:56:30 +00:00
return cls . req ( url , auth = ANY , headers = headers , allow_redirects = False , * * kwargs )
2023-02-07 03:23:25 +00:00
2023-06-05 20:20:07 +00:00
@classmethod
def as2_resp ( cls , obj ) :
2023-01-07 17:18:11 +00:00
return requests_response ( obj , content_type = as2 . CONTENT_TYPE )
2022-03-17 04:11:09 +00:00
def assert_req ( self , mock , url , * * kwargs ) :
""" Checks a mock requests call. """
2022-11-24 17:39:01 +00:00
kwargs . setdefault ( ' headers ' , { } ) . setdefault (
' User-Agent ' , ' Bridgy Fed (https://fed.brid.gy/) ' )
2022-03-17 04:11:09 +00:00
kwargs . setdefault ( ' stream ' , True )
kwargs . setdefault ( ' timeout ' , util . HTTP_TIMEOUT )
mock . assert_any_call ( url , * * kwargs )
2023-01-29 22:13:58 +00:00
2023-03-21 02:17:55 +00:00
def assert_object ( self , id , delivered_protocol = None , * * props ) :
2023-07-07 04:16:04 +00:00
ignore = props . pop ( ' ignore ' , [ ] )
2023-01-29 22:13:58 +00:00
got = Object . get_by_id ( id )
assert got , id
2023-02-01 21:19:41 +00:00
for field in ' delivered ' , ' undelivered ' , ' failed ' :
2023-03-21 02:17:55 +00:00
props [ field ] = [ Target ( uri = uri , protocol = delivered_protocol )
2023-02-01 21:19:41 +00:00
for uri in props . get ( field , [ ] ) ]
2023-07-07 04:16:04 +00:00
if ' our_as1 ' in props :
assert ' as2 ' not in props
assert ' bsky ' not in props
assert ' mf2 ' not in props
ignore . extend ( [ ' as2 ' , ' bsky ' , ' mf2 ' ] )
2023-02-24 03:17:26 +00:00
mf2 = props . get ( ' mf2 ' )
if mf2 and ' items ' in mf2 :
props [ ' mf2 ' ] = mf2 [ ' items ' ] [ 0 ]
2023-10-11 23:35:05 +00:00
# strip @context
if ' as2 ' in props :
props [ ' as2 ' ] . pop ( ' @context ' , None )
for field in ' actor ' , ' object ' :
val = props [ ' as2 ' ] . get ( field )
if isinstance ( val , dict ) :
val . pop ( ' @context ' , None )
2023-04-24 04:13:11 +00:00
type = props . pop ( ' type ' , None )
if type is not None :
self . assertEqual ( type , got . type )
object_ids = props . pop ( ' object_ids ' , None )
if object_ids is not None :
self . assertSetEqual ( set ( object_ids ) , set ( got . object_ids ) )
2023-02-14 05:43:49 +00:00
2023-02-24 03:17:26 +00:00
if expected_as1 := props . pop ( ' as1 ' , None ) :
2023-12-02 22:25:38 +00:00
self . assert_equals ( expected_as1 , got . as1 )
2023-02-24 03:17:26 +00:00
2023-04-02 02:13:51 +00:00
if got . mf2 :
got . mf2 . pop ( ' url ' , None )
2023-06-27 18:39:57 +00:00
for target in got . delivered :
del target . key
2023-01-29 22:13:58 +00:00
self . assert_entities_equal ( Object ( id = id , * * props ) , got ,
2023-07-16 21:06:03 +00:00
ignore = [ ' as1 ' , ' created ' , ' expire ' , ' labels ' ,
2023-06-21 03:59:32 +00:00
' object_ids ' , ' type ' , ' updated '
] + ignore )
2023-06-06 21:50:20 +00:00
return got
2023-04-02 02:13:51 +00:00
2023-05-31 20:17:17 +00:00
def assert_user ( self , cls , id , * * props ) :
got = cls . get_by_id ( id )
assert got , id
2023-06-16 04:22:20 +00:00
obj_as2 = props . pop ( ' obj_as2 ' , None )
if obj_as2 :
2023-10-25 20:23:11 +00:00
self . assert_equals ( obj_as2 , as2 . from_as1 ( got . obj . as1 ) )
2023-06-16 04:22:20 +00:00
# generated, computed, etc
2023-09-25 22:08:14 +00:00
ignore = [ ' created ' , ' mod ' , ' handle ' , ' obj_key ' , ' private_exponent ' ,
' public_exponent ' , ' updated ' ]
2023-06-16 04:22:20 +00:00
for prop in ignore :
assert prop not in props
self . assert_entities_equal ( cls ( id = id , * * props ) , got , ignore = ignore )
2023-05-31 20:17:17 +00:00
if cls != ActivityPub :
assert got . mod
assert got . private_exponent
assert got . public_exponent
2023-06-06 21:50:20 +00:00
return got
2024-05-25 05:16:20 +00:00
def assert_task ( self , mock_create_task , queue , eta_seconds = None , * * params ) :
2024-01-01 22:47:03 +00:00
expected = {
' app_engine_http_request ' : {
' http_method ' : ' POST ' ,
2024-05-25 05:16:20 +00:00
' relative_uri ' : f ' /queue/ { queue } ' ,
2024-01-01 22:47:03 +00:00
' body ' : urlencode ( sorted ( params . items ( ) ) ) . encode ( ) ,
' headers ' : { ' Content-Type ' : ' application/x-www-form-urlencoded ' } ,
} ,
}
if eta_seconds :
expected [ ' schedule_time ' ] = Timestamp ( seconds = int ( eta_seconds ) )
2023-09-13 21:36:24 +00:00
mock_create_task . assert_any_call (
parent = f ' projects/ { appengine_info . APP_ID } /locations/ { TASKS_LOCATION } /queues/ { queue } ' ,
2024-01-01 22:47:03 +00:00
task = expected ,
2023-09-13 21:36:24 +00:00
)
2023-04-02 02:13:51 +00:00
def assert_equals ( self , expected , actual , msg = None , ignore = ( ) , * * kwargs ) :
return super ( ) . assert_equals (
expected , actual , msg = msg , ignore = tuple ( ignore ) + ( ' @context ' , ) , * * kwargs )
2023-10-16 17:36:43 +00:00
@contextlib.contextmanager
def assertLogs ( self ) :
""" Wraps :meth:`unittest.TestCase.assertLogs` and enables/disables logs.
Works around ` ` oauth_dropins . webutil . tests . __init__ ` ` .
"""
orig_disable_level = logging . root . manager . disable
logging . disable ( logging . NOTSET )
try :
with super ( ) . assertLogs ( ) as logs :
yield logs
finally :
# emit logs that were captured
for record in logs . records :
2023-10-16 18:13:38 +00:00
if record . levelno > = orig_disable_level :
logging . root . handle ( record )
logging . disable ( orig_disable_level )