Skip to content

Commit 9fb6620

Browse files
committed
Add some new tests for coverage
1 parent 6877137 commit 9fb6620

20 files changed

+390
-719
lines changed

.codacy.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exclude_paths:
2+
- tests/*
3+
- README.md

.coveragerc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
[run]
22
source = cloudscraper
3-
omit =
4-
*test*
5-
cloudscraper/interpreters/jsfuck.py
3+
omit = tests/*,cloudscraper/interpreters/jsfuck.py

Makefile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ retry:
1616
py.test -n auto --forked --looponfail
1717

1818
ci:
19-
/bin/true
20-
#py.test -n 8 --forked --junitxml=report.xml --collect-only
19+
py.test -n 8 --forked --junitxml=report.xml
2120

2221
lint:
2322
flake8 --ignore $(pep8-rules) cloudscraper tests
@@ -27,7 +26,7 @@ format:
2726
autopep8 -aaa --ignore $(pep8-rules) --in-place --recursive cloudscraper tests
2827

2928
coverage:
30-
py.test --cov-config .coveragerc --verbose --cov-report term --cov-report xml --cov=cloudscraper tests
29+
py.test --cov-config=.coveragerc --verbose --cov-report=term --cov-report=xml --cov=cloudscraper tests
3130
coveralls
3231

3332
clean:

cloudscraper/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def request(self, method, url, *args, **kwargs):
194194

195195
resp = self.Challenge_Response(resp, **kwargs)
196196
else:
197-
if resp.status_code not in [302, 429, 503]:
197+
if not resp.is_redirect and resp.status_code not in [429, 503]:
198198
self._solveDepthCnt = 0
199199

200200
return resp
@@ -452,9 +452,7 @@ def updateAttr(obj, name, newValue):
452452
cloudflare_kwargs['headers'] = updateAttr(
453453
cloudflare_kwargs,
454454
'headers',
455-
{
456-
'Referer': resp.url
457-
}
455+
{'Referer': resp.url}
458456
)
459457

460458
ret = self.request(

cloudscraper/user_agent/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import sys
66
import ssl
77

8-
98
from collections import OrderedDict
109

1110
# ------------------------------------------------------------------------------- #
@@ -111,6 +110,7 @@ def loadUserAgent(self, *args, **kwargs):
111110

112111
self.headers['User-Agent'] = random.SystemRandom().choice(filteredAgents[user_agent_version])
113112

114-
if not kwargs.get('allow_brotli', False):
115-
if 'br' in self.headers['Accept-Encoding']:
116-
self.headers['Accept-Encoding'] = ','.join([encoding for encoding in self.headers['Accept-Encoding'].split(',') if encoding.strip() != 'br']).strip()
113+
if not kwargs.get('allow_brotli', False) and 'br' in self.headers['Accept-Encoding']:
114+
self.headers['Accept-Encoding'] = ','.join([
115+
encoding for encoding in self.headers['Accept-Encoding'].split(',') if encoding.strip() != 'br'
116+
]).strip()

dev_requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ pytest >= 4.4.1
33
pytest-cov >= 2.6.1
44
pytest-xdist >= 1.28.0
55
pytest-forked >= 1.0.2
6-
pytest-testmon >= 0.9.16
76
pytest-watch >= 4.2.0
87
pytest-timeout >= 1.3.3
8+
pytest-env >= 0.6.2
99
responses >= 0.10.6
10-
sure >= 1.4.11
1110
flake8 >= 3.7.7
1211
tox >= 3.9.0
1312
coveralls >= 1.7.0
14-
autopep8 >= 1.4.4
13+
autopep8 >= 1.4.4
14+
js2py >= 0.60

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[pytest]
22
addopts = -p no:warnings
33
timeout = 2000
4+
env = PYTHONHASHSEED=0

setup.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,16 @@
3333
],
3434
include_package_data = True,
3535
install_requires = [
36-
'pyopenssl >= 17.0',
3736
'requests >= 2.9.2',
38-
'js2py >= 0.60',
3937
'requests_toolbelt >= 0.9.1',
4038
'brotli >= 1.0.7'
4139
],
4240
classifiers=[
4341
'Development Status :: 5 - Production/Stable',
4442
'Intended Audience :: Developers',
4543
'Natural Language :: English',
46-
"License :: OSI Approved :: MIT License",
47-
"Operating System :: OS Independent",
44+
'License :: OSI Approved :: MIT License',
45+
'Operating System :: OS Independent',
4846
'Programming Language :: Python',
4947
'Programming Language :: Python :: 2',
5048
'Programming Language :: Python :: 2.7',

tests/__init__.py

Lines changed: 94 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,118 @@
11
# -*- coding: utf-8 -*-
2-
2+
import hashlib
33
import responses
4-
import pytest
5-
import re
64

7-
from requests.compat import urlencode
8-
from collections import OrderedDict
95
from os import path
106
from io import open
117

8+
try:
9+
from urlparse import parse_qsl
10+
except ImportError:
11+
from urllib.parse import parse_qsl
12+
1213
# Fake URL, network requests are not allowed by default when using the decorator
13-
url = 'https://example-site.dev'
14+
url = 'http://www.evildomain.com'
15+
1416
# These kwargs will be passed to tests by the decorator
15-
cloudscraper_kwargs = dict(
16-
delay=0.01,
17-
debug=False
18-
)
17+
cloudscraper_kwargs = dict(delay=0.01, debug=False)
18+
1919
# Cloudflare challenge fixtures are only read from the FS once
2020
cache = {}
2121

22-
23-
class ChallengeResponse(responses.Response):
24-
"""Simulates a standard IUAM JS challenge response from Cloudflare
25-
26-
This would be the first response in a test.
27-
28-
Kwargs:
29-
Keyword arguments used to override the defaults.
30-
The request will error if it doesn't match a defined response.
31-
"""
32-
33-
def __init__(self, **kwargs):
34-
defaults = (
35-
('method', 'GET'),
36-
('status', 503),
37-
('headers', {'Server': 'cloudflare'}),
38-
('content_type', 'text/html')
39-
)
40-
41-
for k, v in defaults:
42-
kwargs.setdefault(k, v)
43-
44-
super(ChallengeResponse, self).__init__(**kwargs)
45-
46-
47-
class RedirectResponse(responses.CallbackResponse):
48-
"""Simulate the redirect response that occurs after sending a correct answer
49-
50-
This would be the second response in a test.
51-
It will call the provided callback when a matching request is received.
52-
Afterwards, the default is to redirect to the index page "/" aka fake URL.
53-
54-
Kwargs:
55-
Keyword arguments used to override the defaults.
56-
The request will error if it doesn't match a defined response.
57-
"""
58-
59-
def __init__(self, callback=lambda request: None, **kwargs):
60-
defaults = (
61-
('method', 'GET'),
62-
('status', 302),
63-
('headers', {'Location': '/'}),
64-
('content_type', 'text/html'),
65-
('body', '')
66-
)
67-
68-
for k, v in defaults:
69-
kwargs.setdefault(k, v)
70-
71-
args = tuple(kwargs.pop(k) for k in ('status', 'headers', 'body'))
72-
kwargs['callback'] = lambda request: callback(request) or args
73-
74-
super(RedirectResponse, self).__init__(**kwargs)
75-
76-
77-
class DefaultResponse(responses.Response):
78-
"""Simulate the final response after the challenge is solved
79-
80-
This would be the last response in a test and normally occurs after a redirect.
81-
82-
Kwargs:
83-
Keyword arguments used to override the defaults.
84-
The request will error if it doesn't match a defined response.
85-
"""
86-
87-
def __init__(self, **kwargs):
88-
defaults = (
89-
('method', 'GET'),
90-
('status', 200),
91-
('content_type', 'text/html')
92-
)
93-
94-
for k, v in defaults:
95-
kwargs.setdefault(k, v)
96-
97-
super(DefaultResponse, self).__init__(**kwargs)
22+
# ------------------------------------------------------------------------------- #
9823

9924

10025
def fixtures(filename):
101-
"""Read and cache a challenge fixture
26+
"""
27+
Read and cache a challenge fixture
10228
10329
Returns: HTML (bytes): The HTML challenge fixture
10430
"""
10531
if not cache.get(filename):
106-
with open(path.join(path.dirname(__file__), 'fixtures', filename), 'rb') as fp:
32+
print('reading...')
33+
with open(path.join(path.dirname(__file__), 'fixtures', filename), 'r') as fp:
10734
cache[filename] = fp.read()
10835
return cache[filename]
10936

110-
111-
# This is the page that should be received after bypassing the JS challenge.
112-
requested_page = fixtures('requested_page.html')
37+
# ------------------------------------------------------------------------------- #
11338

11439

115-
# This fancy decorator wraps tests so the responses will be mocked.
116-
# It could be called directly e.g. challenge_responses(*args)(test_func) -> wrapper
117-
def challenge_responses(filename, jschl_answer):
118-
# This function is called with the test_func and returns a new wrapper.
119-
def challenge_responses_decorator(test):
40+
def mockCloudflare(fixture, payload):
41+
def responses_decorator(test):
12042
@responses.activate
121-
def wrapper(self, interpreter):
122-
html = fixtures(filename).decode('utf-8')
123-
124-
params = OrderedDict(re.findall(r'name="(s|jschl_vc|pass)"\svalue="(\S+)"', html))
125-
params['jschl_answer'] = jschl_answer
126-
127-
submit_uri = '{}/cdn-cgi/l/chk_jschl?{}'.format(url, urlencode(params))
128-
129-
responses.add(ChallengeResponse(url=url, body=fixtures(filename)))
130-
131-
def onRedirect(request):
132-
# We don't register the last response unless the redirect occurs
133-
responses.add(DefaultResponse(url=url, body=requested_page))
134-
135-
responses.add(RedirectResponse(url=submit_uri, callback=onRedirect))
136-
137-
return test(self, interpreter=interpreter, **cloudscraper_kwargs)
138-
# The following causes pytest to call the test wrapper once for each interpreter.
139-
return pytest.mark.parametrize('interpreter', ['js2py', 'nodejs'])(wrapper)
140-
141-
return challenge_responses_decorator
43+
def wrapper(self):
44+
def post_callback(request):
45+
postPayload = dict(parse_qsl(request.body))
46+
postPayload['r'] = hashlib.sha256(postPayload.get('r', '').encode('ascii')).hexdigest()
47+
48+
for param in payload:
49+
if param not in postPayload or postPayload[param] != payload[param]:
50+
return (
51+
503,
52+
{'Server': 'cloudflare'},
53+
fixtures(fixture)
54+
)
55+
56+
# ------------------------------------------------------------------------------- #
57+
58+
return (
59+
200,
60+
[
61+
(
62+
'Set-Cookie', '__cfduid=d5927a7cbaa96ec536939f93648e3c08a1576098703; Domain=.evildomain.com; path=/'
63+
),
64+
(
65+
'Set-Cookie',
66+
'__cfduid=d5927a7cbaa96ec536939f93648e3c08a1576098703; domain=.evildomain.com; path=/'
67+
),
68+
('Server', 'cloudflare')
69+
],
70+
'Solved OK'
71+
)
72+
73+
# ------------------------------------------------------------------------------- #
74+
75+
def challengeCallback(request):
76+
status_code = 503
77+
78+
if 'reCaptcha' in fixture or '1020' in fixture:
79+
status_code = 403
80+
return (
81+
status_code,
82+
[
83+
(
84+
'Set-Cookie',
85+
'__cfduid=d5927a7cbaa96ec536939f93648e3c08a1576098703; Domain=.evildomain.com; path=/'
86+
),
87+
('Server', 'cloudflare')
88+
],
89+
fixtures(fixture)
90+
)
91+
92+
# ------------------------------------------------------------------------------- #
93+
94+
responses.add_callback(
95+
responses.POST,
96+
url,
97+
callback=post_callback,
98+
content_type='text/html',
99+
)
100+
101+
responses.add_callback(
102+
responses.GET,
103+
url,
104+
callback=challengeCallback,
105+
content_type='text/html',
106+
)
107+
108+
# ------------------------------------------------------------------------------- #
109+
110+
return test(self, **cloudscraper_kwargs)
111+
112+
# ------------------------------------------------------------------------------- #
113+
114+
return wrapper
115+
116+
# ------------------------------------------------------------------------------- #
117+
118+
return responses_decorator

0 commit comments

Comments
 (0)