Add more session tests and small error improvements
This commit is contained in:
@@ -4,7 +4,10 @@ import requests
|
||||
import random
|
||||
import re
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
# TODO: Only import when required
|
||||
# Or maybe just replace usage with `html.parser`?
|
||||
import bs4
|
||||
|
||||
from ._common import log, kw_only
|
||||
from . import _graphql, _util, _exception
|
||||
@@ -27,6 +30,10 @@ def parse_server_js_define(html: str) -> Mapping[str, Any]:
|
||||
rtn = []
|
||||
if not define_splits:
|
||||
raise _exception.ParseError("Could not find any ServerJSDefine", data=html)
|
||||
if len(define_splits) < 2:
|
||||
raise _exception.ParseError("Could not find enough ServerJSDefine", data=html)
|
||||
if len(define_splits) > 2:
|
||||
raise _exception.ParseError("Found too many ServerJSDefine", data=define_splits)
|
||||
# Parse entries (should be two)
|
||||
for entry in define_splits:
|
||||
try:
|
||||
@@ -94,16 +101,7 @@ def client_id_factory() -> str:
|
||||
return hex(int(random.random() * 2 ** 31))[2:]
|
||||
|
||||
|
||||
def get_next_url(url: str) -> Optional[str]:
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
query = urllib.parse.parse_qs(parsed_url.query)
|
||||
return query.get("next", [None])[0]
|
||||
|
||||
|
||||
def find_form_request(html: str):
|
||||
# Only import when required
|
||||
import bs4
|
||||
|
||||
soup = bs4.BeautifulSoup(html, "html.parser", parse_only=bs4.SoupStrainer("form"))
|
||||
|
||||
form = soup.form
|
||||
@@ -114,6 +112,7 @@ def find_form_request(html: str):
|
||||
if not url:
|
||||
raise _exception.ParseError("Could not find url to submit to", data=form)
|
||||
|
||||
# From what I've seen, it'll always do this!
|
||||
if url.startswith("/"):
|
||||
url = "https://www.facebook.com" + url
|
||||
|
||||
@@ -168,15 +167,12 @@ def two_factor_helper(session: requests.Session, r, on_2fa_callback):
|
||||
|
||||
def get_error_data(html: str) -> Optional[str]:
|
||||
"""Get error message from a request."""
|
||||
# Only import when required
|
||||
import bs4
|
||||
|
||||
soup = bs4.BeautifulSoup(
|
||||
html, "html.parser", parse_only=bs4.SoupStrainer("form", id="login_form")
|
||||
)
|
||||
# Attempt to extract and format the error string
|
||||
# The error message is in the user's own language!
|
||||
return ". ".join(list(soup.stripped_strings)[:2]) or None
|
||||
return " ".join(list(soup.stripped_strings)[1:3]) or None
|
||||
|
||||
|
||||
def get_fb_dtsg(define) -> Optional[str]:
|
||||
@@ -298,7 +294,7 @@ class Session:
|
||||
)
|
||||
# Get a facebook.com url that handles the 2FA flow
|
||||
# This probably works differently for Messenger-only accounts
|
||||
url = get_next_url(url)
|
||||
url = _util.get_url_parameter(url, "next")
|
||||
# Explicitly allow redirects
|
||||
r = session.get(url, allow_redirects=True)
|
||||
url = two_factor_helper(session, r, on_2fa_callback)
|
||||
|
@@ -1,15 +1,47 @@
|
||||
import datetime
|
||||
import pytest
|
||||
from fbchat import ParseError
|
||||
from fbchat._session import (
|
||||
parse_server_js_define,
|
||||
base36encode,
|
||||
prefix_url,
|
||||
generate_message_id,
|
||||
session_factory,
|
||||
client_id_factory,
|
||||
is_home,
|
||||
find_form_request,
|
||||
get_error_data,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_server_js_define():
|
||||
html = """
|
||||
some data;require("TimeSliceImpl").guard(function(){(require("ServerJSDefine")).handleDefines([["DTSGInitialData",[],{"token":"123"},100]])
|
||||
|
||||
<script>require("TimeSliceImpl").guard(function() {require("ServerJSDefine").handleDefines([["DTSGInitData",[],{"token":"123","async_get_token":"12345"},3333]])
|
||||
|
||||
</script>
|
||||
other irrelevant data
|
||||
"""
|
||||
define = parse_server_js_define(html)
|
||||
assert define == {
|
||||
"DTSGInitialData": {"token": "123"},
|
||||
"DTSGInitData": {"async_get_token": "12345", "token": "123"},
|
||||
}
|
||||
|
||||
|
||||
def test_parse_server_js_define_error():
|
||||
with pytest.raises(ParseError, match="Could not find any"):
|
||||
parse_server_js_define("")
|
||||
|
||||
html = 'function(){(require("ServerJSDefine")).handleDefines([{"a": function(){}}])'
|
||||
with pytest.raises(ParseError, match="Invalid"):
|
||||
parse_server_js_define(html + html)
|
||||
|
||||
html = 'function(){require("ServerJSDefine").handleDefines({"a": "b"})'
|
||||
with pytest.raises(ParseError, match="Invalid"):
|
||||
parse_server_js_define(html + html)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"number,expected",
|
||||
[(1, "1"), (10, "a"), (123, "3f"), (1000, "rs"), (123456789, "21i3v9")],
|
||||
@@ -19,8 +51,10 @@ def test_base36encode(number, expected):
|
||||
|
||||
|
||||
def test_prefix_url():
|
||||
assert prefix_url("/") == "https://www.facebook.com/"
|
||||
assert prefix_url("/abc") == "https://www.facebook.com/abc"
|
||||
static_url = "https://upload.messenger.com/"
|
||||
assert prefix_url(static_url) == static_url
|
||||
assert prefix_url("/") == "https://www.messenger.com/"
|
||||
assert prefix_url("/abc") == "https://www.messenger.com/abc"
|
||||
|
||||
|
||||
def test_generate_message_id():
|
||||
@@ -28,46 +62,106 @@ def test_generate_message_id():
|
||||
assert generate_message_id(datetime.datetime.utcnow(), "def")
|
||||
|
||||
|
||||
def test_session_factory():
|
||||
session = session_factory()
|
||||
assert session.headers
|
||||
|
||||
|
||||
def test_client_id_factory():
|
||||
# Returns random output, so hard to test more thoroughly
|
||||
assert client_id_factory()
|
||||
|
||||
|
||||
def test_is_home():
|
||||
assert not is_home("https://m.facebook.com/login/?...")
|
||||
assert is_home("https://m.facebook.com/home.php?refsrc=...")
|
||||
def test_find_form_request():
|
||||
html = """
|
||||
<div>
|
||||
<form action="/checkpoint/?next=https%3A%2F%2Fwww.messenger.com%2F" class="checkpoint" id="u_0_c" method="post" onsubmit="">
|
||||
<input autocomplete="off" name="jazoest" type="hidden" value="some-number" />
|
||||
<input autocomplete="off" name="fb_dtsg" type="hidden" value="some-base64" />
|
||||
<input class="hidden_elem" data-default-submit="true" name="submit[Continue]" type="submit" />
|
||||
<input autocomplete="off" name="nh" type="hidden" value="some-hex" />
|
||||
<div class="_4-u2 _5x_7 _p0k _5x_9 _4-u8">
|
||||
<div class="_2e9n" id="u_0_d">
|
||||
<strong id="u_0_e">Two factor authentication required</strong>
|
||||
<div id="u_0_f"></div>
|
||||
</div>
|
||||
<div class="_2ph_">
|
||||
<input autocomplete="off" name="no_fido" type="hidden" value="true" />
|
||||
<div class="_50f4">You've asked us to require a 6-digit login code when anyone tries to access your account from a new device or browser.</div>
|
||||
<div class="_3-8y _50f4">Enter the 6-digit code from your Code Generator or 3rd party app below.</div>
|
||||
<div class="_2pie _2pio">
|
||||
<span>
|
||||
<input aria-label="Login code" autocomplete="off" class="inputtext" id="approvals_code" name="approvals_code" placeholder="Login code" tabindex="1" type="text" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_5hzs" id="checkpointBottomBar">
|
||||
<div class="_2s5p">
|
||||
<button class="_42ft _4jy0 _2kak _4jy4 _4jy1 selected _51sy" id="checkpointSubmitButton" name="submit[Continue]" type="submit" value="Continue">Continue</button>
|
||||
</div>
|
||||
<div class="_2s5q">
|
||||
<div class="_25b6" id="u_0_g">
|
||||
<a href="#" id="u_0_h" role="button">Need another way to authenticate?</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
url, data = find_form_request(html)
|
||||
|
||||
|
||||
def test_find_form_request_error():
|
||||
with pytest.raises(ParseError, match="Could not find form to submit"):
|
||||
assert find_form_request("")
|
||||
with pytest.raises(ParseError, match="Could not find url to submit to"):
|
||||
assert find_form_request("<form></form>")
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_get_error_data():
|
||||
html = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
html = """<!DOCTYPE html>
|
||||
<html lang="da" id="facebook" class="no_js">
|
||||
|
||||
<head>
|
||||
<title>Log in to Facebook | Facebook</title>
|
||||
<meta name="referrer" content="origin-when-crossorigin" id="meta_referrer" />
|
||||
<style type="text/css">...</style>
|
||||
<meta name="description" content="..." />
|
||||
<link rel="canonical" href="https://www.facebook.com/login/" />
|
||||
<meta charset="utf-8" />
|
||||
<title id="pageTitle">Messenger</title>
|
||||
<meta name="referrer" content="default" id="meta_referrer" />
|
||||
</head>
|
||||
|
||||
<body tabindex="0" class="b c d e f g">
|
||||
<div class="h"><div id="viewport">...<div id="objects_container"><div class="g" id="root" role="main">
|
||||
<table class="x" role="presentation"><tbody><tr><td class="y">
|
||||
<div class="z ba bb" style="" id="login_error">
|
||||
<div class="bc">
|
||||
<span>The password you entered is incorrect. <a href="/recover/initiate/?ars=facebook_login_pw_error&email=abc@mail.com&__ccr=XXX" class="bd" aria-label="Have you forgotten your password?">Did you forget your password?</a></span>
|
||||
<body class="_605a x1 Locale_da_DK" dir="ltr">
|
||||
<div class="_3v_o" id="XMessengerDotComLoginViewPlaceholder">
|
||||
<form id="login_form" action="/login/password/" method="post" onsubmit="">
|
||||
<input type="hidden" name="jazoest" value="2222" autocomplete="off" />
|
||||
<input type="hidden" name="lsd" value="xyz-abc" autocomplete="off" />
|
||||
<div class="_3403 _3404">
|
||||
<div>Type your password again</div>
|
||||
<div>The password you entered is incorrect. <a href="https://www.facebook.com/recover/initiate?ars=facebook_login_pw_error">Did you forget your password?</a></div>
|
||||
</div>
|
||||
<div id="loginform">
|
||||
<input type="hidden" autocomplete="off" id="initial_request_id" name="initial_request_id" value="xxx" />
|
||||
<input type="hidden" autocomplete="off" name="timezone" value="" id="u_0_1" />
|
||||
<input type="hidden" autocomplete="off" name="lgndim" value="" id="u_0_2" />
|
||||
<input type="hidden" name="lgnrnd" value="aaa" />
|
||||
<input type="hidden" id="lgnjs" name="lgnjs" value="n" />
|
||||
<input type="text" class="inputtext _55r1 _43di" id="email" name="email" placeholder="E-mail or phone number" value="some@email.com" tabindex="0" aria-label="E-mail or phone number" />
|
||||
<input type="password" class="inputtext _55r1 _43di" name="pass" id="pass" tabindex="0" placeholder="Password" aria-label="Password" />
|
||||
<button value="1" class="_42ft _4jy0 _2m_r _43dh _4jy4 _517h _51sy" id="loginbutton" name="login" tabindex="0" type="submit">Continue</button>
|
||||
<div class="_43dj">
|
||||
<div class="uiInputLabel clearfix">
|
||||
<label class="uiInputLabelInput">
|
||||
<input type="checkbox" value="1" name="persistent" tabindex="0" class="" id="u_0_0" />
|
||||
<span class=""></span>
|
||||
</label>
|
||||
<label for="u_0_0" class="uiInputLabelLabel">Stay logged in</label>
|
||||
</div>
|
||||
<input type="hidden" autocomplete="off" id="default_persistent" name="default_persistent" value="0" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
...
|
||||
</td></tr></tbody></table>
|
||||
<div style="display:none"></div><span><img src="https://facebook.com/security/hsts-pixel.gif" width="0" height="0" style="display:none" /></span>
|
||||
</div></div><div></div></div></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
"""
|
||||
url = "https://m.facebook.com/login/?email=abc@mail.com&li=XXX&e=1348092"
|
||||
msg = "The password you entered is incorrect. Did you forget your password?"
|
||||
assert (1348092, msg) == get_error_data(html)
|
||||
assert msg == get_error_data(html)
|
||||
|
Reference in New Issue
Block a user