Testing helpers¶
PlatformChecks is for anyone writing a platform of their own. It runs on
pytest, so it wants pip install "socialchimp[testing]" - see
Adding a platform for how it proves a platform
behaves like the built-in ones.
The three doubles below it - FakePlatform, RecordingTransport and
RecordingStorage - need nothing but socialchimp itself. They are for
building an app as much as for testing one, and a program that only uses
those does not want the extra.
PlatformChecks¶
A pytest mixin: inherit from it, point it at your platform, and it runs
the checks every built-in platform passes. Subclassing it without pytest
installed says so, and says what to install.
PlatformChecks
¶
Checks that your platform behaves like the others. Subclass it.
Write one method saying how to build your platform and you inherit every check below:
class TestMyPlatform(PlatformChecks):
def make_platform(self) -> Platform:
return MyPlatform(transport=self.transport)
Your subclass must be called Test..., because that is what pytest
collects. This base is not, on purpose - renaming it would make pytest
run these checks on their own, with no platform to check.
Some checks need an account and something to answer requests. Add
make_connection and make_transport and those run too; leave them out
and they skip with a line saying what to add. A check never fails
because you did not write an optional method.
None of this replaces mypy. These checks run your code; only a type checker looks at what your methods take and return.
transport
property
¶
Hand this to your platform inside make_platform.
It is what make_transport returned, wrapped so these checks can
see the requests. Give your platform something else and the checks
that watch the wire will pass without ever having looked.
make_platform
¶
make_platform() -> Platform
Build the platform to check. Every subclass writes this one.
Returns:
| Type | Description |
|---|---|
Platform
|
Your platform, built and ready to use. |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
Always, until you write it. |
Source code in src/socialchimp/testing.py
make_connection
¶
make_connection() -> Connection | None
Build an account for the checks that need one. Optional.
Return a connection your fake transport will answer for. Without one the checks that publish, ask for limits or read updates skip.
Returns:
| Type | Description |
|---|---|
Connection | None
|
The account to use, or |
Source code in src/socialchimp/testing.py
make_transport
¶
Build something to answer requests. Optional.
RecordingTransport is a reasonable starting point. Whatever you
return is wrapped so the checks can see what went to the wire, so
hand self.transport - not this - to your platform.
Returns:
| Type | Description |
|---|---|
AsyncBaseTransport | None
|
The transport to answer with, or |
AsyncBaseTransport | None
|
watch the wire. |
Source code in src/socialchimp/testing.py
make_post
¶
make_post(text: str) -> Post
Build a post your platform would take, carrying this text. Optional.
The checks that measure length need a post that is right in every
other way, so that its length is the only thing being judged. Left
alone, this is Post(text=...), with a small picture or video
attached for a network that has no text-only post.
Write your own when your network wants more than that. YouTube refuses any video without a title, so there is no post it will look at twice without one:
def make_post(self, text: str) -> Post:
return Post(
text=text,
media=(Media.from_bytes(b"video", filename="a.mp4"),),
options={"title": "A video", "made_for_kids": False},
)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The words the post has to carry, exactly as they are given. The checks count them, so a post that changes them is a post that measures the wrong thing. |
required |
Returns:
| Type | Description |
|---|---|
Post
|
A post your platform would take. |
Source code in src/socialchimp/testing.py
connection_or_skip
¶
connection_or_skip() -> Connection
Return the account to use, or skip the check that asked for it.
Returns:
| Type | Description |
|---|---|
Connection
|
The connection from |
Source code in src/socialchimp/testing.py
requests_or_skip
¶
Return the live list of requests sent, or skip the check.
Returns:
| Type | Description |
|---|---|
list[Request]
|
The requests that have gone through |
list[Request]
|
keeps filling up as more are sent. |
Source code in src/socialchimp/testing.py
test_it_provides_everything_a_platform_must
async
¶
Every method a platform has to have is there, and is async.
Source code in src/socialchimp/testing.py
test_its_name_can_be_an_entry_point_name
async
¶
The name is something a package can register and a person can type.
Source code in src/socialchimp/testing.py
test_it_declares_at_least_one_way_to_post
async
¶
features is a Feature and says it can post something.
Source code in src/socialchimp/testing.py
test_everything_it_claims_in_features_it_can_actually_do
async
¶
Anything listed in features has the method that backs it up.
This is the one that matters most. features is what socialchimp
and your users read before deciding whether to call something, so a
claim with nothing behind it fails at the worst moment, in someone
else's app.
Source code in src/socialchimp/testing.py
test_it_says_where_its_api_lives
async
¶
api_base gives a whole address that a path can be joined onto.
Source code in src/socialchimp/testing.py
test_it_says_what_headers_prove_who_we_are
async
¶
auth_headers gives headers that can go on a request as they are.
Source code in src/socialchimp/testing.py
test_the_details_it_asks_for_can_be_shown_in_a_form
async
¶
A network with no sign-in page asks for things a person can type.
Bluesky takes an app password, Discord and Telegram a bot token. There is nowhere to send anybody, so the platform says what to ask for and the app draws the form. A box with no label on it is a box nobody knows what to put in.
Source code in src/socialchimp/testing.py
test_a_platform_with_no_app_starts_a_login_without_one
async
¶
A network with no app to register signs somebody in without one.
Bluesky has no developer portal and no app: a person signs in with
their handle and an app password they made themselves. That is what
Feature.NEEDS_NO_APP says, and where it is listed socialchimp asks
your storage for nothing and hands the platform a LoginRequest
with app as None.
A platform that lists the flag and then refuses without credentials is the worst of both: nothing to save, and a sign-in that will not start, with a message telling somebody to save credentials that do not exist anywhere.
Source code in src/socialchimp/testing.py
test_a_platform_that_keeps_working_can_be_asked_how_it_is_going
async
¶
A check_state is async def check_state(self, connection, post_id).
YouTube encodes for minutes and TikTok can put a video in somebody's
drafts, so both answer publish while they are still busy.
Account.check_state hands this the connection and the post id, in
that order and by position. A plain def, or one that takes some
other number of things, is a TypeError in somebody else's app rather
than a failure here.
Source code in src/socialchimp/testing.py
test_a_platform_that_pauses_to_ask_can_carry_on
async
¶
A resume_login is async def and takes what it will be given.
Facebook asks which page, YouTube which channel. Both answer
finish_login with ChooseAccount and finish the job in resume_login,
and socialchimp calls that one by name with resume_token, account_id
and remember. A plain def, or arguments under other names, leaves
the person stuck on the page where they picked - and it fails there, in
someone else's app, rather than here.
Source code in src/socialchimp/testing.py
test_its_limits_are_never_zero_for_unknown
async
¶
limits() gives a Limits, and every number is None or positive.
Source code in src/socialchimp/testing.py
test_a_post_over_a_limit_is_refused_before_any_request
async
¶
A post that breaks a declared limit never reaches the network.
Source code in src/socialchimp/testing.py
test_it_counts_text_the_way_it_says_it_does
async
¶
Text is counted the way this platform's Limits says it is.
Hardly any network's "300" means characters. Bluesky counts letters as a person would, Threads counts bytes, TikTok counts an emoji as two. A platform that says which and then counts characters anyway refuses posts the network would have taken, or sends posts it will not - both quietly, and both only once somebody uses an emoji.
Source code in src/socialchimp/testing.py
3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 | |
test_a_text_only_post_is_refused_when_it_cannot_post_text
async
¶
A platform without POST_TEXT turns words away, and says why.
YouTube has no text-only post at all: everything on it is a video,
and its community posts are not in the API. A platform in that
position has to say so plainly, because Post(text="hello") is the
first thing anybody tries.
Source code in src/socialchimp/testing.py
test_scheduling_is_refused_when_it_cannot_schedule
async
¶
A platform without SCHEDULE says so, rather than posting now.
Source code in src/socialchimp/testing.py
test_the_errors_it_raises_are_all_socialchimp_errors
async
¶
Anything it refuses, it refuses with a socialchimp error.
Source code in src/socialchimp/testing.py
test_the_updates_it_reads_come_back_as_updates
async
¶
fetch_updates gives back Updates, each with a real timestamp.
Source code in src/socialchimp/testing.py
Building a fake platform¶
FakePlatform
¶
FakePlatform(
*,
name: str = "fake",
features: Feature = _FAKE_FEATURES,
limits: Limits | None = None,
transport: AsyncBaseTransport | None = None,
accounts: tuple[AccountChoice, ...] = (),
ask_for: tuple[LoginField, ...] = (),
secret: str = _FAKE_SIGNING_KEY,
updates: Sequence[Update] = (),
states: Sequence[PostState] = (),
token_lifetime: timedelta | None = timedelta(hours=1),
avatar_url: str | None = None,
publish_fails_with: SocialChimpError | None = None,
login_fails_with: SocialChimpError | None = None,
answers_setup_checks: bool = True,
page_size: int = _FAKE_PAGE_SIZE,
)
A platform that works without a network, for tests of your own.
It passes every check in PlatformChecks, which is the point: it is
what a well-behaved platform looks like, and it is the thing the checks
themselves are tested against.
Every knob is a way of making it misbehave on purpose, so you can see
what your app does about it. Give it accounts and signing in stops to
ask which one, and carries on once one is picked. Give it
publish_fails_with and every post raises that; login_fails_with and
signing in raises that instead of finishing. Give it
token_lifetime=None and its tokens never expire, the way Mastodon's do
not. Give it avatar_url and every connection it builds - including the
one a finished sign-in hands back - carries that as its picture, and
read_profile hands the same one back, the way Account.profile reads.
Give it a transport and publish really sends a request through
HttpClient, so retries, rate limits and error handling all run. Leave
the transport out and it answers from memory.
Give it ask_for and signing in asks for those instead of sending
anybody anywhere, the way Bluesky's app password and the bot-token
networks work.
It answers Meta's hub.challenge handshake out of the box. Give it
answers_setup_checks=False and it has no answer_setup_check at all,
so it is not a CanAnswerSetupCheck - the way TikTok is, which pushes
without asking anything first.
It also reads back, replies to, likes and messages posts, the same as a real inbox would. Nothing is there until a test puts it there:
platform = FakePlatform()
post = platform.add_post(text="hello")
await platform.like(platform.connection(), post.id)
read_back = await platform.read_post(platform.connection(), post.id)
assert read_back.liked_by_me
add_post, add_reply, add_like, add_update, add_conversation
and add_message are how a test puts something there. Publishing or
replying through the fake seeds the same way publish always has, so a
post your test publishes can be read straight back, replied to and
liked without seeding it twice. Reading, liking, replying to or
messaging an id nothing put there raises PostGoneError or
NotFoundError, the way a real network would for a post or a
conversation that never existed.
Give it fail_next(method, error) to make the next call to one of those
methods raise error instead of doing its normal thing, for testing the
error paths a real network sometimes answers with -
RateLimitError, MissingPermissionError, BlockedError,
PostGoneError:
platform.fail_next("like", RateLimitError("slow down", retry_after=30))
with pytest.raises(RateLimitError):
await platform.like(platform.connection(), post.id)
Example
transport = RecordingTransport({"POST /posts": {"id": "1"}}) platform = FakePlatform(transport=transport) result = await platform.publish(platform.connection(), Post(text="hi"))
Attributes:
| Name | Type | Description |
|---|---|---|
name |
How this platform is named in code. |
|
features |
What it says it can do. |
|
accounts |
Accounts sign-in offers to choose between. Empty means it
never asks - and then there is no |
|
ask_for |
What signing in should ask a person for. Empty sends them to a sign-in page instead, which is what most networks do. |
|
secret |
The secret |
|
updates |
What |
|
states |
What |
|
token_lifetime |
How long a fresh token lasts. |
|
avatar_url |
The picture every connection this fake builds carries,
and what |
|
publish_fails_with |
An error every |
|
login_fails_with |
An error raised instead of finishing a sign-in, by
|
|
published |
list[tuple[str, Post]]
|
Every post published, as (connection id, post). |
deleted |
list[str]
|
The id of every post deleted. |
created_apps |
list[AppCredentials]
|
Every app registered. |
resumed |
list[tuple[str, str]]
|
Every account picked part way through a sign-in, as (resume token, account id). |
state_asked |
list[tuple[str, str]]
|
Every post asked about, as (connection id, post id). |
refreshed |
list[str]
|
The id of every connection whose token was renewed. |
refreshed_with |
list[AppCredentials | None]
|
Your app's credentials as each renewal was handed
them, in the same order as |
last_remember |
RawData | None
|
What the last |
replied |
list[tuple[str, str, str]]
|
Every reply made through |
liked |
list[tuple[str, str]]
|
Every like made through |
unliked |
list[tuple[str, str]]
|
Every unlike made through |
marked_seen |
list[str]
|
Every marker handed to |
sent_messages |
list[Message]
|
Every message sent through |
marked_read |
list[str]
|
The id of every conversation |
Set up a fake network that behaves however you need it to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
How this platform is named in code. |
'fake'
|
features
|
Feature
|
What it says it can do. |
_FAKE_FEATURES
|
limits
|
Limits | None
|
What it allows. Left out, a sensible small set. |
None
|
transport
|
AsyncBaseTransport | None
|
Where |
None
|
accounts
|
tuple[AccountChoice, ...]
|
Accounts to offer during sign-in. Empty never asks,
and leaves this fake without a |
()
|
ask_for
|
tuple[LoginField, ...]
|
What signing in should ask a person for. Empty sends them to a sign-in page instead. |
()
|
secret
|
str
|
The secret |
_FAKE_SIGNING_KEY
|
updates
|
Sequence[Update]
|
What |
()
|
states
|
Sequence[PostState]
|
What |
()
|
token_lifetime
|
timedelta | None
|
How long a fresh token lasts, or |
timedelta(hours=1)
|
avatar_url
|
str | None
|
The picture every connection this fake builds
carries, and what |
None
|
publish_fails_with
|
SocialChimpError | None
|
An error every |
None
|
login_fails_with
|
SocialChimpError | None
|
An error |
None
|
answers_setup_checks
|
bool
|
Whether this fake answers Meta's setup
check. |
True
|
page_size
|
int
|
How many rows |
_FAKE_PAGE_SIZE
|
Source code in src/socialchimp/testing.py
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 | |
fail_next
¶
fail_next(method: str, error: SocialChimpError) -> None
Make the next call to one inbox method raise error instead.
For testing the error paths a real network sometimes answers with, without a network to make answer that way:
platform.fail_next("like", RateLimitError("slow down"))
with pytest.raises(RateLimitError):
await platform.like(connection, post_id)
Only the inbox methods added in 0.8.0 look at this - read_post,
read_thread, reply, like, unlike, read_likes,
fetch_updates_after, mark_seen, read_conversations,
read_messages, send_message, mark_read and
start_conversation. publish_fails_with and login_fails_with
are still how you fail publish and signing in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
The method's name, such as |
required |
error
|
SocialChimpError
|
What it should raise, once, the next time it is called. |
required |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If |
Source code in src/socialchimp/testing.py
add_post
¶
add_post(
*,
text: str = "",
post_id: str | None = None,
author: Person | None = None,
is_mine: bool = False,
visibility: Visibility | None = None,
created_at: datetime | None = None,
attachments: tuple[Attachment, ...] = (),
) -> PostDetails
Put a top-level post into this fake.
Ready to be read, replied to or liked, straight away.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The post's words. |
''
|
post_id
|
str | None
|
Its id. Left out, one is made up and returned to you. |
None
|
author
|
Person | None
|
Who wrote it. Left out, a person made up for this post. |
None
|
is_mine
|
bool
|
Whether the connected account wrote it. |
False
|
visibility
|
Visibility | None
|
Who it was shared with. Left out, |
None
|
created_at
|
datetime | None
|
When it was posted. Left out, this fake's own clock. |
None
|
attachments
|
tuple[Attachment, ...]
|
Pictures or videos on the post. |
()
|
Returns:
| Type | Description |
|---|---|
PostDetails
|
The post, exactly as |
Source code in src/socialchimp/testing.py
add_reply
¶
add_reply(
to: str,
*,
text: str = "",
post_id: str | None = None,
author: Person | None = None,
is_mine: bool = False,
created_at: datetime | None = None,
) -> PostDetails
Put a reply into this fake, under a post or comment already there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
to
|
str
|
The post or comment this replies to. |
required |
text
|
str
|
The reply's words. |
''
|
post_id
|
str | None
|
Its id. Left out, one is made up and returned to you. |
None
|
author
|
Person | None
|
Who wrote it. Left out, a person made up for this post. |
None
|
is_mine
|
bool
|
Whether the connected account wrote it. |
False
|
created_at
|
datetime | None
|
When it was posted. Left out, this fake's own clock. |
None
|
Returns:
| Type | Description |
|---|---|
PostDetails
|
The reply, exactly as |
Raises:
| Type | Description |
|---|---|
PostGoneError
|
If |
Source code in src/socialchimp/testing.py
add_like
¶
add_like(
post_id: str,
person: Person | None = None,
*,
liked_at: datetime | None = None,
) -> None
Put someone else's like on a post already in this fake.
For the connected account's own like, call like instead - that is
what makes liked_by_me and my_like_id true on a post read back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
post_id
|
str
|
The post or comment to like. |
required |
person
|
Person | None
|
Who liked it. Left out, a person made up for this like. |
None
|
liked_at
|
datetime | None
|
When they liked it. Left out, this fake's own clock. |
None
|
Raises:
| Type | Description |
|---|---|
PostGoneError
|
If |
Source code in src/socialchimp/testing.py
add_update
¶
add_update(
kind: UpdateKind,
*,
update_id: str | None = None,
connection_id: str | None = None,
actor: Person | None = None,
post_id: str | None = None,
about_post_id: str | None = None,
thread_root_id: str | None = None,
conversation_id: str | None = None,
created_at: datetime | None = None,
) -> Update
Queue an update for fetch_updates_after to hand back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
UpdateKind
|
What happened. |
required |
update_id
|
str | None
|
Its id. Left out, one is made up and returned to you. |
None
|
connection_id
|
str | None
|
Which connection it concerns. Left out, this fake's own default connection. |
None
|
actor
|
Person | None
|
Who did it. |
None
|
post_id
|
str | None
|
The thing that happened, as a post id. |
None
|
about_post_id
|
str | None
|
The connected account's own post this concerns. |
None
|
thread_root_id
|
str | None
|
The top of the thread this sits in. |
None
|
conversation_id
|
str | None
|
Which conversation this concerns. |
None
|
created_at
|
datetime | None
|
When it happened. Left out, this fake's own clock. |
None
|
Returns:
| Type | Description |
|---|---|
Update
|
The update, exactly as |
Source code in src/socialchimp/testing.py
add_conversation
¶
add_conversation(
people: Sequence[Person],
*,
conversation_id: str | None = None,
unread_count: int = 0,
can_reply_until: datetime | None = None,
full_history: bool = True,
) -> Conversation
Put a conversation into this fake, ready to be read or sent into.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
people
|
Sequence[Person]
|
Everyone in it except the connected account. |
required |
conversation_id
|
str | None
|
Its id. Left out, one is made up and returned to you. |
None
|
unread_count
|
int
|
How many messages start out unread. |
0
|
can_reply_until
|
datetime | None
|
When a reply window closes. Left out, |
None
|
full_history
|
bool
|
Whether |
True
|
Returns:
| Type | Description |
|---|---|
Conversation
|
The conversation, exactly as |
Conversation
|
back. |
Source code in src/socialchimp/testing.py
add_message
¶
add_message(
conversation_id: str,
sender: Person,
text: str,
*,
message_id: str | None = None,
sent_at: datetime | None = None,
is_mine: bool = False,
attachments: tuple[Attachment, ...] = (),
) -> Message
Put a message into a conversation already in this fake.
A message from anyone but the connected account adds one to
Conversation.unread_count, the way a real message arriving would.
Call mark_read to clear it, or seed it already read with
is_mine=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversation_id
|
str
|
The conversation to add it to. |
required |
sender
|
Person
|
Who sent it. |
required |
text
|
str
|
Its words. |
required |
message_id
|
str | None
|
Its id. Left out, one is made up and returned to you. |
None
|
sent_at
|
datetime | None
|
When it was sent. Left out, this fake's own clock. |
None
|
is_mine
|
bool
|
Whether the connected account sent it. |
False
|
attachments
|
tuple[Attachment, ...]
|
Pictures, videos or other files sent with it. |
()
|
Returns:
| Type | Description |
|---|---|
Message
|
The message, exactly as |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
If |
Source code in src/socialchimp/testing.py
connection
¶
connection(
*,
connection_id: str | None = None,
account_id: str = _FAKE_ACCOUNT,
) -> Connection
Build a connection to this fake, ready to use.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection_id
|
str | None
|
The id your app would have given it. Left out, the network's name and the account's id joined by a colon, which is what every real platform hands back. It used to be the same word whatever the fake was called, so an app tested against nine fake networks got nine rows sharing one primary key. |
None
|
account_id
|
str
|
The id the network would use. |
_FAKE_ACCOUNT
|
Returns:
| Type | Description |
|---|---|
Connection
|
A connection with a working token, carrying |
Connection
|
its picture. |
Source code in src/socialchimp/testing.py
read_profile
async
¶
read_profile(connection: Connection) -> AccountProfile
Ask this fake for the account's current name and picture.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask about. |
required |
Returns:
| Type | Description |
|---|---|
AccountProfile
|
The connection's own name, together with |
Source code in src/socialchimp/testing.py
sign
¶
Return the headers this fake wants alongside a pushed body.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
bytes
|
The exact bytes that will be sent. |
required |
secret
|
str | None
|
Sign with this instead of the fake's own secret, to see what happens when a signature does not match. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
Headers to send with the body. |
Source code in src/socialchimp/testing.py
api_base
¶
api_base(connection: Connection) -> str
Return where this fake's API lives.
One address for every account, the way most real networks work.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account we are about to act as. Ignored here. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The address every path is joined onto. |
Source code in src/socialchimp/testing.py
auth_headers
¶
auth_headers(connection: Connection) -> Mapping[str, str]
Return the header that proves we may act as this account.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account we are acting as. |
required |
Returns:
| Type | Description |
|---|---|
Mapping[str, str]
|
An ordinary bearer token header, built from the connection and |
Mapping[str, str]
|
nothing else. |
Source code in src/socialchimp/testing.py
limits
async
¶
limits(connection: Connection) -> Limits
Return what this fake currently allows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask about. Ignored here. |
required |
Returns:
| Type | Description |
|---|---|
Limits
|
The limits it was built with. |
Source code in src/socialchimp/testing.py
start_login
async
¶
start_login(request: LoginRequest) -> LoginStep
Begin signing someone in.
Asks for details when this fake was given ask_for, the way a
network with no sign-in page does, and sends the person to one
otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
LoginRequest
|
Where to send them back to, and what to ask for. |
required |
Returns:
| Type | Description |
|---|---|
LoginStep
|
Where to send the person next, or what to ask them for. |
Source code in src/socialchimp/testing.py
finish_login
async
¶
finish_login(
request: LoginRequest,
callback: Mapping[str, str],
remember: RawData | None = None,
) -> LoginStep
Carry on after the person comes back.
Asks which account to use when this fake was given accounts and the callback does not name one yet, the way Facebook asks which page.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
LoginRequest
|
The same request used to start the login. |
required |
callback
|
Mapping[str, str]
|
The query values the network sent back. |
required |
remember
|
RawData | None
|
Whatever |
None
|
Returns:
| Type | Description |
|---|---|
LoginStep
|
The finished connection, or the question to ask first. |
Raises:
| Type | Description |
|---|---|
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
refresh
async
¶
refresh(
connection: Connection,
app: AppCredentials | None = None,
) -> Token
Hand out a fresh token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account whose token is running out. |
required |
app
|
AppCredentials | None
|
Your app's credentials. This fake does not need them - it
asks nobody for anything - but it writes down what arrived
on |
None
|
Returns:
| Type | Description |
|---|---|
Token
|
A new token, with a new refresh token, the way the networks that |
Token
|
rotate them do it. |
Source code in src/socialchimp/testing.py
publish
async
¶
publish(connection: Connection, post: Post) -> PostResult
Publish a post, checking it first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to publish as. |
required |
post
|
Post
|
What to publish. |
required |
Returns:
| Type | Description |
|---|---|
PostResult
|
What this fake says about the new post. |
Raises:
| Type | Description |
|---|---|
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
create_app
async
¶
create_app(
*,
name: str,
redirect_uri: str,
host: str | None = None,
scopes: tuple[str, ...] = (),
) -> AppCredentials
Register an app with this fake.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The app name people would see. |
required |
redirect_uri
|
str
|
Where the network sends people back to. |
required |
host
|
str | None
|
Which server to register on. |
None
|
scopes
|
tuple[str, ...]
|
Permissions the app will ask for. |
()
|
Returns:
| Type | Description |
|---|---|
AppCredentials
|
Credentials that work with this fake. |
Source code in src/socialchimp/testing.py
delete_post
async
¶
delete_post(connection: Connection, post_id: str) -> None
Remove a post this fake published.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account that published it. |
required |
post_id
|
str
|
The id this fake handed back. |
required |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
If this fake never published that post, or it has already been deleted. Real networks say the same. |
Source code in src/socialchimp/testing.py
read_post
async
¶
read_post(
connection: Connection, post_id: str
) -> PostDetails
Read one post back in full.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to read it as. |
required |
post_id
|
str
|
The post or comment to read. |
required |
Returns:
| Type | Description |
|---|---|
PostDetails
|
The post, in full. |
Raises:
| Type | Description |
|---|---|
PostGoneError
|
If nothing in this fake knows that id. |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
read_thread
async
¶
read_thread(
connection: Connection,
post_id: str,
*,
depth: int | None = None,
limit: int | None = None,
) -> Thread
Read a post together with every reply under it, oldest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to read it as. |
required |
post_id
|
str
|
The post to read. |
required |
depth
|
int | None
|
How many reply levels to fetch. |
None
|
limit
|
int | None
|
A cap on how many replies come back, across every level. |
None
|
Returns:
| Type | Description |
|---|---|
Thread
|
The post and its replies. |
Thread
|
|
Raises:
| Type | Description |
|---|---|
PostGoneError
|
If nothing in this fake knows that id. |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
reply
async
¶
reply(
connection: Connection,
post_id: str,
text: str,
*,
media: tuple[Media, ...] = (),
options: RawData | None = None,
) -> PostResult
Reply to a post or comment, as the connected account.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to reply as. |
required |
post_id
|
str
|
The post or comment being replied to. |
required |
text
|
str
|
The reply's words. |
required |
media
|
tuple[Media, ...]
|
Pictures or videos to attach to the reply. |
()
|
options
|
RawData | None
|
Ignored. This fake has no per-network settings. |
None
|
Returns:
| Type | Description |
|---|---|
PostResult
|
What this fake says about the new reply. |
Raises:
| Type | Description |
|---|---|
PostGoneError
|
If nothing in this fake knows |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
like
async
¶
like(connection: Connection, post_id: str) -> LikeResult
Like a post or a comment, as the connected account.
Liking something already liked returns the existing like rather than making a new one, the same as a real network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account doing the liking. |
required |
post_id
|
str
|
The post or comment to like. |
required |
Returns:
| Type | Description |
|---|---|
LikeResult
|
What this fake says about the like. |
Raises:
| Type | Description |
|---|---|
PostGoneError
|
If nothing in this fake knows that id. |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
unlike
async
¶
unlike(
connection: Connection,
post_id: str,
*,
like_id: str | None = None,
) -> None
Take back the connected account's like on a post or a comment.
Unliking something not liked succeeds and does nothing, the same as a real network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account taking the like back. |
required |
post_id
|
str
|
The post or comment to unlike. |
required |
like_id
|
str | None
|
The like's own identifier, from |
None
|
Raises:
| Type | Description |
|---|---|
PostGoneError
|
If nothing in this fake knows that id. |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
read_likes
async
¶
read_likes(
connection: Connection,
post_id: str,
*,
after: str | None = None,
limit: int | None = None,
) -> Page[Like]
List who liked a post.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask as. |
required |
post_id
|
str
|
The post or comment to list likes for. |
required |
after
|
str | None
|
A |
None
|
limit
|
int | None
|
A cap on how many come back. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Like]
|
One page of likes. |
Raises:
| Type | Description |
|---|---|
PostGoneError
|
If nothing in this fake knows that id. |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
fetch_updates_after
async
¶
fetch_updates_after(
connection: Connection,
marker: str | None,
*,
limit: int | None = None,
) -> UpdateBatch
Read what is new since a marker, oldest first.
marker=None returns only the latest page - the newest limit
updates, or page_size many - the way a first call with nothing
saved yet sets a starting point, rather than handing back
everything ever queued.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask about. Ignored - this fake has one shared queue. |
required |
marker
|
str | None
|
The marker from the last call's |
required |
limit
|
int | None
|
A cap on how many updates come back in this page.
|
None
|
Returns:
| Type | Description |
|---|---|
UpdateBatch
|
The new updates, and a marker to store for next time. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
mark_seen
async
¶
mark_seen(connection: Connection, marker: str) -> None
Note that a marker from fetch_updates_after has been handled.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to mark it for. Ignored - this fake
just remembers every marker it is given, on |
required |
marker
|
str
|
The marker that has been handled. |
required |
Source code in src/socialchimp/testing.py
read_conversations
async
¶
read_conversations(
connection: Connection,
*,
after: str | None = None,
limit: int | None = None,
) -> Page[Conversation]
List this account's conversations, newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask as. |
required |
after
|
str | None
|
A |
None
|
limit
|
int | None
|
A cap on how many come back. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Conversation]
|
One page of conversations. |
Raises:
| Type | Description |
|---|---|
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
read_messages
async
¶
read_messages(
connection: Connection,
conversation_id: str,
*,
after: str | None = None,
limit: int | None = None,
) -> Page[Message]
Read the messages in one conversation, newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask as. |
required |
conversation_id
|
str
|
Which conversation to read. |
required |
after
|
str | None
|
A |
None
|
limit
|
int | None
|
A cap on how many come back. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Message]
|
One page of messages, newest first. |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
If nothing in this fake knows that conversation. |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
send_message
async
¶
send_message(
connection: Connection,
conversation_id: str,
text: str,
*,
options: RawData | None = None,
) -> Message
Send a message into an existing conversation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to send as. |
required |
conversation_id
|
str
|
Which conversation to send into. |
required |
text
|
str
|
The message's words. |
required |
options
|
RawData | None
|
Ignored. This fake has no per-network settings. |
None
|
Returns:
| Type | Description |
|---|---|
Message
|
The message that was sent. |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
If nothing in this fake knows that conversation. |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
mark_read
async
¶
mark_read(
connection: Connection, conversation_id: str
) -> None
Mark a conversation as read.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to mark it for. |
required |
conversation_id
|
str
|
Which conversation to mark. |
required |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
If nothing in this fake knows that conversation. |
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
start_conversation
async
¶
start_conversation(
connection: Connection,
person_ids: Sequence[str],
text: str,
) -> Message
Start a conversation with one or more people.
A conversation already in this fake with exactly these people is reused rather than starting a second one alongside it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to send as. |
required |
person_ids
|
Sequence[str]
|
Who to start it with. |
required |
text
|
str
|
The first message's words. |
required |
Returns:
| Type | Description |
|---|---|
Message
|
The message that was sent. |
Raises:
| Type | Description |
|---|---|
SocialChimpError
|
Whatever |
Source code in src/socialchimp/testing.py
2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 | |
fetch_updates
async
¶
fetch_updates(
connection: Connection, since: datetime | None
) -> Sequence[Update]
Return what has happened since a moment in time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask about. Ignored here. |
required |
since
|
datetime | None
|
Only return things newer than this. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Update]
|
The updates this fake was built with, oldest first. |
Source code in src/socialchimp/testing.py
check_signature
¶
Check a pushed request really came from this fake.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
bytes
|
The request body, untouched. |
required |
headers
|
Mapping[str, str]
|
The request headers. |
required |
secret
|
str
|
The secret agreed with the network. |
required |
Raises:
| Type | Description |
|---|---|
SignatureError
|
If the header is missing or does not match. |
Source code in src/socialchimp/testing.py
read_updates
¶
read_updates(body: bytes) -> list[Update]
Turn a checked request into every update it carries.
This fake never batches, so it is always a list of one. It is here
so that SocialChimp.read_updates reaches a fake the same way it
reaches Facebook, and an app's own tests do not have to know the
difference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
bytes
|
The request body, untouched. |
required |
Returns:
| Type | Description |
|---|---|
list[Update]
|
What happened, as a list of one. |
Source code in src/socialchimp/testing.py
read_update
¶
read_update(
body: bytes, headers: Mapping[str, str]
) -> Update
Turn a checked request into an update.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
bytes
|
The request body, untouched. |
required |
headers
|
Mapping[str, str]
|
The request headers. Not needed by this fake. |
required |
Returns:
| Type | Description |
|---|---|
Update
|
What happened, in socialchimp's own words. |
Source code in src/socialchimp/testing.py
Recording HTTP without a network¶
RecordingTransport
¶
RecordingTransport(
replies: Mapping[str, RawData] | None = None,
*,
answer: Callable[[Request], Response] | None = None,
status_code: int = 200,
)
Bases: AsyncBaseTransport
Answers httpx requests from a table, and keeps every request.
Hand it to HttpClient(transport=...) and a platform runs end to end
with no network at all. Replies are keyed by method and path together:
transport = RecordingTransport({"POST /api/v1/statuses": {"id": "1"}})
Anything it has no reply for comes back as a 404 whose body names what was asked for and what it does know, because "your platform asked for a path you did not set up" is otherwise a very quiet failure.
Attributes:
| Name | Type | Description |
|---|---|---|
replies |
dict[str, RawData]
|
What to answer, keyed by |
answer |
Your own function, used instead of the table when you need to look at the body or count the calls. |
|
status_code |
The status used for a reply found in the table. Set it to 429 or 500 to see what your platform does then. |
|
requests |
list[Request]
|
Every request sent, in order. |
Set up the replies this transport will give.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
replies
|
Mapping[str, RawData] | None
|
What to answer, keyed by |
None
|
answer
|
Callable[[Request], Response] | None
|
Your own function, used for every request instead of the table. |
None
|
status_code
|
int
|
The status used for replies found in the table. |
200
|
Source code in src/socialchimp/testing.py
handle_async_request
async
¶
Record the request and answer it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The request on its way out. |
required |
Returns:
| Type | Description |
|---|---|
Response
|
The reply from your |
Response
|
404 saying which key was missing. |
Source code in src/socialchimp/testing.py
Recording storage calls¶
RecordingStorage
¶
RecordingStorage(
*,
connections: Iterable[Connection] = (),
apps: Iterable[AppCredentials] = (),
)
Storage that works properly and remembers every call.
Use it wherever a test needs a Storage and then wants to say what
should have reached it - that a rotated refresh token was written, that
a revoked connection was deleted, that nothing was read twice.
It really stores things, so reads see earlier writes. That matters: a double that forgets is a double that hides bugs.
Example
storage = RecordingStorage(connections=[connection]) await sc.account(connection.id).post(Post(text="hi")) assert storage.names() == ["get_connection"]
Attributes:
| Name | Type | Description |
|---|---|---|
calls |
list[StorageCall]
|
Every call made, in order. |
Start with whatever should already be stored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connections
|
Iterable[Connection]
|
Connections to start with. |
()
|
apps
|
Iterable[AppCredentials]
|
App credentials to start with. |
()
|
Source code in src/socialchimp/testing.py
fails
¶
Make one method raise from now on.
For the half of your code that only runs when the database is down.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
Which method, such as |
required |
error
|
Exception
|
What it should raise. |
required |
Source code in src/socialchimp/testing.py
reset
¶
names
¶
calls_to
¶
calls_to(method: str) -> list[StorageCall]
Return every recorded call to one method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
Which method to look for. |
required |
Returns:
| Type | Description |
|---|---|
list[StorageCall]
|
The calls, in the order they were made. |
Source code in src/socialchimp/testing.py
get_connection
async
¶
get_connection(connection_id: str) -> Connection | None
Look up one connected account.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection_id
|
str
|
The id your app gave this connection. |
required |
Returns:
| Type | Description |
|---|---|
Connection | None
|
The connection, or |
Source code in src/socialchimp/testing.py
save_connection
async
¶
save_connection(connection: Connection) -> None
Write a connection, replacing any earlier one with the same id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The connection to write. |
required |
Source code in src/socialchimp/testing.py
delete_connection
async
¶
Remove a connection. Quiet if it is already gone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection_id
|
str
|
The id your app gave this connection. |
required |
Source code in src/socialchimp/testing.py
get_app
async
¶
get_app(
platform: str, host: str | None
) -> AppCredentials | None
Look up your app's credentials for one network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
platform
|
str
|
Which network. |
required |
host
|
str | None
|
Which server, or |
required |
Returns:
| Type | Description |
|---|---|
AppCredentials | None
|
The credentials, or |
Source code in src/socialchimp/testing.py
save_app
async
¶
save_app(app: AppCredentials) -> None
Write your app's credentials for one network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app
|
AppCredentials
|
The credentials to write. |
required |
StorageCall
dataclass
¶
One call your code made to storage.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Which method, such as |
args |
tuple[object, ...]
|
What it was called with, in the order they were passed. |