Merge pull request #480 from carpedm20/add-unit-tests

Add unit tests
This commit is contained in:
Mads Marquart
2019-10-28 11:01:39 +01:00
committed by GitHub
27 changed files with 2130 additions and 146 deletions

View File

@@ -2,7 +2,7 @@ import attr
from . import _util
@attr.s(cmp=False)
@attr.s
class Attachment:
"""Represents a Facebook attachment."""
@@ -10,12 +10,12 @@ class Attachment:
uid = attr.ib(None)
@attr.s(cmp=False)
@attr.s
class UnsentMessage(Attachment):
"""Represents an unsent message attachment."""
@attr.s(cmp=False)
@attr.s
class ShareAttachment(Attachment):
"""Represents a shared item (e.g. URL) attachment."""

View File

@@ -3,7 +3,7 @@ from . import _util
from ._attachment import Attachment
@attr.s(cmp=False)
@attr.s
class FileAttachment(Attachment):
"""Represents a file that has been sent as a Facebook attachment."""
@@ -29,7 +29,7 @@ class FileAttachment(Attachment):
)
@attr.s(cmp=False)
@attr.s
class AudioAttachment(Attachment):
"""Represents an audio file that has been sent as a Facebook attachment."""
@@ -55,7 +55,7 @@ class AudioAttachment(Attachment):
)
@attr.s(cmp=False, init=False)
@attr.s(init=False)
class ImageAttachment(Attachment):
"""Represents an image that has been sent as a Facebook attachment.
@@ -166,7 +166,7 @@ class ImageAttachment(Attachment):
)
@attr.s(cmp=False, init=False)
@attr.s(init=False)
class VideoAttachment(Attachment):
"""Represents a video that has been sent as a Facebook attachment."""

View File

@@ -3,10 +3,12 @@ from . import _util, _plan
from ._thread import ThreadType, Thread
@attr.s(cmp=False, init=False)
@attr.s
class Group(Thread):
"""Represents a Facebook group. Inherits `Thread`."""
type = ThreadType.GROUP
#: Unique list (set) of the group thread's participant user IDs
participants = attr.ib(factory=set, converter=lambda x: set() if x is None else x)
#: A dictionary, containing user nicknames mapped to their IDs
@@ -26,38 +28,6 @@ class Group(Thread):
# Link for joining group
join_link = attr.ib(None)
def __init__(
self,
uid,
participants=None,
nicknames=None,
color=None,
emoji=None,
admins=None,
approval_mode=None,
approval_requests=None,
join_link=None,
privacy_mode=None,
**kwargs
):
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
if participants is None:
participants = set()
self.participants = participants
if nicknames is None:
nicknames = []
self.nicknames = nicknames
self.color = color
self.emoji = emoji
if admins is None:
admins = set()
self.admins = admins
self.approval_mode = approval_mode
if approval_requests is None:
approval_requests = set()
self.approval_requests = approval_requests
self.join_link = join_link
@classmethod
def _from_graphql(cls, data):
if data.get("image") is None:

View File

@@ -3,7 +3,7 @@ from ._attachment import Attachment
from . import _util
@attr.s(cmp=False)
@attr.s
class LocationAttachment(Attachment):
"""Represents a user location.
@@ -53,7 +53,7 @@ class LocationAttachment(Attachment):
return rtn
@attr.s(cmp=False, init=False)
@attr.s
class LiveLocationAttachment(LocationAttachment):
"""Represents a live user location."""
@@ -64,11 +64,6 @@ class LiveLocationAttachment(LocationAttachment):
#: True if live location is expired
is_expired = attr.ib(None)
def __init__(self, name=None, expires_at=None, is_expired=None, **kwargs):
super(LiveLocationAttachment, self).__init__(**kwargs)
self.expires_at = expires_at
self.is_expired = is_expired
@classmethod
def _from_pull(cls, data):
return cls(
@@ -96,7 +91,7 @@ class LiveLocationAttachment(LocationAttachment):
if target.get("coordinate")
else None,
name=data["title_with_entities"]["text"],
expires_at=_util.millis_to_datetime(target.get("expiration_time")),
expires_at=_util.seconds_to_datetime(target.get("expiration_time")),
is_expired=target.get("is_expired"),
)
media = data.get("media")

View File

@@ -42,7 +42,7 @@ class MessageReaction(Enum):
NO = "👎"
@attr.s(cmp=False)
@attr.s
class Mention:
"""Represents a ``@mention``."""
@@ -54,7 +54,7 @@ class Mention:
length = attr.ib(10)
@attr.s(cmp=False)
@attr.s
class Message:
"""Represents a Facebook message."""

View File

@@ -3,10 +3,12 @@ from . import _plan
from ._thread import ThreadType, Thread
@attr.s(cmp=False, init=False)
@attr.s
class Page(Thread):
"""Represents a Facebook page. Inherits `Thread`."""
type = ThreadType.PAGE
#: The page's custom URL
url = attr.ib(None)
#: The name of the page's location city
@@ -18,23 +20,6 @@ class Page(Thread):
#: The page's category
category = attr.ib(None)
def __init__(
self,
uid,
url=None,
city=None,
likes=None,
sub_title=None,
category=None,
**kwargs
):
super(Page, self).__init__(ThreadType.PAGE, uid, **kwargs)
self.url = url
self.city = city
self.likes = likes
self.sub_title = sub_title
self.category = category
@classmethod
def _from_graphql(cls, data):
if data.get("profile_picture") is None:

View File

@@ -10,7 +10,7 @@ class GuestStatus(Enum):
DECLINED = 3
@attr.s(cmp=False)
@attr.s
class Plan:
"""Represents a plan."""

View File

@@ -1,7 +1,7 @@
import attr
@attr.s(cmp=False)
@attr.s
class Poll:
"""Represents a poll."""
@@ -24,7 +24,7 @@ class Poll:
)
@attr.s(cmp=False)
@attr.s
class PollOption:
"""Represents a poll option."""

View File

@@ -2,7 +2,7 @@ import attr
from ._attachment import Attachment
@attr.s(cmp=False)
@attr.s
class QuickReply:
"""Represents a quick reply."""
@@ -16,7 +16,7 @@ class QuickReply:
is_response = attr.ib(False)
@attr.s(cmp=False, init=False)
@attr.s
class QuickReplyText(QuickReply):
"""Represents a text quick reply."""
@@ -27,25 +27,16 @@ class QuickReplyText(QuickReply):
#: Type of the quick reply
_type = "text"
def __init__(self, title=None, image_url=None, **kwargs):
super(QuickReplyText, self).__init__(**kwargs)
self.title = title
self.image_url = image_url
@attr.s(cmp=False, init=False)
@attr.s
class QuickReplyLocation(QuickReply):
"""Represents a location quick reply (Doesn't work on mobile)."""
#: Type of the quick reply
_type = "location"
def __init__(self, **kwargs):
super(QuickReplyLocation, self).__init__(**kwargs)
self.is_response = False
@attr.s(cmp=False, init=False)
@attr.s
class QuickReplyPhoneNumber(QuickReply):
"""Represents a phone number quick reply (Doesn't work on mobile)."""
@@ -54,12 +45,8 @@ class QuickReplyPhoneNumber(QuickReply):
#: Type of the quick reply
_type = "user_phone_number"
def __init__(self, image_url=None, **kwargs):
super(QuickReplyPhoneNumber, self).__init__(**kwargs)
self.image_url = image_url
@attr.s(cmp=False, init=False)
@attr.s
class QuickReplyEmail(QuickReply):
"""Represents an email quick reply (Doesn't work on mobile)."""
@@ -68,10 +55,6 @@ class QuickReplyEmail(QuickReply):
#: Type of the quick reply
_type = "user_email"
def __init__(self, image_url=None, **kwargs):
super(QuickReplyEmail, self).__init__(**kwargs)
self.image_url = image_url
def graphql_to_quick_reply(q, is_response=False):
data = dict()

View File

@@ -2,7 +2,7 @@ import attr
from ._attachment import Attachment
@attr.s(cmp=False, init=False)
@attr.s
class Sticker(Attachment):
"""Represents a Facebook sticker that has been sent to a thread as an attachment."""
@@ -32,9 +32,6 @@ class Sticker(Attachment):
#: The sticker's label/name
label = attr.ib(None)
def __init__(self, uid=None):
super(Sticker, self).__init__(uid=uid)
@classmethod
def _from_graphql(cls, data):
if not data:

View File

@@ -67,14 +67,14 @@ class ThreadColor(Enum):
return cls._extend_if_invalid(value)
@attr.s(cmp=False, init=False)
@attr.s
class Thread:
"""Represents a Facebook thread."""
#: The unique identifier of the thread. Can be used a ``thread_id``. See :ref:`intro_threads` for more info
uid = attr.ib(converter=str)
#: Specifies the type of thread. Can be used a ``thread_type``. See :ref:`intro_threads` for more info
type = attr.ib()
type = None
#: A URL to the thread's picture
photo = attr.ib(None)
#: The name of the thread
@@ -86,24 +86,6 @@ class Thread:
#: Set :class:`Plan`
plan = attr.ib(None)
def __init__(
self,
_type,
uid,
photo=None,
name=None,
last_active=None,
message_count=None,
plan=None,
):
self.uid = str(uid)
self.type = _type
self.photo = photo
self.name = name
self.last_active = last_active
self.message_count = message_count
self.plan = plan
@staticmethod
def _parse_customization_info(data):
if data is None or data.get("customization_info") is None:

View File

@@ -41,10 +41,12 @@ class TypingStatus(Enum):
TYPING = 1
@attr.s(cmp=False, init=False)
@attr.s
class User(Thread):
"""Represents a Facebook user. Inherits `Thread`."""
type = ThreadType.USER
#: The profile URL
url = attr.ib(None)
#: The users first name
@@ -66,33 +68,6 @@ class User(Thread):
#: The default emoji
emoji = attr.ib(None)
def __init__(
self,
uid,
url=None,
first_name=None,
last_name=None,
is_friend=None,
gender=None,
affinity=None,
nickname=None,
own_nickname=None,
color=None,
emoji=None,
**kwargs
):
super(User, self).__init__(ThreadType.USER, uid, **kwargs)
self.url = url
self.first_name = first_name
self.last_name = last_name
self.is_friend = is_friend
self.gender = gender
self.affinity = affinity
self.nickname = nickname
self.own_nickname = own_nickname
self.color = color
self.emoji = emoji
@classmethod
def _from_graphql(cls, data):
if data.get("profile_picture") is None:
@@ -179,7 +154,7 @@ class User(Thread):
)
@attr.s(cmp=False)
@attr.s
class ActiveStatus:
#: Whether the user is active now
active = attr.ib(None)

456
tests/test_attachment.py Normal file
View File

@@ -0,0 +1,456 @@
import pytest
import datetime
import fbchat
from fbchat._attachment import UnsentMessage, ShareAttachment
def test_parse_unsent_message():
data = {
"legacy_attachment_id": "ee.mid.$xyz",
"story_attachment": {
"description": {"text": "You removed a message"},
"media": None,
"source": None,
"style_list": ["globally_deleted_message_placeholder", "fallback"],
"title_with_entities": {"text": ""},
"properties": [],
"url": None,
"deduplication_key": "deadbeef123",
"action_links": [],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": None,
"subattachments": [],
},
"genie_attachment": {"genie_message": None},
}
assert UnsentMessage(
uid="ee.mid.$xyz"
) == fbchat._message.graphql_to_extensible_attachment(data)
def test_share_from_graphql_minimal():
data = {
"target": {},
"url": "a.com",
"title_with_entities": {"text": "a.com"},
"subattachments": [],
}
assert ShareAttachment(
url="a.com", original_url="a.com", title="a.com"
) == ShareAttachment._from_graphql(data)
def test_share_from_graphql_link():
data = {
"description": {"text": ""},
"media": {
"animated_image": None,
"image": None,
"playable_duration_in_ms": 0,
"is_playable": False,
"playable_url": None,
},
"source": {"text": "a.com"},
"style_list": ["share", "fallback"],
"title_with_entities": {"text": "a.com"},
"properties": [],
"url": "http://l.facebook.com/l.php?u=http%3A%2F%2Fa.com%2F&h=def&s=1",
"deduplication_key": "ee.mid.$xyz",
"action_links": [{"title": "About this website", "url": None}],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {"__typename": "ExternalUrl"},
"subattachments": [],
}
assert ShareAttachment(
author=None,
url="http://l.facebook.com/l.php?u=http%3A%2F%2Fa.com%2F&h=def&s=1",
original_url="http://a.com/",
title="a.com",
description="",
source="a.com",
image_url=None,
original_image_url=None,
image_width=None,
image_height=None,
attachments=[],
uid="ee.mid.$xyz",
) == ShareAttachment._from_graphql(data)
def test_share_from_graphql_link_with_image():
data = {
"description": {
"text": (
"Create an account or log in to Facebook."
" Connect with friends, family and other people you know."
" Share photos and videos, send messages and get updates."
)
},
"media": {
"animated_image": None,
"image": {
"uri": "https://www.facebook.com/rsrc.php/v3/x.png",
"height": 325,
"width": 325,
},
"playable_duration_in_ms": 0,
"is_playable": False,
"playable_url": None,
},
"source": None,
"style_list": ["share", "fallback"],
"title_with_entities": {"text": "Facebook log in or sign up"},
"properties": [],
"url": "http://facebook.com/",
"deduplication_key": "deadbeef123",
"action_links": [],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {"__typename": "ExternalUrl"},
"subattachments": [],
}
assert ShareAttachment(
author=None,
url="http://facebook.com/",
original_url="http://facebook.com/",
title="Facebook log in or sign up",
description=(
"Create an account or log in to Facebook."
" Connect with friends, family and other people you know."
" Share photos and videos, send messages and get updates."
),
source=None,
image_url="https://www.facebook.com/rsrc.php/v3/x.png",
original_image_url="https://www.facebook.com/rsrc.php/v3/x.png",
image_width=325,
image_height=325,
attachments=[],
uid="deadbeef123",
) == ShareAttachment._from_graphql(data)
def test_share_from_graphql_video():
data = {
"description": {
"text": (
"Rick Astley's official music video for “Never Gonna Give You Up”"
" Listen to Rick Astley: https://RickAstley.lnk.to/_listenYD"
" Subscribe to the official Rick As..."
)
},
"media": {
"animated_image": None,
"image": {
"uri": (
"https://external-arn2-1.xx.fbcdn.net/safe_image.php?d=xyz123"
"&w=960&h=540&url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FdQw4w9WgXcQ"
"%2Fmaxresdefault.jpg&sx=0&sy=0&sw=1280&sh=720&_nc_hash=abc123"
),
"height": 540,
"width": 960,
},
"playable_duration_in_ms": 0,
"is_playable": True,
"playable_url": "https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1",
},
"source": {"text": "youtube.com"},
"style_list": ["share", "fallback"],
"title_with_entities": {
"text": "Rick Astley - Never Gonna Give You Up (Video)"
},
"properties": [
{"key": "width", "value": {"text": "1280"}},
{"key": "height", "value": {"text": "720"}},
],
"url": "https://l.facebook.com/l.php?u=https%3A%2F%2Fyoutu.be%2FdQw4w9WgXcQ",
"deduplication_key": "ee.mid.$gAAT4Sw1WSGhzQ9uRWVtEpZHZ8ZPV",
"action_links": [{"title": "About this website", "url": None}],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {"__typename": "ExternalUrl"},
"subattachments": [],
}
assert ShareAttachment(
author=None,
url="https://l.facebook.com/l.php?u=https%3A%2F%2Fyoutu.be%2FdQw4w9WgXcQ",
original_url="https://youtu.be/dQw4w9WgXcQ",
title="Rick Astley - Never Gonna Give You Up (Video)",
description=(
"Rick Astley's official music video for “Never Gonna Give You Up”"
" Listen to Rick Astley: https://RickAstley.lnk.to/_listenYD"
" Subscribe to the official Rick As..."
),
source="youtube.com",
image_url=(
"https://external-arn2-1.xx.fbcdn.net/safe_image.php?d=xyz123"
"&w=960&h=540&url=https%3A%2F%2Fi.ytimg.com%2Fvi%2FdQw4w9WgXcQ"
"%2Fmaxresdefault.jpg&sx=0&sy=0&sw=1280&sh=720&_nc_hash=abc123"
),
original_image_url="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
image_width=960,
image_height=540,
attachments=[],
uid="ee.mid.$gAAT4Sw1WSGhzQ9uRWVtEpZHZ8ZPV",
) == ShareAttachment._from_graphql(data)
def test_share_with_image_subattachment():
data = {
"description": {"text": "Abc"},
"media": {
"animated_image": None,
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
"height": 960,
"width": 720,
},
"playable_duration_in_ms": 0,
"is_playable": False,
"playable_url": None,
},
"source": {"text": "Def"},
"style_list": ["attached_story", "fallback"],
"title_with_entities": {"text": ""},
"properties": [],
"url": "https://www.facebook.com/groups/11223344/permalink/1234/",
"deduplication_key": "deadbeef123",
"action_links": [
{"title": None, "url": None},
{"title": None, "url": "https://www.facebook.com/groups/11223344/"},
{
"title": "Report Post to Admin",
"url": "https://www.facebook.com/groups/11223344/members/",
},
],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {
"__typename": "Story",
"title": None,
"description": {"text": "Abc"},
"actors": [
{
"__typename": "User",
"name": "Def",
"id": "1111",
"short_name": "Def",
"url": "https://www.facebook.com/some-user",
"profile_picture": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-1/c123.123.123.123a/s50x50/img.jpg",
"height": 50,
"width": 50,
},
}
],
"to": {
"__typename": "Group",
"name": "Some group",
"url": "https://www.facebook.com/groups/11223344/",
},
"attachments": [
{
"url": "https://www.facebook.com/photo.php?fbid=4321&set=gm.1234&type=3",
"media": {
"is_playable": False,
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
"height": 960,
"width": 720,
},
},
}
],
"attached_story": None,
},
"subattachments": [
{
"description": {"text": "Abc"},
"media": {
"animated_image": None,
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
"height": 960,
"width": 720,
},
"playable_duration_in_ms": 0,
"is_playable": False,
"playable_url": None,
},
"source": None,
"style_list": ["photo", "games_app", "fallback"],
"title_with_entities": {"text": ""},
"properties": [
{"key": "photoset_reference_token", "value": {"text": "gm.1234"}},
{"key": "layout_x", "value": {"text": "0"}},
{"key": "layout_y", "value": {"text": "0"}},
{"key": "layout_w", "value": {"text": "0"}},
{"key": "layout_h", "value": {"text": "0"}},
],
"url": "https://www.facebook.com/photo.php?fbid=4321&set=gm.1234&type=3",
"deduplication_key": "deadbeef456",
"action_links": [],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {"__typename": "Photo"},
}
],
}
assert ShareAttachment(
author="1111",
url="https://www.facebook.com/groups/11223344/permalink/1234/",
original_url="https://www.facebook.com/groups/11223344/permalink/1234/",
title="",
description="Abc",
source="Def",
image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
original_image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
image_width=720,
image_height=960,
attachments=[None],
uid="deadbeef123",
) == ShareAttachment._from_graphql(data)
def test_share_with_video_subattachment():
data = {
"description": {"text": "Abc"},
"media": {
"animated_image": None,
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
"height": 540,
"width": 960,
},
"playable_duration_in_ms": 24469,
"is_playable": True,
"playable_url": "https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
},
"source": {"text": "Def"},
"style_list": ["attached_story", "fallback"],
"title_with_entities": {"text": ""},
"properties": [],
"url": "https://www.facebook.com/groups/11223344/permalink/1234/",
"deduplication_key": "deadbeef123",
"action_links": [
{"title": None, "url": None},
{"title": None, "url": "https://www.facebook.com/groups/11223344/"},
{"title": None, "url": None},
{"title": "A watch party is currently playing this video.", "url": None},
],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {
"__typename": "Story",
"title": None,
"description": {"text": "Abc"},
"actors": [
{
"__typename": "User",
"name": "Def",
"id": "1111",
"short_name": "Def",
"url": "https://www.facebook.com/some-user",
"profile_picture": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-1/c1.0.50.50a/p50x50/profile.jpg",
"height": 50,
"width": 50,
},
}
],
"to": {
"__typename": "Group",
"name": "Some group",
"url": "https://www.facebook.com/groups/11223344/",
},
"attachments": [
{
"url": "https://www.facebook.com/some-user/videos/2222/",
"media": {
"is_playable": True,
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
"height": 540,
"width": 960,
},
},
}
],
"attached_story": None,
},
"subattachments": [
{
"description": None,
"media": {
"animated_image": None,
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
"height": 540,
"width": 960,
},
"playable_duration_in_ms": 24469,
"is_playable": True,
"playable_url": "https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
},
"source": None,
"style_list": [
"video_autoplay",
"video_inline",
"video",
"games_app",
"fallback",
],
"title_with_entities": {"text": ""},
"properties": [
{
"key": "can_autoplay_result",
"value": {"text": "ugc_default_allowed"},
}
],
"url": "https://www.facebook.com/some-user/videos/2222/",
"deduplication_key": "deadbeef456",
"action_links": [],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {
"__typename": "Video",
"video_id": "2222",
"video_messenger_cta_payload": None,
},
}
],
}
assert ShareAttachment(
author="1111",
url="https://www.facebook.com/groups/11223344/permalink/1234/",
original_url="https://www.facebook.com/groups/11223344/permalink/1234/",
title="",
description="Abc",
source="Def",
image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
original_image_url="https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
image_width=960,
image_height=540,
attachments=[
fbchat.VideoAttachment(
uid="2222",
duration=datetime.timedelta(seconds=24, microseconds=469000),
preview_url="https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
medium_image={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
"width": 960,
"height": 540,
},
)
],
uid="deadbeef123",
) == ShareAttachment._from_graphql(data)

14
tests/test_core.py Normal file
View File

@@ -0,0 +1,14 @@
import pytest
from fbchat._core import Enum
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_enum_extend_if_invalid():
class TestEnum(Enum):
A = 1
B = 2
assert TestEnum._extend_if_invalid(1) == TestEnum.A
assert TestEnum._extend_if_invalid(3) == TestEnum.UNKNOWN_3
assert TestEnum._extend_if_invalid(3) == TestEnum.UNKNOWN_3
assert TestEnum(3) == TestEnum.UNKNOWN_3

358
tests/test_file.py Normal file
View File

@@ -0,0 +1,358 @@
import datetime
import fbchat
from fbchat._file import (
FileAttachment,
AudioAttachment,
ImageAttachment,
VideoAttachment,
graphql_to_attachment,
graphql_to_subattachment,
)
def test_imageattachment_from_list():
data = {
"__typename": "MessageImage",
"id": "bWVzc2...",
"legacy_attachment_id": "1234",
"image": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s261x260/1.jpg"},
"image1": {
"height": 463,
"width": 960,
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg",
},
"image2": {
"height": 988,
"width": 2048,
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s2048x2048/3.jpg",
},
"original_dimensions": {"x": 2833, "y": 1367},
"photo_encodings": [],
}
assert ImageAttachment(
uid="1234",
width=2833,
height=1367,
thumbnail_url="https://scontent-arn2-1.xx.fbcdn.net/v/s261x260/1.jpg",
preview={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg",
"width": 960,
"height": 463,
},
large_preview={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s2048x2048/3.jpg",
"width": 2048,
"height": 988,
},
) == ImageAttachment._from_list({"node": data})
def test_videoattachment_from_list():
data = {
"__typename": "MessageVideo",
"id": "bWVzc2...",
"legacy_attachment_id": "1234",
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/p261x260/1.jpg"
},
"image1": {
"height": 368,
"width": 640,
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/2.jpg",
},
"image2": {
"height": 368,
"width": 640,
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/3.jpg",
},
"original_dimensions": {"x": 640, "y": 368},
}
assert VideoAttachment(
uid="1234",
width=640,
height=368,
small_image={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/p261x260/1.jpg"
},
medium_image={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/2.jpg",
"width": 640,
"height": 368,
},
large_image={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.3394-10/3.jpg",
"width": 640,
"height": 368,
},
) == VideoAttachment._from_list({"node": data})
def test_graphql_to_attachment_empty():
assert fbchat.Attachment() == graphql_to_attachment({"__typename": "Unknown"})
def test_graphql_to_attachment_simple():
data = {"__typename": "Unknown", "legacy_attachment_id": "1234"}
assert fbchat.Attachment(uid="1234") == graphql_to_attachment(data)
def test_graphql_to_attachment_file():
data = {
"__typename": "MessageFile",
"attribution_app": None,
"attribution_metadata": None,
"filename": "file.txt",
"url": "https://l.facebook.com/l.php?u=https%3A%2F%2Fcdn.fbsbx.com%2Fv%2Ffile.txt&h=AT1...&s=1",
"content_type": "attach:text",
"is_malicious": False,
"message_file_fbid": "1234",
"url_shimhash": "AT0...",
"url_skipshim": True,
}
assert FileAttachment(
uid="1234",
url="https://l.facebook.com/l.php?u=https%3A%2F%2Fcdn.fbsbx.com%2Fv%2Ffile.txt&h=AT1...&s=1",
size=None,
name="file.txt",
is_malicious=False,
) == graphql_to_attachment(data)
def test_graphql_to_attachment_audio():
data = {
"__typename": "MessageAudio",
"attribution_app": None,
"attribution_metadata": None,
"filename": "audio.mp3",
"playable_url": "https://cdn.fbsbx.com/v/audio.mp3?dl=1",
"playable_duration_in_ms": 27745,
"is_voicemail": False,
"audio_type": "FILE_ATTACHMENT",
"url_shimhash": "AT0...",
"url_skipshim": True,
}
assert AudioAttachment(
uid=None,
filename="audio.mp3",
url="https://cdn.fbsbx.com/v/audio.mp3?dl=1",
duration=datetime.timedelta(seconds=27, microseconds=745000),
audio_type="FILE_ATTACHMENT",
) == graphql_to_attachment(data)
def test_graphql_to_attachment_image1():
data = {
"__typename": "MessageImage",
"attribution_app": None,
"attribution_metadata": None,
"filename": "image-1234",
"preview": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/1.png",
"height": 128,
"width": 128,
},
"large_preview": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.png",
"height": 128,
"width": 128,
},
"thumbnail": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p50x50/3.png"},
"photo_encodings": [],
"legacy_attachment_id": "1234",
"original_dimensions": {"x": 128, "y": 128},
"original_extension": "png",
"render_as_sticker": False,
"blurred_image_uri": None,
}
assert ImageAttachment(
uid="1234",
original_extension="png",
width=None,
height=None,
is_animated=False,
thumbnail_url="https://scontent-arn2-1.xx.fbcdn.net/v/p50x50/3.png",
preview={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/1.png",
"width": 128,
"height": 128,
},
large_preview={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.png",
"width": 128,
"height": 128,
},
) == graphql_to_attachment(data)
def test_graphql_to_attachment_image2():
data = {
"__typename": "MessageAnimatedImage",
"attribution_app": None,
"attribution_metadata": None,
"filename": "gif-1234",
"animated_image": {
"uri": "https://cdn.fbsbx.com/v/1.gif",
"height": 128,
"width": 128,
},
"legacy_attachment_id": "1234",
"preview_image": {
"uri": "https://cdn.fbsbx.com/v/1.gif",
"height": 128,
"width": 128,
},
"original_dimensions": {"x": 128, "y": 128},
}
assert ImageAttachment(
uid="1234",
original_extension="gif",
width=None,
height=None,
is_animated=True,
preview={"uri": "https://cdn.fbsbx.com/v/1.gif", "width": 128, "height": 128},
animated_preview={
"uri": "https://cdn.fbsbx.com/v/1.gif",
"width": 128,
"height": 128,
},
) == graphql_to_attachment(data)
def test_graphql_to_attachment_video():
data = {
"__typename": "MessageVideo",
"attribution_app": None,
"attribution_metadata": None,
"filename": "video-4321.mp4",
"playable_url": "https://video-arn2-1.xx.fbcdn.net/v/video-4321.mp4",
"chat_image": {
"height": 96,
"width": 168,
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s168x128/1.jpg",
},
"legacy_attachment_id": "1234",
"video_type": "FILE_ATTACHMENT",
"original_dimensions": {"x": 640, "y": 368},
"playable_duration_in_ms": 6000,
"large_image": {
"height": 368,
"width": 640,
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg",
},
"inbox_image": {
"height": 260,
"width": 452,
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p261x260/3.jpg",
},
}
assert VideoAttachment(
uid="1234",
width=None,
height=None,
duration=datetime.timedelta(seconds=6),
preview_url="https://video-arn2-1.xx.fbcdn.net/v/video-4321.mp4",
small_image={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/s168x128/1.jpg",
"width": 168,
"height": 96,
},
medium_image={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/p261x260/3.jpg",
"width": 452,
"height": 260,
},
large_image={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/2.jpg",
"width": 640,
"height": 368,
},
) == graphql_to_attachment(data)
def test_graphql_to_subattachment_empty():
assert None is graphql_to_subattachment({})
def test_graphql_to_subattachment_image():
data = {
"description": {"text": "Abc"},
"media": {
"animated_image": None,
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t1.0-9/1.jpg",
"height": 960,
"width": 720,
},
"playable_duration_in_ms": 0,
"is_playable": False,
"playable_url": None,
},
"source": None,
"style_list": ["photo", "games_app", "fallback"],
"title_with_entities": {"text": ""},
"properties": [
{"key": "photoset_reference_token", "value": {"text": "gm.4321"}},
{"key": "layout_x", "value": {"text": "0"}},
{"key": "layout_y", "value": {"text": "0"}},
{"key": "layout_w", "value": {"text": "0"}},
{"key": "layout_h", "value": {"text": "0"}},
],
"url": "https://www.facebook.com/photo.php?fbid=1234&set=gm.4321&type=3",
"deduplication_key": "8334...",
"action_links": [],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {"__typename": "Photo"},
}
assert None is graphql_to_subattachment(data)
def test_graphql_to_subattachment_video():
data = {
"description": None,
"media": {
"animated_image": None,
"image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
"height": 540,
"width": 960,
},
"playable_duration_in_ms": 24469,
"is_playable": True,
"playable_url": "https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
},
"source": None,
"style_list": [
"video_autoplay",
"video_inline",
"video",
"games_app",
"fallback",
],
"title_with_entities": {"text": ""},
"properties": [
{"key": "can_autoplay_result", "value": {"text": "ugc_default_allowed"}}
],
"url": "https://www.facebook.com/some-username/videos/1234/",
"deduplication_key": "ddb7...",
"action_links": [],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {
"__typename": "Video",
"video_id": "1234",
"video_messenger_cta_payload": None,
},
}
assert VideoAttachment(
uid="1234",
duration=datetime.timedelta(seconds=24, microseconds=469000),
preview_url="https://video-arn2-1.xx.fbcdn.net/v/t42.9040-2/vid.mp4",
medium_image={
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/t15.5256-10/p180x540/1.jpg",
"width": 960,
"height": 540,
},
) == graphql_to_subattachment(data)

35
tests/test_graphql.py Normal file
View File

@@ -0,0 +1,35 @@
import pytest
import json
from fbchat._graphql import ConcatJSONDecoder, queries_to_json, response_to_json
@pytest.mark.parametrize(
"content,result",
[
("", []),
('{"a":"b"}', [{"a": "b"}]),
('{"a":"b"}{"b":"c"}', [{"a": "b"}, {"b": "c"}]),
(' \n{"a": "b" } \n { "b" \n\n : "c" }', [{"a": "b"}, {"b": "c"}]),
],
)
def test_concat_json_decoder(content, result):
assert result == json.loads(content, cls=ConcatJSONDecoder)
def test_queries_to_json():
assert {"q0": "A", "q1": "B", "q2": "C"} == json.loads(
queries_to_json("A", "B", "C")
)
def test_response_to_json():
data = (
'{"q1":{"data":{"b":"c"}}}\r\n'
'{"q0":{"response":[1,2]}}\r\n'
"{\n"
' "successful_results": 2,\n'
' "error_results": 0,\n'
' "skipped_results": 0\n'
"}"
)
assert [[1, 2], {"b": "c"}] == response_to_json(data)

43
tests/test_group.py Normal file
View File

@@ -0,0 +1,43 @@
from fbchat._group import Group
def test_group_from_graphql():
data = {
"name": "Group ABC",
"thread_key": {"thread_fbid": "11223344"},
"image": None,
"is_group_thread": True,
"all_participants": {
"nodes": [
{"messaging_actor": {"id": "1234"}},
{"messaging_actor": {"id": "2345"}},
{"messaging_actor": {"id": "3456"}},
]
},
"customization_info": {
"participant_customizations": [],
"outgoing_bubble_color": None,
"emoji": "😀",
},
"thread_admins": [{"id": "1234"}],
"group_approval_queue": {"nodes": []},
"approval_mode": 0,
"joinable_mode": {"mode": "0", "link": ""},
"event_reminders": {"nodes": []},
}
assert Group(
uid="11223344",
photo=None,
name="Group ABC",
last_active=None,
message_count=None,
plan=None,
participants={"1234", "2345", "3456"},
nicknames={},
color=None,
emoji="😀",
admins={"1234"},
approval_mode=False,
approval_requests=set(),
join_link="",
) == Group._from_graphql(data)

91
tests/test_location.py Normal file
View File

@@ -0,0 +1,91 @@
import pytest
import datetime
from fbchat._location import LocationAttachment, LiveLocationAttachment
def test_location_attachment_from_graphql():
data = {
"description": {"text": ""},
"media": {
"animated_image": None,
"image": {
"uri": "https://external-arn2-1.xx.fbcdn.net/static_map.php?v=1020&osm_provider=2&size=545x280&zoom=15&markers=55.40000000%2C12.43220000&language=en",
"height": 280,
"width": 545,
},
"playable_duration_in_ms": 0,
"is_playable": False,
"playable_url": None,
},
"source": None,
"style_list": ["message_location", "fallback"],
"title_with_entities": {"text": "Your location"},
"properties": [
{"key": "width", "value": {"text": "545"}},
{"key": "height", "value": {"text": "280"}},
],
"url": "https://l.facebook.com/l.php?u=https%3A%2F%2Fwww.bing.com%2Fmaps%2Fdefault.aspx%3Fv%3D2%26pc%3DFACEBK%26mid%3D8100%26where1%3D55.4%252C%2B12.4322%26FORM%3DFBKPL1%26mkt%3Den-GB&h=a&s=1",
"deduplication_key": "400828513928715",
"action_links": [],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"xma_layout_info": None,
"target": {"__typename": "MessageLocation"},
"subattachments": [],
}
expected = LocationAttachment(latitude=55.4, longitude=12.4322, uid=400828513928715)
expected.image_url = "https://external-arn2-1.xx.fbcdn.net/static_map.php?v=1020&osm_provider=2&size=545x280&zoom=15&markers=55.40000000%2C12.43220000&language=en"
expected.image_width = 545
expected.image_height = 280
expected.url = "https://l.facebook.com/l.php?u=https%3A%2F%2Fwww.bing.com%2Fmaps%2Fdefault.aspx%3Fv%3D2%26pc%3DFACEBK%26mid%3D8100%26where1%3D55.4%252C%2B12.4322%26FORM%3DFBKPL1%26mkt%3Den-GB&h=a&s=1"
assert expected == LocationAttachment._from_graphql(data)
@pytest.mark.skip(reason="need to gather test data")
def test_live_location_from_pull():
data = ...
assert LiveLocationAttachment(...) == LiveLocationAttachment._from_pull(data)
def test_live_location_from_graphql_expired():
data = {
"description": {"text": "Last update 4 Jan"},
"media": None,
"source": None,
"style_list": ["message_live_location", "fallback"],
"title_with_entities": {"text": "Location-sharing ended"},
"properties": [],
"url": "https://www.facebook.com/",
"deduplication_key": "2254535444791641",
"action_links": [],
"messaging_attribution": None,
"messenger_call_to_actions": [],
"target": {
"__typename": "MessageLiveLocation",
"live_location_id": "2254535444791641",
"is_expired": True,
"expiration_time": 1546626345,
"sender": {"id": "100007056224713"},
"coordinate": None,
"location_title": None,
"sender_destination": None,
"stop_reason": "CANCELED",
},
"subattachments": [],
}
expected = LiveLocationAttachment(
uid=2254535444791641,
name="Location-sharing ended",
expires_at=datetime.datetime(
2019, 1, 4, 18, 25, 45, tzinfo=datetime.timezone.utc
),
is_expired=True,
)
expected.url = "https://www.facebook.com/"
assert expected == LiveLocationAttachment._from_graphql(data)
@pytest.mark.skip(reason="need to gather test data")
def test_live_location_from_graphql():
data = ...
assert LiveLocationAttachment(...) == LiveLocationAttachment._from_graphql(data)

140
tests/test_message.py Normal file
View File

@@ -0,0 +1,140 @@
import pytest
import fbchat
from fbchat._message import (
EmojiSize,
Mention,
Message,
graphql_to_extensible_attachment,
)
@pytest.mark.parametrize(
"tags,size",
[
(None, None),
(["hot_emoji_size:unknown"], None),
(["bunch", "of:different", "tags:large", "hot_emoji_size:s"], EmojiSize.SMALL),
(["hot_emoji_size:s"], EmojiSize.SMALL),
(["hot_emoji_size:m"], EmojiSize.MEDIUM),
(["hot_emoji_size:l"], EmojiSize.LARGE),
(["hot_emoji_size:small"], EmojiSize.SMALL),
(["hot_emoji_size:medium"], EmojiSize.MEDIUM),
(["hot_emoji_size:large"], EmojiSize.LARGE),
],
)
def test_emojisize_from_tags(tags, size):
assert size is EmojiSize._from_tags(tags)
def test_graphql_to_extensible_attachment_empty():
assert None is graphql_to_extensible_attachment({})
@pytest.mark.parametrize(
"obj,type_",
[
# UnsentMessage testing is done in test_attachment.py
(fbchat.LocationAttachment, "MessageLocation"),
(fbchat.LiveLocationAttachment, "MessageLiveLocation"),
(fbchat.ShareAttachment, "ExternalUrl"),
(fbchat.ShareAttachment, "Story"),
],
)
def test_graphql_to_extensible_attachment_dispatch(monkeypatch, obj, type_):
monkeypatch.setattr(obj, "_from_graphql", lambda data: True)
data = {"story_attachment": {"target": {"__typename": type_}}}
assert graphql_to_extensible_attachment(data)
def test_message_format_mentions():
expected = Message(
text="Hey 'Peter'! My name is Michael",
mentions=[
Mention(thread_id="1234", offset=4, length=7),
Mention(thread_id="4321", offset=24, length=7),
],
)
assert expected == Message.format_mentions(
"Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael")
)
assert expected == Message.format_mentions(
"Hey {p!r}! My name is {}", ("4321", "Michael"), p=("1234", "Peter")
)
def test_message_get_forwarded_from_tags():
assert not Message._get_forwarded_from_tags(None)
assert not Message._get_forwarded_from_tags(["hot_emoji_size:unknown"])
assert Message._get_forwarded_from_tags(
["attachment:photo", "inbox", "sent", "source:chat:forward", "tq"]
)
def test_message_to_send_data_minimal():
assert {"action_type": "ma-type:user-generated-message", "body": "Hey"} == Message(
text="Hey"
)._to_send_data()
def test_message_to_send_data_mentions():
msg = Message(
text="Hey 'Peter'! My name is Michael",
mentions=[
Mention(thread_id="1234", offset=4, length=7),
Mention(thread_id="4321", offset=24, length=7),
],
)
assert {
"action_type": "ma-type:user-generated-message",
"body": "Hey 'Peter'! My name is Michael",
"profile_xmd[0][id]": "1234",
"profile_xmd[0][length]": 7,
"profile_xmd[0][offset]": 4,
"profile_xmd[0][type]": "p",
"profile_xmd[1][id]": "4321",
"profile_xmd[1][length]": 7,
"profile_xmd[1][offset]": 24,
"profile_xmd[1][type]": "p",
} == msg._to_send_data()
def test_message_to_send_data_sticker():
msg = Message(sticker=fbchat.Sticker(uid="123"))
assert {
"action_type": "ma-type:user-generated-message",
"sticker_id": "123",
} == msg._to_send_data()
def test_message_to_send_data_emoji():
msg = Message(text="😀", emoji_size=EmojiSize.LARGE)
assert {
"action_type": "ma-type:user-generated-message",
"body": "😀",
"tags[0]": "hot_emoji_size:large",
} == msg._to_send_data()
msg = Message(emoji_size=EmojiSize.LARGE)
assert {
"action_type": "ma-type:user-generated-message",
"sticker_id": "369239383222810",
} == msg._to_send_data()
@pytest.mark.skip(reason="need to be added")
def test_message_to_send_data_quick_replies():
raise NotImplementedError
@pytest.mark.skip(reason="need to gather test data")
def test_message_from_graphql():
pass
@pytest.mark.skip(reason="need to gather test data")
def test_message_from_reply():
pass
@pytest.mark.skip(reason="need to gather test data")
def test_message_from_pull():
pass

20
tests/test_page.py Normal file
View File

@@ -0,0 +1,20 @@
from fbchat._page import Page
def test_page_from_graphql():
data = {
"id": "123456",
"name": "Some school",
"profile_picture": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/..."},
"url": "https://www.facebook.com/some-school/",
"category_type": "SCHOOL",
"city": None,
}
assert Page(
uid="123456",
photo="https://scontent-arn2-1.xx.fbcdn.net/v/...",
name="Some school",
url="https://www.facebook.com/some-school/",
city=None,
category="SCHOOL",
) == Page._from_graphql(data)

150
tests/test_plan.py Normal file
View File

@@ -0,0 +1,150 @@
import datetime
from fbchat._plan import GuestStatus, Plan
def test_plan_properties():
plan = Plan(time=..., title=...)
plan.guests = {
"1234": GuestStatus.INVITED,
"2345": GuestStatus.INVITED,
"3456": GuestStatus.GOING,
"4567": GuestStatus.DECLINED,
}
assert set(plan.invited) == {"1234", "2345"}
assert plan.going == ["3456"]
assert plan.declined == ["4567"]
def test_plan_from_pull():
data = {
"event_timezone": "",
"event_creator_id": "1234",
"event_id": "1111",
"event_type": "EVENT",
"event_track_rsvp": "1",
"event_title": "abc",
"event_time": "1500000000",
"event_seconds_to_notify_before": "3600",
"guest_state_list": (
'[{"guest_list_state":"INVITED","node":{"id":"1234"}},'
'{"guest_list_state":"INVITED","node":{"id":"2356"}},'
'{"guest_list_state":"DECLINED","node":{"id":"3456"}},'
'{"guest_list_state":"GOING","node":{"id":"4567"}}]'
),
}
plan = Plan(
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
title="abc",
)
plan.uid = "1111"
plan.author_id = "1234"
plan.guests = {
"1234": GuestStatus.INVITED,
"2356": GuestStatus.INVITED,
"3456": GuestStatus.DECLINED,
"4567": GuestStatus.GOING,
}
assert plan == Plan._from_pull(data)
def test_plan_from_fetch():
data = {
"message_thread_id": 123456789,
"event_time": 1500000000,
"creator_id": 1234,
"event_time_updated_time": 1450000000,
"title": "abc",
"track_rsvp": 1,
"event_type": "EVENT",
"status": "created",
"message_id": "mid.xyz",
"seconds_to_notify_before": 3600,
"event_time_source": "user",
"repeat_mode": "once",
"creation_time": 1400000000,
"location_id": 0,
"location_name": None,
"latitude": "",
"longitude": "",
"event_id": 0,
"trigger_message_id": "",
"note": "",
"timezone_id": 0,
"end_time": 0,
"list_id": 0,
"payload_id": 0,
"cu_app": "",
"location_sharing_subtype": "",
"reminder_notif_param": [],
"workplace_meeting_id": "",
"genie_fbid": 0,
"galaxy": "",
"oid": 1111,
"type": 8128,
"is_active": True,
"location_address": None,
"event_members": {
"1234": "INVITED",
"2356": "INVITED",
"3456": "DECLINED",
"4567": "GOING",
},
}
plan = Plan(
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
title="abc",
location="",
location_id="",
)
plan.uid = 1111
plan.author_id = 1234
plan.guests = {
"1234": GuestStatus.INVITED,
"2356": GuestStatus.INVITED,
"3456": GuestStatus.DECLINED,
"4567": GuestStatus.GOING,
}
assert plan == Plan._from_fetch(data)
def test_plan_from_graphql():
data = {
"id": "1111",
"lightweight_event_creator": {"id": "1234"},
"time": 1500000000,
"lightweight_event_type": "EVENT",
"location_name": None,
"location_coordinates": None,
"location_page": None,
"lightweight_event_status": "CREATED",
"note": "",
"repeat_mode": "ONCE",
"event_title": "abc",
"trigger_message": None,
"seconds_to_notify_before": 3600,
"allows_rsvp": True,
"related_event": None,
"event_reminder_members": {
"edges": [
{"node": {"id": "1234"}, "guest_list_state": "INVITED"},
{"node": {"id": "2356"}, "guest_list_state": "INVITED"},
{"node": {"id": "3456"}, "guest_list_state": "DECLINED"},
{"node": {"id": "4567"}, "guest_list_state": "GOING"},
]
},
}
plan = Plan(
time=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
title="abc",
location="",
location_id="",
)
plan.uid = "1111"
plan.author_id = "1234"
plan.guests = {
"1234": GuestStatus.INVITED,
"2356": GuestStatus.INVITED,
"3456": GuestStatus.DECLINED,
"4567": GuestStatus.GOING,
}
assert plan == Plan._from_graphql(data)

87
tests/test_poll.py Normal file
View File

@@ -0,0 +1,87 @@
from fbchat._poll import Poll, PollOption
def test_poll_option_from_graphql_unvoted():
data = {
"id": "123456789",
"text": "abc",
"total_count": 0,
"viewer_has_voted": "false",
"voters": [],
}
assert PollOption(
text="abc", vote=False, voters=[], votes_count=0, uid=123456789
) == PollOption._from_graphql(data)
def test_poll_option_from_graphql_voted():
data = {
"id": "123456789",
"text": "abc",
"total_count": 2,
"viewer_has_voted": "true",
"voters": ["1234", "2345"],
}
assert PollOption(
text="abc", vote=True, voters=["1234", "2345"], votes_count=2, uid=123456789
) == PollOption._from_graphql(data)
def test_poll_option_from_graphql_alternate_format():
# Format received when fetching poll options
data = {
"id": "123456789",
"text": "abc",
"viewer_has_voted": True,
"voters": {
"count": 2,
"edges": [{"node": {"id": "1234"}}, {"node": {"id": "2345"}}],
},
}
assert PollOption(
text="abc", vote=True, voters=["1234", "2345"], votes_count=2, uid=123456789
) == PollOption._from_graphql(data)
def test_poll_from_graphql():
data = {
"id": "123456789",
"text": "Some poll",
"total_count": 5,
"viewer_has_voted": "true",
"options": [
{
"id": "1111",
"text": "Abc",
"total_count": 1,
"viewer_has_voted": "true",
"voters": ["1234"],
},
{
"id": "2222",
"text": "Def",
"total_count": 2,
"viewer_has_voted": "false",
"voters": ["2345", "3456"],
},
{
"id": "3333",
"text": "Ghi",
"total_count": 0,
"viewer_has_voted": "false",
"voters": [],
},
],
}
assert Poll(
title="Some poll",
options=[
PollOption(text="Abc", vote=True, voters=["1234"], votes_count=1, uid=1111),
PollOption(
text="Def", vote=False, voters=["2345", "3456"], votes_count=2, uid=2222
),
PollOption(text="Ghi", vote=False, voters=[], votes_count=0, uid=3333),
],
options_count=5,
uid=123456789,
) == Poll._from_graphql(data)

49
tests/test_quick_reply.py Normal file
View File

@@ -0,0 +1,49 @@
from fbchat._quick_reply import (
QuickReplyText,
QuickReplyLocation,
QuickReplyPhoneNumber,
QuickReplyEmail,
graphql_to_quick_reply,
)
def test_parse_minimal():
data = {
"content_type": "text",
"payload": None,
"external_payload": None,
"data": None,
"title": "A",
"image_url": None,
}
assert QuickReplyText(title="A") == graphql_to_quick_reply(data)
data = {"content_type": "location"}
assert QuickReplyLocation() == graphql_to_quick_reply(data)
data = {"content_type": "user_phone_number"}
assert QuickReplyPhoneNumber() == graphql_to_quick_reply(data)
data = {"content_type": "user_email"}
assert QuickReplyEmail() == graphql_to_quick_reply(data)
def test_parse_text_full():
data = {
"content_type": "text",
"title": "A",
"payload": "Some payload",
"image_url": "https://example.com/image.jpg",
"data": None,
}
assert QuickReplyText(
payload="Some payload",
data=None,
is_response=False,
title="A",
image_url="https://example.com/image.jpg",
) == graphql_to_quick_reply(data)
def test_parse_with_is_response():
data = {"content_type": "text"}
assert QuickReplyText(is_response=True) == graphql_to_quick_reply(
data, is_response=True
)

86
tests/test_sticker.py Normal file
View File

@@ -0,0 +1,86 @@
import pytest
from fbchat._sticker import Sticker
def test_from_graphql_none():
assert None == Sticker._from_graphql(None)
def test_from_graphql_minimal():
assert Sticker(uid=1) == Sticker._from_graphql({"id": 1})
def test_from_graphql_normal():
assert Sticker(
uid="369239383222810",
pack="227877430692340",
is_animated=False,
medium_sprite_image=None,
large_sprite_image=None,
frames_per_row=None,
frames_per_col=None,
frame_rate=None,
url="https://scontent-arn2-1.xx.fbcdn.net/v/redacted.png",
width=274,
height=274,
label="Like, thumbs up",
) == Sticker._from_graphql(
{
"id": "369239383222810",
"pack": {"id": "227877430692340"},
"label": "Like, thumbs up",
"frame_count": 1,
"frame_rate": 83,
"frames_per_row": 1,
"frames_per_column": 1,
"sprite_image_2x": None,
"sprite_image": None,
"padded_sprite_image": None,
"padded_sprite_image_2x": None,
"url": "https://scontent-arn2-1.xx.fbcdn.net/v/redacted.png",
"height": 274,
"width": 274,
}
)
def test_from_graphql_animated():
assert Sticker(
uid="144885035685763",
pack="350357561732812",
is_animated=True,
medium_sprite_image="https://scontent-arn2-1.xx.fbcdn.net/v/redacted2.png",
large_sprite_image="https://scontent-arn2-1.fbcdn.net/v/redacted3.png",
frames_per_row=2,
frames_per_col=2,
frame_rate=142,
url="https://scontent-arn2-1.fbcdn.net/v/redacted1.png",
width=240,
height=293,
label="Love, cat with heart",
) == Sticker._from_graphql(
{
"id": "144885035685763",
"pack": {"id": "350357561732812"},
"label": "Love, cat with heart",
"frame_count": 4,
"frame_rate": 142,
"frames_per_row": 2,
"frames_per_column": 2,
"sprite_image_2x": {
"uri": "https://scontent-arn2-1.fbcdn.net/v/redacted3.png"
},
"sprite_image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/redacted2.png"
},
"padded_sprite_image": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/unused1.png"
},
"padded_sprite_image_2x": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/unused2.png"
},
"url": "https://scontent-arn2-1.fbcdn.net/v/redacted1.png",
"height": 293,
"width": 240,
}
)

65
tests/test_thread.py Normal file
View File

@@ -0,0 +1,65 @@
import pytest
import fbchat
from fbchat._thread import ThreadType, ThreadColor, Thread
def test_thread_type_to_class():
assert fbchat.User == ThreadType.USER._to_class()
assert fbchat.Group == ThreadType.GROUP._to_class()
assert fbchat.Page == ThreadType.PAGE._to_class()
def test_thread_color_from_graphql():
assert None is ThreadColor._from_graphql(None)
assert ThreadColor.MESSENGER_BLUE is ThreadColor._from_graphql("")
assert ThreadColor.VIKING is ThreadColor._from_graphql("FF44BEC7")
assert ThreadColor._from_graphql("DEADBEEF") is getattr(
ThreadColor, "UNKNOWN_#ADBEEF"
)
def test_thread_parse_customization_info_empty():
assert {} == Thread._parse_customization_info(None)
assert {} == Thread._parse_customization_info({"customization_info": None})
def test_thread_parse_customization_info_group():
data = {
"thread_key": {"thread_fbid": "11111", "other_user_id": None},
"customization_info": {
"emoji": "🎉",
"participant_customizations": [
{"participant_id": "123456789", "nickname": "A"},
{"participant_id": "987654321", "nickname": "B"},
],
"outgoing_bubble_color": "FFFF5CA1",
},
"customization_enabled": True,
"thread_type": "GROUP",
# ... Other irrelevant fields
}
expected = {
"emoji": "🎉",
"color": ThreadColor.BRILLIANT_ROSE,
"nicknames": {"123456789": "A", "987654321": "B"},
}
assert expected == Thread._parse_customization_info(data)
def test_thread_parse_customization_info_user():
data = {
"thread_key": {"thread_fbid": None, "other_user_id": "987654321"},
"customization_info": {
"emoji": None,
"participant_customizations": [
{"participant_id": "123456789", "nickname": "A"},
{"participant_id": "987654321", "nickname": "B"},
],
"outgoing_bubble_color": None,
},
"customization_enabled": True,
"thread_type": "ONE_TO_ONE",
# ... Other irrelevant fields
}
expected = {"emoji": None, "color": None, "own_nickname": "A", "nickname": "B"}
assert expected == Thread._parse_customization_info(data)

194
tests/test_user.py Normal file
View File

@@ -0,0 +1,194 @@
import pytest
import datetime
from fbchat._user import User, ActiveStatus
def test_user_from_graphql():
data = {
"id": "1234",
"name": "Abc Def Ghi",
"first_name": "Abc",
"last_name": "Ghi",
"profile_picture": {"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/..."},
"is_viewer_friend": True,
"url": "https://www.facebook.com/profile.php?id=1234",
"gender": "FEMALE",
"viewer_affinity": 0.4560002,
}
assert User(
uid="1234",
photo="https://scontent-arn2-1.xx.fbcdn.net/v/...",
name="Abc Def Ghi",
url="https://www.facebook.com/profile.php?id=1234",
first_name="Abc",
last_name="Ghi",
is_friend=True,
gender="female_singular",
) == User._from_graphql(data)
def test_user_from_thread_fetch():
data = {
"thread_key": {"thread_fbid": None, "other_user_id": "1234"},
"name": None,
"last_message": {
"nodes": [
{
"snippet": "aaa",
"message_sender": {"messaging_actor": {"id": "1234"}},
"timestamp_precise": "1500000000000",
"commerce_message_type": None,
"extensible_attachment": None,
"sticker": None,
"blob_attachments": [],
}
]
},
"unread_count": 0,
"messages_count": 1111,
"image": None,
"updated_time_precise": "1500000000000",
"mute_until": None,
"is_pin_protected": False,
"is_viewer_subscribed": True,
"thread_queue_enabled": False,
"folder": "INBOX",
"has_viewer_archived": False,
"is_page_follow_up": False,
"cannot_reply_reason": None,
"ephemeral_ttl_mode": 0,
"customization_info": {
"emoji": None,
"participant_customizations": [
{"participant_id": "4321", "nickname": "B"},
{"participant_id": "1234", "nickname": "A"},
],
"outgoing_bubble_color": None,
},
"thread_admins": [],
"approval_mode": None,
"joinable_mode": {"mode": "0", "link": ""},
"thread_queue_metadata": None,
"event_reminders": {"nodes": []},
"montage_thread": None,
"last_read_receipt": {"nodes": [{"timestamp_precise": "1500000050000"}]},
"related_page_thread": None,
"rtc_call_data": {
"call_state": "NO_ONGOING_CALL",
"server_info_data": "",
"initiator": None,
},
"associated_object": None,
"privacy_mode": 1,
"reactions_mute_mode": "REACTIONS_NOT_MUTED",
"mentions_mute_mode": "MENTIONS_NOT_MUTED",
"customization_enabled": True,
"thread_type": "ONE_TO_ONE",
"participant_add_mode_as_string": None,
"is_canonical_neo_user": False,
"participants_event_status": [],
"page_comm_item": None,
"all_participants": {
"nodes": [
{
"messaging_actor": {
"id": "1234",
"__typename": "User",
"name": "Abc Def Ghi",
"gender": "FEMALE",
"url": "https://www.facebook.com/profile.php?id=1234",
"big_image_src": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/..."
},
"short_name": "Abc",
"username": "",
"is_viewer_friend": True,
"is_messenger_user": True,
"is_verified": False,
"is_message_blocked_by_viewer": False,
"is_viewer_coworker": False,
"is_employee": None,
}
},
{
"messaging_actor": {
"id": "4321",
"__typename": "User",
"name": "Aaa Bbb Ccc",
"gender": "NEUTER",
"url": "https://www.facebook.com/aaabbbccc",
"big_image_src": {
"uri": "https://scontent-arn2-1.xx.fbcdn.net/v/..."
},
"short_name": "Aaa",
"username": "aaabbbccc",
"is_viewer_friend": False,
"is_messenger_user": True,
"is_verified": False,
"is_message_blocked_by_viewer": False,
"is_viewer_coworker": False,
"is_employee": None,
}
},
]
},
"read_receipts": ...,
"delivery_receipts": ...,
}
assert User(
uid="1234",
photo="https://scontent-arn2-1.xx.fbcdn.net/v/...",
name="Abc Def Ghi",
last_active=datetime.datetime(2017, 7, 14, 2, 40, tzinfo=datetime.timezone.utc),
message_count=1111,
url="https://www.facebook.com/profile.php?id=1234",
first_name="Abc",
last_name="Def Ghi",
is_friend=True,
gender="female_singular",
nickname="A",
own_nickname="B",
color=None,
emoji=None,
) == User._from_thread_fetch(data)
def test_user_from_all_fetch():
data = {
"id": "1234",
"name": "Abc Def Ghi",
"firstName": "Abc",
"vanity": "",
"thumbSrc": "https://scontent-arn2-1.xx.fbcdn.net/v/...",
"uri": "https://www.facebook.com/profile.php?id=1234",
"gender": 1,
"i18nGender": 2,
"type": "friend",
"is_friend": True,
"mThumbSrcSmall": None,
"mThumbSrcLarge": None,
"dir": None,
"searchTokens": ["Abc", "Ghi"],
"alternateName": "",
"is_nonfriend_messenger_contact": False,
"is_blocked": False,
}
assert User(
uid="1234",
photo="https://scontent-arn2-1.xx.fbcdn.net/v/...",
name="Abc Def Ghi",
url="https://www.facebook.com/profile.php?id=1234",
first_name="Abc",
is_friend=True,
gender="female_singular",
) == User._from_all_fetch(data)
@pytest.mark.skip(reason="can't gather test data, the pulling is broken")
def test_active_status_from_chatproxy_presence():
assert ActiveStatus() == ActiveStatus._from_chatproxy_presence(data)
@pytest.mark.skip(reason="can't gather test data, the pulling is broken")
def test_active_status_from_buddylist_overlay():
assert ActiveStatus() == ActiveStatus._from_buddylist_overlay(data)

309
tests/test_util.py Normal file
View File

@@ -0,0 +1,309 @@
import pytest
import fbchat
import datetime
from fbchat._util import (
strip_json_cruft,
parse_json,
str_base,
generate_message_id,
get_signature_id,
handle_payload_error,
handle_graphql_errors,
check_http_code,
get_jsmods_require,
require_list,
mimetype_to_key,
get_url_parameter,
prefix_url,
seconds_to_datetime,
millis_to_datetime,
datetime_to_seconds,
datetime_to_millis,
seconds_to_timedelta,
millis_to_timedelta,
timedelta_to_seconds,
)
def test_strip_json_cruft():
assert strip_json_cruft('for(;;);{"abc": "def"}') == '{"abc": "def"}'
assert strip_json_cruft('{"abc": "def"}') == '{"abc": "def"}'
def test_strip_json_cruft_invalid():
with pytest.raises(AttributeError):
strip_json_cruft(None)
with pytest.raises(fbchat.FBchatException, match="No JSON object found"):
strip_json_cruft("No JSON object here!")
def test_parse_json():
assert parse_json('{"a":"b"}') == {"a": "b"}
def test_parse_json_invalid():
with pytest.raises(fbchat.FBchatFacebookError, match="Error while parsing JSON"):
parse_json("No JSON object here!")
@pytest.mark.parametrize(
"number,base,expected",
[
(123, 10, "123"),
(1, 36, "1"),
(10, 36, "a"),
(123, 36, "3f"),
(1000, 36, "rs"),
(123456789, 36, "21i3v9"),
],
)
def test_str_base(number, base, expected):
assert str_base(number, base) == expected
def test_generate_message_id():
# Returns random output, so hard to test more thoroughly
generate_message_id("abc")
def test_get_signature_id():
# Returns random output, so hard to test more thoroughly
get_signature_id()
ERROR_DATA = [
(
fbchat._exception.FBchatNotLoggedIn,
1357001,
"Not logged in",
"Please log in to continue.",
),
(
fbchat._exception.FBchatPleaseRefresh,
1357004,
"Sorry, something went wrong",
"Please try closing and re-opening your browser window.",
),
(
fbchat._exception.FBchatInvalidParameters,
1357031,
"This content is no longer available",
(
"The content you requested cannot be displayed at the moment. It may be"
" temporarily unavailable, the link you clicked on may have expired or you"
" may not have permission to view this page."
),
),
(
fbchat._exception.FBchatInvalidParameters,
1545010,
"Messages Unavailable",
(
"Sorry, messages are temporarily unavailable."
" Please try again in a few minutes."
),
),
(
fbchat.FBchatFacebookError,
1545026,
"Unable to Attach File",
(
"The type of file you're trying to attach isn't allowed."
" Please try again with a different format."
),
),
(
fbchat._exception.FBchatInvalidParameters,
1545003,
"Invalid action",
"You cannot perform that action.",
),
(
fbchat.FBchatFacebookError,
1545012,
"Temporary Failure",
"There was a temporary error, please try again.",
),
]
@pytest.mark.parametrize("exception,code,description,summary", ERROR_DATA)
def test_handle_payload_error(exception, code, summary, description):
data = {"error": code, "errorSummary": summary, "errorDescription": description}
with pytest.raises(exception, match=r"Error #\d+ when sending request"):
handle_payload_error(data)
def test_handle_payload_error_no_error():
assert handle_payload_error({}) is None
assert handle_payload_error({"payload": {"abc": ["Something", "else"]}}) is None
def test_handle_graphql_errors():
error = {
"allow_user_retry": False,
"api_error_code": -1,
"code": 1675030,
"debug_info": None,
"description": "Error performing query.",
"fbtrace_id": "CLkuLR752sB",
"is_silent": False,
"is_transient": False,
"message": (
'Errors while executing operation "MessengerThreadSharedLinks":'
" At Query.message_thread: Field implementation threw an exception."
" Check your server logs for more information."
),
"path": ["message_thread"],
"query_path": None,
"requires_reauth": False,
"severity": "CRITICAL",
"summary": "Query error",
}
with pytest.raises(fbchat.FBchatFacebookError, match="GraphQL error"):
handle_graphql_errors({"data": {"message_thread": None}, "errors": [error]})
def test_handle_graphql_errors_singular_error_key():
with pytest.raises(fbchat.FBchatFacebookError, match="GraphQL error #123"):
handle_graphql_errors({"error": {"code": 123}})
def test_handle_graphql_errors_no_error():
assert handle_graphql_errors({"data": {"message_thread": None}}) is None
def test_check_http_code():
with pytest.raises(fbchat.FBchatFacebookError):
check_http_code(400)
with pytest.raises(fbchat.FBchatFacebookError):
check_http_code(500)
def test_check_http_code_404_handling():
with pytest.raises(fbchat.FBchatFacebookError, match="invalid id"):
check_http_code(404)
def test_check_http_code_no_error():
assert check_http_code(200) is None
assert check_http_code(302) is None
def test_get_jsmods_require_get_image_url():
data = {
"__ar": 1,
"payload": None,
"jsmods": {
"require": [
[
"ServerRedirect",
"redirectPageTo",
[],
[
"https://scontent-arn2-1.xx.fbcdn.net/v/image.png&dl=1",
False,
False,
],
],
["TuringClientSignalCollectionTrigger", ..., [], ...],
["TuringClientSignalCollectionTrigger", "retrieveSignals", [], ...],
["BanzaiODS"],
["BanzaiScuba"],
],
"define": ...,
},
"js": ...,
"css": ...,
"bootloadable": ...,
"resource_map": ...,
"ixData": {},
"bxData": {},
"gkxData": ...,
"qexData": {},
"lid": "123",
}
url = "https://scontent-arn2-1.xx.fbcdn.net/v/image.png&dl=1"
assert get_jsmods_require(data, 3) == url
def test_require_list():
assert require_list([]) == set()
assert require_list([1, 2, 2]) == {1, 2}
assert require_list(1) == {1}
assert require_list("abc") == {"abc"}
def test_mimetype_to_key():
assert mimetype_to_key(None) == "file_id"
assert mimetype_to_key("image/gif") == "gif_id"
assert mimetype_to_key("video/mp4") == "video_id"
assert mimetype_to_key("video/quicktime") == "video_id"
assert mimetype_to_key("image/png") == "image_id"
assert mimetype_to_key("image/jpeg") == "image_id"
assert mimetype_to_key("audio/mpeg") == "audio_id"
assert mimetype_to_key("application/json") == "file_id"
def test_get_url_parameter():
assert get_url_parameter("http://example.com?a=b&c=d", "c") == "d"
assert get_url_parameter("http://example.com?a=b&a=c", "a") == "b"
with pytest.raises(IndexError):
get_url_parameter("http://example.com", "a")
def test_prefix_url():
assert prefix_url("/") == "https://www.facebook.com/"
assert prefix_url("/abc") == "https://www.facebook.com/abc"
assert prefix_url("abc") == "abc"
assert prefix_url("https://m.facebook.com/abc") == "https://m.facebook.com/abc"
DT_0 = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
DT = datetime.datetime(2018, 11, 16, 1, 51, 4, 162000, tzinfo=datetime.timezone.utc)
DT_NO_TIMEZONE = datetime.datetime(2018, 11, 16, 1, 51, 4, 162000)
def test_seconds_to_datetime():
assert seconds_to_datetime(0) == DT_0
assert seconds_to_datetime(1542333064.162) == DT
assert seconds_to_datetime(1542333064.162) != DT_NO_TIMEZONE
def test_millis_to_datetime():
assert millis_to_datetime(0) == DT_0
assert millis_to_datetime(1542333064162) == DT
assert millis_to_datetime(1542333064162) != DT_NO_TIMEZONE
def test_datetime_to_seconds():
assert datetime_to_seconds(DT_0) == 0
assert datetime_to_seconds(DT) == 1542333064 # Rounded
datetime_to_seconds(DT_NO_TIMEZONE) # Depends on system timezone
def test_datetime_to_millis():
assert datetime_to_millis(DT_0) == 0
assert datetime_to_millis(DT) == 1542333064162
datetime_to_millis(DT_NO_TIMEZONE) # Depends on system timezone
def test_seconds_to_timedelta():
assert seconds_to_timedelta(0.001) == datetime.timedelta(microseconds=1000)
assert seconds_to_timedelta(1) == datetime.timedelta(seconds=1)
assert seconds_to_timedelta(3600) == datetime.timedelta(hours=1)
assert seconds_to_timedelta(86400) == datetime.timedelta(days=1)
def test_millis_to_timedelta():
assert millis_to_timedelta(1) == datetime.timedelta(microseconds=1000)
assert millis_to_timedelta(1000) == datetime.timedelta(seconds=1)
assert millis_to_timedelta(3600000) == datetime.timedelta(hours=1)
assert millis_to_timedelta(86400000) == datetime.timedelta(days=1)
def test_timedelta_to_seconds():
assert timedelta_to_seconds(datetime.timedelta(microseconds=1000)) == 0 # Rounded
assert timedelta_to_seconds(datetime.timedelta(seconds=1)) == 1
assert timedelta_to_seconds(datetime.timedelta(hours=1)) == 3600
assert timedelta_to_seconds(datetime.timedelta(days=1)) == 86400