httpie-cli/tests/test_sessions.py

852 lines
27 KiB
Python
Raw Normal View History

import json
2014-04-24 14:07:31 +02:00
import os
import shutil
2022-02-01 10:14:24 +01:00
from contextlib import contextmanager
from datetime import datetime
from unittest import mock
2022-02-01 10:14:24 +01:00
from pathlib import Path
from typing import Iterator
import pytest
2014-04-24 14:07:31 +02:00
from .fixtures import FILE_PATH_ARG, UNICODE
2022-02-01 10:14:24 +01:00
from httpie.context import Environment
from httpie.encoding import UTF8
from httpie.plugins import AuthPlugin
2014-04-26 19:32:08 +02:00
from httpie.plugins.builtin import HTTPBasicAuth
from httpie.plugins.registry import plugin_manager
from httpie.sessions import Session
from httpie.utils import get_expired_cookies
from .test_auth_plugins import basic_auth
2022-02-01 10:14:24 +01:00
from .utils import DUMMY_HOST, HTTP_OK, MockEnvironment, http, mk_config_dir
from base64 import b64encode
2014-04-24 14:07:31 +02:00
class SessionTestBase:
def start_session(self, httpbin):
2014-04-25 13:57:33 +02:00
"""Create and reuse a unique config dir for each test."""
2014-04-24 14:07:31 +02:00
self.config_dir = mk_config_dir()
def teardown_method(self, method):
2014-04-24 14:07:31 +02:00
shutil.rmtree(self.config_dir)
2014-04-25 13:50:44 +02:00
def env(self):
"""
Return an environment.
2019-12-02 18:12:51 +01:00
Each environment created within a test method
2014-04-25 13:50:44 +02:00
will share the same config_dir. It is necessary
for session files being reused.
"""
return MockEnvironment(config_dir=self.config_dir)
2014-04-24 14:07:31 +02:00
class CookieTestBase:
def setup_method(self, method):
self.config_dir = mk_config_dir()
orig_session = {
'cookies': {
'cookie1': {
'value': 'foo',
},
'cookie2': {
'value': 'foo',
}
}
}
self.session_path = self.config_dir / 'test-session.json'
self.session_path.write_text(json.dumps(orig_session), encoding=UTF8)
def teardown_method(self, method):
shutil.rmtree(self.config_dir)
2014-04-25 13:52:43 +02:00
class TestSessionFlow(SessionTestBase):
2014-04-25 13:50:44 +02:00
"""
2014-04-25 13:57:33 +02:00
These tests start with an existing session created in `setup_method()`.
2014-04-25 13:50:44 +02:00
"""
def start_session(self, httpbin):
2014-04-25 13:50:44 +02:00
"""
Start a full-blown session with a custom request header,
authorization, and response cookies.
"""
super().start_session(httpbin)
r1 = http(
'--follow',
'--session=test',
'--auth=username:password',
'GET',
httpbin.url + '/cookies/set?hello=world',
'Hello:World',
env=self.env()
)
2014-04-25 13:50:44 +02:00
assert HTTP_OK in r1
def test_session_created_and_reused(self, httpbin):
self.start_session(httpbin)
2014-04-25 13:50:44 +02:00
# Verify that the session created in setup_method() has been used.
r2 = http('--session=test',
'GET', httpbin.url + '/get', env=self.env())
assert HTTP_OK in r2
2014-04-25 13:50:44 +02:00
assert r2.json['headers']['Hello'] == 'World'
assert r2.json['headers']['Cookie'] == 'hello=world'
assert 'Basic ' in r2.json['headers']['Authorization']
2014-04-24 14:07:31 +02:00
def test_session_update(self, httpbin):
self.start_session(httpbin)
2014-04-24 14:07:31 +02:00
# Get a response to a request from the original session.
2015-10-22 19:32:16 +02:00
r2 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
2014-04-25 13:50:44 +02:00
assert HTTP_OK in r2
2014-04-24 14:07:31 +02:00
# Make a request modifying the session data.
2014-04-25 13:50:44 +02:00
r3 = http('--follow', '--session=test', '--auth=username:password2',
2015-10-22 19:32:16 +02:00
'GET', httpbin.url + '/cookies/set?hello=world2',
'Hello:World2',
2014-04-25 13:50:44 +02:00
env=self.env())
assert HTTP_OK in r3
2014-04-24 14:07:31 +02:00
# Get a response to a request from the updated session.
2015-10-22 19:32:16 +02:00
r4 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
2014-04-25 13:50:44 +02:00
assert HTTP_OK in r4
assert r4.json['headers']['Hello'] == 'World2'
assert r4.json['headers']['Cookie'] == 'hello=world2'
assert (r2.json['headers']['Authorization']
!= r4.json['headers']['Authorization'])
2014-04-24 14:07:31 +02:00
def test_session_read_only(self, httpbin):
self.start_session(httpbin)
2014-04-24 14:07:31 +02:00
# Get a response from the original session.
2015-10-22 19:32:16 +02:00
r2 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
2014-04-25 13:50:44 +02:00
assert HTTP_OK in r2
2014-04-24 14:07:31 +02:00
# Make a request modifying the session data but
# with --session-read-only.
2014-04-25 13:50:44 +02:00
r3 = http('--follow', '--session-read-only=test',
2014-04-24 15:48:01 +02:00
'--auth=username:password2', 'GET',
httpbin.url + '/cookies/set?hello=world2', 'Hello:World2',
2014-04-25 13:50:44 +02:00
env=self.env())
assert HTTP_OK in r3
2014-04-24 14:07:31 +02:00
# Get a response from the updated session.
2015-10-22 19:32:16 +02:00
r4 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
2014-04-25 13:50:44 +02:00
assert HTTP_OK in r4
2014-04-24 14:07:31 +02:00
# Origin can differ on Travis.
2014-04-25 13:50:44 +02:00
del r2.json['origin'], r4.json['origin']
2014-04-24 14:07:31 +02:00
# Different for each request.
2014-04-25 13:50:44 +02:00
# Should be the same as before r3.
assert r2.json == r4.json
def test_session_overwrite_header(self, httpbin):
self.start_session(httpbin)
r2 = http('--session=test', 'GET', httpbin.url + '/get',
'Hello:World2', env=self.env())
assert HTTP_OK in r2
assert r2.json['headers']['Hello'] == 'World2'
r3 = http('--session=test', 'GET', httpbin.url + '/get',
'Hello:World2', 'Hello:World3', env=self.env())
assert HTTP_OK in r3
assert r3.json['headers']['Hello'] == 'World2,World3'
r3 = http('--session=test', 'GET', httpbin.url + '/get',
'Hello:', 'Hello:World3', env=self.env())
assert HTTP_OK in r3
assert 'Hello' not in r3.json['headers']['Hello']
2014-04-25 13:50:44 +02:00
2014-04-25 13:52:43 +02:00
class TestSession(SessionTestBase):
2014-04-25 13:50:44 +02:00
"""Stand-alone session tests."""
def test_session_ignored_header_prefixes(self, httpbin):
self.start_session(httpbin)
r1 = http('--session=test', 'GET', httpbin.url + '/get',
2014-04-25 13:52:43 +02:00
'Content-Type: text/plain',
'If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT',
env=self.env())
2014-04-25 13:50:44 +02:00
assert HTTP_OK in r1
2015-10-22 19:32:16 +02:00
r2 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
2014-04-25 13:50:44 +02:00
assert HTTP_OK in r2
2016-03-01 16:22:50 +01:00
assert 'Content-Type' not in r2.json['headers']
2014-04-25 13:50:44 +02:00
assert 'If-Unmodified-Since' not in r2.json['headers']
2014-04-24 14:07:31 +02:00
def test_session_with_upload(self, httpbin):
self.start_session(httpbin)
r = http('--session=test', '--form', '--verbose', 'POST', httpbin.url + '/post',
f'test-file@{FILE_PATH_ARG}', 'foo=bar', env=self.env())
assert HTTP_OK in r
def test_session_by_path(self, httpbin):
self.start_session(httpbin)
session_path = self.config_dir / 'session-by-path.json'
r1 = http('--session', str(session_path), 'GET', httpbin.url + '/get',
2014-04-25 13:50:44 +02:00
'Foo:Bar', env=self.env())
assert HTTP_OK in r1
2014-04-24 14:07:31 +02:00
r2 = http('--session', str(session_path), 'GET', httpbin.url + '/get',
2014-04-25 13:50:44 +02:00
env=self.env())
assert HTTP_OK in r2
2014-04-25 12:18:35 +02:00
assert r2.json['headers']['Foo'] == 'Bar'
def test_session_with_cookie_followed_by_another_header(self, httpbin):
"""
Make sure headers dont get mutated <https://github.com/httpie/cli/issues/1126>
"""
self.start_session(httpbin)
session_data = {
2022-02-01 10:14:24 +01:00
'headers': {
'cookie': '...',
'zzz': '...'
}
}
session_path = self.config_dir / 'session-data.json'
session_path.write_text(json.dumps(session_data))
r = http('--session', str(session_path), 'GET', httpbin.url + '/get',
env=self.env())
assert HTTP_OK in r
assert 'Zzz' in r
def test_session_unicode(self, httpbin):
self.start_session(httpbin)
r1 = http('--session=test', f'--auth=test:{UNICODE}',
'GET', httpbin.url + '/get', f'Test:{UNICODE}',
env=self.env())
assert HTTP_OK in r1
2014-06-03 19:44:22 +02:00
r2 = http('--session=test', '--verbose', 'GET',
httpbin.url + '/get', env=self.env())
assert HTTP_OK in r2
# FIXME: Authorization *sometimes* is not present
assert (r2.json['headers']['Authorization']
2021-05-27 13:05:41 +02:00
== HTTPBasicAuth.make_header('test', UNICODE))
# httpbin doesn't interpret UTF-8 headers
2014-06-03 19:44:22 +02:00
assert UNICODE in r2
def test_session_default_header_value_overwritten(self, httpbin):
self.start_session(httpbin)
# https://github.com/httpie/cli/issues/180
r1 = http('--session=test',
httpbin.url + '/headers', 'User-Agent:custom',
env=self.env())
assert HTTP_OK in r1
assert r1.json['headers']['User-Agent'] == 'custom'
r2 = http('--session=test', httpbin.url + '/headers', env=self.env())
assert HTTP_OK in r2
assert r2.json['headers']['User-Agent'] == 'custom'
def test_download_in_session(self, tmp_path, httpbin):
# https://github.com/httpie/cli/issues/412
self.start_session(httpbin)
cwd = os.getcwd()
os.chdir(tmp_path)
try:
http('--session=test', '--download',
httpbin.url + '/get', env=self.env())
finally:
os.chdir(cwd)
@pytest.mark.parametrize(
'auth_require_param, auth_parse_param',
[
(False, False),
(False, True),
(True, False)
]
)
def test_auth_type_reused_in_session(self, auth_require_param, auth_parse_param, httpbin):
self.start_session(httpbin)
session_path = self.config_dir / 'test-session.json'
header = 'Custom dXNlcjpwYXNzd29yZA'
class Plugin(AuthPlugin):
auth_type = 'test-reused'
auth_require = auth_require_param
auth_parse = auth_parse_param
def get_auth(self, username=None, password=None):
return basic_auth(header=f'{header}==')
plugin_manager.register(Plugin)
r1 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user:password',
'--print=H',
)
r2 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--print=H',
)
assert f'Authorization: {header}' in r1
assert f'Authorization: {header}' in r2
plugin_manager.unregister(Plugin)
def test_auth_plugin_prompt_password_in_session(self, httpbin):
self.start_session(httpbin)
session_path = self.config_dir / 'test-session.json'
class Plugin(AuthPlugin):
auth_type = 'test-prompted'
def get_auth(self, username=None, password=None):
2022-02-01 10:14:24 +01:00
basic_auth_header = 'Basic ' + b64encode(self.raw_auth.encode()).strip().decode('latin1')
return basic_auth(basic_auth_header)
plugin_manager.register(Plugin)
with mock.patch(
'httpie.cli.argtypes.AuthCredentials._getpass',
new=lambda self, prompt: 'password'
):
r1 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user',
)
r2 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
)
assert HTTP_OK in r1
assert HTTP_OK in r2
# additional test for issue: https://github.com/httpie/cli/issues/1098
with open(session_path) as session_file:
session_file_lines = ''.join(session_file.readlines())
assert "\"type\": \"test-prompted\"" in session_file_lines
assert "\"raw_auth\": \"user:password\"" in session_file_lines
plugin_manager.unregister(Plugin)
def test_auth_type_stored_in_session_file(self, httpbin):
self.config_dir = mk_config_dir()
self.session_path = self.config_dir / 'test-session.json'
class Plugin(AuthPlugin):
auth_type = 'test-saved'
auth_require = True
def get_auth(self, username=None, password=None):
return basic_auth()
plugin_manager.register(Plugin)
http('--session', str(self.session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user:password',
)
updated_session = json.loads(self.session_path.read_text(encoding=UTF8))
assert updated_session['auth']['type'] == 'test-saved'
2022-02-01 10:14:24 +01:00
assert updated_session['auth']['raw_auth'] == 'user:password'
plugin_manager.unregister(Plugin)
class TestExpiredCookies(CookieTestBase):
2020-06-15 23:02:16 +02:00
@pytest.mark.parametrize(
'initial_cookie, expired_cookie',
[
2022-02-01 10:14:24 +01:00
({'id': {'value': 123}}, {'name': 'id'}),
({'id': {'value': 123}}, {'name': 'token'})
]
)
2022-02-01 10:14:24 +01:00
def test_removes_expired_cookies_from_session_obj(self, initial_cookie, expired_cookie, httpbin, mock_env):
session = Session(self.config_dir, env=mock_env, session_id=None, bound_host=None)
session['cookies'] = initial_cookie
session.remove_cookies([expired_cookie])
assert expired_cookie not in session.cookies
def test_expired_cookies(self, httpbin):
r = http(
'--session', str(self.session_path),
'--print=H',
httpbin.url + '/cookies/delete?cookie2',
)
assert 'Cookie: cookie1=foo; cookie2=foo' in r
updated_session = json.loads(self.session_path.read_text(encoding=UTF8))
assert 'cookie1' in updated_session['cookies']
assert 'cookie2' not in updated_session['cookies']
def test_get_expired_cookies_using_max_age(self):
Add internal support for file-like object responses to improve adapter plugin support (#1094) * Support `requests.response.raw` being a file-like object Previously HTTPie relied on `requests.models.Response.raw` being `urllib3.HTTPResponse`. The `requests` documentation specifies that (requests.models.Response.raw)[https://docs.python-requests.org/en/master/api/#requests.Response.raw] is a file-like object but allows for other types for internal use. This change introduces graceful handling for scenarios when `requests.models.Response.raw` is not `urllib3.HTTPResponse`. In such a scenario HTTPie now falls back to extracting metadata from `requests.models.Response` directly instead of direct access from protected protected members such as `response.raw._original_response`. A side effect in this fallback procedure is that we can no longer determine HTTP protocol version and report it as `1.1`. This change is necessary to make it possible to implement `TransportPlugins` without having to also needing to emulate internal behavior of `urlib3` and `http.client`. * Load cookies from `response.headers` instead of `response.raw._original_response.msg._headers` `response.cookies` was not utilized as it not possible to construct original payload from `http.cookiejar.Cookie`. Data is stored in lossy format. For example `Cookie.secure` defaults to `False` so we cannot distinguish if `Cookie.secure` was set to `False` or was not set at all. Same problem applies to other fields also. * Simpler HTTP envelope data extraction * Test cookie extraction and make cookie presentment backwards compatible Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr> Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-07-06 21:00:06 +02:00
cookies = 'one=two; Max-Age=0; path=/; domain=.tumblr.com; HttpOnly'
expected_expired = [
{'name': 'one', 'path': '/'}
]
Add internal support for file-like object responses to improve adapter plugin support (#1094) * Support `requests.response.raw` being a file-like object Previously HTTPie relied on `requests.models.Response.raw` being `urllib3.HTTPResponse`. The `requests` documentation specifies that (requests.models.Response.raw)[https://docs.python-requests.org/en/master/api/#requests.Response.raw] is a file-like object but allows for other types for internal use. This change introduces graceful handling for scenarios when `requests.models.Response.raw` is not `urllib3.HTTPResponse`. In such a scenario HTTPie now falls back to extracting metadata from `requests.models.Response` directly instead of direct access from protected protected members such as `response.raw._original_response`. A side effect in this fallback procedure is that we can no longer determine HTTP protocol version and report it as `1.1`. This change is necessary to make it possible to implement `TransportPlugins` without having to also needing to emulate internal behavior of `urlib3` and `http.client`. * Load cookies from `response.headers` instead of `response.raw._original_response.msg._headers` `response.cookies` was not utilized as it not possible to construct original payload from `http.cookiejar.Cookie`. Data is stored in lossy format. For example `Cookie.secure` defaults to `False` so we cannot distinguish if `Cookie.secure` was set to `False` or was not set at all. Same problem applies to other fields also. * Simpler HTTP envelope data extraction * Test cookie extraction and make cookie presentment backwards compatible Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr> Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-07-06 21:00:06 +02:00
assert get_expired_cookies(cookies, now=None) == expected_expired
@pytest.mark.parametrize(
'cookies, now, expected_expired',
[
(
Add internal support for file-like object responses to improve adapter plugin support (#1094) * Support `requests.response.raw` being a file-like object Previously HTTPie relied on `requests.models.Response.raw` being `urllib3.HTTPResponse`. The `requests` documentation specifies that (requests.models.Response.raw)[https://docs.python-requests.org/en/master/api/#requests.Response.raw] is a file-like object but allows for other types for internal use. This change introduces graceful handling for scenarios when `requests.models.Response.raw` is not `urllib3.HTTPResponse`. In such a scenario HTTPie now falls back to extracting metadata from `requests.models.Response` directly instead of direct access from protected protected members such as `response.raw._original_response`. A side effect in this fallback procedure is that we can no longer determine HTTP protocol version and report it as `1.1`. This change is necessary to make it possible to implement `TransportPlugins` without having to also needing to emulate internal behavior of `urlib3` and `http.client`. * Load cookies from `response.headers` instead of `response.raw._original_response.msg._headers` `response.cookies` was not utilized as it not possible to construct original payload from `http.cookiejar.Cookie`. Data is stored in lossy format. For example `Cookie.secure` defaults to `False` so we cannot distinguish if `Cookie.secure` was set to `False` or was not set at all. Same problem applies to other fields also. * Simpler HTTP envelope data extraction * Test cookie extraction and make cookie presentment backwards compatible Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr> Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-07-06 21:00:06 +02:00
'hello=world; Path=/; Expires=Thu, 01-Jan-1970 00:00:00 GMT; HttpOnly',
None,
[
{
'name': 'hello',
'path': '/'
}
]
),
(
Add internal support for file-like object responses to improve adapter plugin support (#1094) * Support `requests.response.raw` being a file-like object Previously HTTPie relied on `requests.models.Response.raw` being `urllib3.HTTPResponse`. The `requests` documentation specifies that (requests.models.Response.raw)[https://docs.python-requests.org/en/master/api/#requests.Response.raw] is a file-like object but allows for other types for internal use. This change introduces graceful handling for scenarios when `requests.models.Response.raw` is not `urllib3.HTTPResponse`. In such a scenario HTTPie now falls back to extracting metadata from `requests.models.Response` directly instead of direct access from protected protected members such as `response.raw._original_response`. A side effect in this fallback procedure is that we can no longer determine HTTP protocol version and report it as `1.1`. This change is necessary to make it possible to implement `TransportPlugins` without having to also needing to emulate internal behavior of `urlib3` and `http.client`. * Load cookies from `response.headers` instead of `response.raw._original_response.msg._headers` `response.cookies` was not utilized as it not possible to construct original payload from `http.cookiejar.Cookie`. Data is stored in lossy format. For example `Cookie.secure` defaults to `False` so we cannot distinguish if `Cookie.secure` was set to `False` or was not set at all. Same problem applies to other fields also. * Simpler HTTP envelope data extraction * Test cookie extraction and make cookie presentment backwards compatible Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr> Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-07-06 21:00:06 +02:00
(
'hello=world; Path=/; Expires=Thu, 01-Jan-1970 00:00:00 GMT; HttpOnly, '
'pea=pod; Path=/ab; Expires=Thu, 01-Jan-1970 00:00:00 GMT; HttpOnly'
),
None,
[
{'name': 'hello', 'path': '/'},
{'name': 'pea', 'path': '/ab'}
]
),
(
# Checks we gracefully ignore expires date in invalid format.
# <https://github.com/httpie/cli/issues/963>
Add internal support for file-like object responses to improve adapter plugin support (#1094) * Support `requests.response.raw` being a file-like object Previously HTTPie relied on `requests.models.Response.raw` being `urllib3.HTTPResponse`. The `requests` documentation specifies that (requests.models.Response.raw)[https://docs.python-requests.org/en/master/api/#requests.Response.raw] is a file-like object but allows for other types for internal use. This change introduces graceful handling for scenarios when `requests.models.Response.raw` is not `urllib3.HTTPResponse`. In such a scenario HTTPie now falls back to extracting metadata from `requests.models.Response` directly instead of direct access from protected protected members such as `response.raw._original_response`. A side effect in this fallback procedure is that we can no longer determine HTTP protocol version and report it as `1.1`. This change is necessary to make it possible to implement `TransportPlugins` without having to also needing to emulate internal behavior of `urlib3` and `http.client`. * Load cookies from `response.headers` instead of `response.raw._original_response.msg._headers` `response.cookies` was not utilized as it not possible to construct original payload from `http.cookiejar.Cookie`. Data is stored in lossy format. For example `Cookie.secure` defaults to `False` so we cannot distinguish if `Cookie.secure` was set to `False` or was not set at all. Same problem applies to other fields also. * Simpler HTTP envelope data extraction * Test cookie extraction and make cookie presentment backwards compatible Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr> Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-07-06 21:00:06 +02:00
'pfg=; Expires=Sat, 19-Sep-2020 06:58:14 GMT+0000; Max-Age=0; path=/; domain=.tumblr.com; secure; HttpOnly',
None,
[]
),
(
Add internal support for file-like object responses to improve adapter plugin support (#1094) * Support `requests.response.raw` being a file-like object Previously HTTPie relied on `requests.models.Response.raw` being `urllib3.HTTPResponse`. The `requests` documentation specifies that (requests.models.Response.raw)[https://docs.python-requests.org/en/master/api/#requests.Response.raw] is a file-like object but allows for other types for internal use. This change introduces graceful handling for scenarios when `requests.models.Response.raw` is not `urllib3.HTTPResponse`. In such a scenario HTTPie now falls back to extracting metadata from `requests.models.Response` directly instead of direct access from protected protected members such as `response.raw._original_response`. A side effect in this fallback procedure is that we can no longer determine HTTP protocol version and report it as `1.1`. This change is necessary to make it possible to implement `TransportPlugins` without having to also needing to emulate internal behavior of `urlib3` and `http.client`. * Load cookies from `response.headers` instead of `response.raw._original_response.msg._headers` `response.cookies` was not utilized as it not possible to construct original payload from `http.cookiejar.Cookie`. Data is stored in lossy format. For example `Cookie.secure` defaults to `False` so we cannot distinguish if `Cookie.secure` was set to `False` or was not set at all. Same problem applies to other fields also. * Simpler HTTP envelope data extraction * Test cookie extraction and make cookie presentment backwards compatible Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr> Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-07-06 21:00:06 +02:00
'hello=world; Path=/; Expires=Fri, 12 Jun 2020 12:28:55 GMT; HttpOnly',
datetime(2020, 6, 11).timestamp(),
2020-06-15 23:02:16 +02:00
[]
),
]
)
Add internal support for file-like object responses to improve adapter plugin support (#1094) * Support `requests.response.raw` being a file-like object Previously HTTPie relied on `requests.models.Response.raw` being `urllib3.HTTPResponse`. The `requests` documentation specifies that (requests.models.Response.raw)[https://docs.python-requests.org/en/master/api/#requests.Response.raw] is a file-like object but allows for other types for internal use. This change introduces graceful handling for scenarios when `requests.models.Response.raw` is not `urllib3.HTTPResponse`. In such a scenario HTTPie now falls back to extracting metadata from `requests.models.Response` directly instead of direct access from protected protected members such as `response.raw._original_response`. A side effect in this fallback procedure is that we can no longer determine HTTP protocol version and report it as `1.1`. This change is necessary to make it possible to implement `TransportPlugins` without having to also needing to emulate internal behavior of `urlib3` and `http.client`. * Load cookies from `response.headers` instead of `response.raw._original_response.msg._headers` `response.cookies` was not utilized as it not possible to construct original payload from `http.cookiejar.Cookie`. Data is stored in lossy format. For example `Cookie.secure` defaults to `False` so we cannot distinguish if `Cookie.secure` was set to `False` or was not set at all. Same problem applies to other fields also. * Simpler HTTP envelope data extraction * Test cookie extraction and make cookie presentment backwards compatible Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr> Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-07-06 21:00:06 +02:00
def test_get_expired_cookies_manages_multiple_cookie_headers(self, cookies, now, expected_expired):
assert get_expired_cookies(cookies, now=now) == expected_expired
class TestCookieStorage(CookieTestBase):
@pytest.mark.parametrize(
2023-05-19 21:51:52 +02:00
['specified_cookie_header', 'new_cookies_dict', 'expected_effective_cookie_header'],
[(
'new=bar',
{'new': 'bar'},
'cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar;chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar; chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
),
(
2023-05-19 21:51:52 +02:00
'new=bar; chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
2023-05-19 21:51:52 +02:00
'cookie1=foo; cookie2=foo; new=bar; chocolate=milk'
),
(
'new=bar; chocolate=milk;;;',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
)
]
)
2023-05-19 21:51:52 +02:00
def test_existing_and_new_cookies_sent_in_request(
self,
specified_cookie_header,
new_cookies_dict,
expected_effective_cookie_header,
httpbin,
):
r = http(
'--session', str(self.session_path),
'--print=H',
httpbin.url,
2023-05-19 21:51:52 +02:00
'Cookie:' + specified_cookie_header,
)
2023-05-19 23:41:16 +02:00
parsed_request_headers = { # noqa
2023-05-19 21:51:52 +02:00
name: value for name, value in [
line.split(': ', 1)
for line in r.splitlines()
if line and ':' in line
]
}
# Note: cookies in the request are in an undefined order.
expected_request_cookie_set = set(expected_effective_cookie_header.split('; '))
actual_request_cookie_set = set(parsed_request_headers['Cookie'].split('; '))
assert actual_request_cookie_set == expected_request_cookie_set
updated_session = json.loads(self.session_path.read_text(encoding=UTF8))
2023-05-19 21:51:52 +02:00
assert 'Cookie' not in updated_session['headers']
for name, value in new_cookies_dict.items():
2023-05-19 21:51:52 +02:00
assert updated_session['cookies'][name]['value'] == value
@pytest.mark.parametrize(
'cli_cookie, set_cookie, expected',
[(
'',
'/cookies/set/cookie1/bar',
'bar'
),
(
'cookie1=not_foo',
'/cookies/set/cookie1/bar',
'bar'
),
(
'cookie1=not_foo',
'',
'not_foo'
),
(
'',
'',
'foo'
)
]
)
def test_cookie_storage_priority(self, cli_cookie, set_cookie, expected, httpbin):
"""
Expected order of priority for cookie storage in session file:
1. set-cookie (from server)
2. command line arg
3. cookie already stored in session file
"""
http(
'--session', str(self.session_path),
httpbin.url + set_cookie,
'Cookie:' + cli_cookie,
)
updated_session = json.loads(self.session_path.read_text(encoding=UTF8))
assert updated_session['cookies']['cookie1']['value'] == expected
2022-02-01 10:14:24 +01:00
@pytest.fixture
def basic_session(httpbin, tmp_path):
session_path = tmp_path / 'session.json'
http(
'--session', str(session_path),
httpbin + '/get'
)
return session_path
@contextmanager
def open_session(path: Path, env: Environment, read_only: bool = False) -> Iterator[Session]:
session = Session(path, env, session_id='test', bound_host=DUMMY_HOST)
session.load()
yield session
if not read_only:
session.save()
@contextmanager
def open_raw_session(path: Path, read_only: bool = False) -> None:
with open(path) as stream:
raw_session = json.load(stream)
yield raw_session
if not read_only:
with open(path, 'w') as stream:
json.dump(raw_session, stream)
def read_stderr(env: Environment) -> bytes:
env.stderr.seek(0)
stderr_data = env.stderr.read()
if isinstance(stderr_data, str):
return stderr_data.encode()
else:
return stderr_data
def test_old_session_version_saved_as_is(basic_session, mock_env):
with open_session(basic_session, mock_env) as session:
session['__meta__'] = {'httpie': '0.0.1'}
with open_session(basic_session, mock_env, read_only=True) as session:
assert session['__meta__']['httpie'] == '0.0.1'
def test_old_session_cookie_layout_warning(basic_session, mock_env):
with open_session(basic_session, mock_env) as session:
# Use the old layout & set a cookie
session['cookies'] = {}
session.cookies.set('foo', 'bar')
assert read_stderr(mock_env) == b''
with open_session(basic_session, mock_env, read_only=True) as session:
assert b'Outdated layout detected' in read_stderr(mock_env)
@pytest.mark.parametrize('cookies, expect_warning', [
# Old-style cookie format
(
# Without 'domain' set
{'foo': {'value': 'bar'}},
True
),
(
# With 'domain' set to empty string
{'foo': {'value': 'bar', 'domain': ''}},
True
),
(
# With 'domain' set to null
{'foo': {'value': 'bar', 'domain': None}},
False,
),
(
# With 'domain' set to a URL
{'foo': {'value': 'bar', 'domain': DUMMY_HOST}},
False,
),
# New style cookie format
(
# Without 'domain' set
[{'name': 'foo', 'value': 'bar'}],
False
),
(
# With 'domain' set to empty string
[{'name': 'foo', 'value': 'bar', 'domain': ''}],
False
),
(
# With 'domain' set to null
[{'name': 'foo', 'value': 'bar', 'domain': None}],
False,
),
(
# With 'domain' set to a URL
[{'name': 'foo', 'value': 'bar', 'domain': DUMMY_HOST}],
False,
),
])
def test_cookie_security_warnings_on_raw_cookies(basic_session, mock_env, cookies, expect_warning):
with open_raw_session(basic_session) as raw_session:
raw_session['cookies'] = cookies
with open_session(basic_session, mock_env, read_only=True):
warning = b'Outdated layout detected'
stderr = read_stderr(mock_env)
if expect_warning:
assert warning in stderr
else:
assert warning not in stderr
def test_old_session_cookie_layout_loading(basic_session, httpbin, mock_env):
with open_session(basic_session, mock_env) as session:
# Use the old layout & set a cookie
session['cookies'] = {}
session.cookies.set('foo', 'bar')
response = http(
'--session', str(basic_session),
httpbin + '/cookies'
)
assert response.json['cookies'] == {'foo': 'bar'}
@pytest.mark.parametrize('layout_type', [
dict, list
])
def test_session_cookie_layout_preservation(basic_session, mock_env, layout_type):
2022-02-01 10:14:24 +01:00
with open_session(basic_session, mock_env) as session:
session['cookies'] = layout_type()
session.cookies.set('foo', 'bar')
session.save()
with open_session(basic_session, mock_env, read_only=True) as session:
assert isinstance(session['cookies'], layout_type)
@pytest.mark.parametrize('layout_type', [
dict, list
])
def test_session_cookie_layout_preservation_on_new_cookies(basic_session, httpbin, mock_env, layout_type):
2022-02-01 10:14:24 +01:00
with open_session(basic_session, mock_env) as session:
session['cookies'] = layout_type()
session.cookies.set('foo', 'bar')
session.save()
http(
'--session', str(basic_session),
httpbin + '/cookies/set/baz/quux'
)
with open_session(basic_session, mock_env, read_only=True) as session:
assert isinstance(session['cookies'], layout_type)
@pytest.mark.parametrize('headers, expect_warning', [
# Old-style header format
(
{},
False
),
(
{'Foo': 'bar'},
True
),
# New style header format
(
[],
False
),
(
[{'name': 'Foo', 'value': 'Bar'}],
False
),
])
def test_headers_old_layout_warning(basic_session, mock_env, headers, expect_warning):
with open_raw_session(basic_session) as raw_session:
raw_session['headers'] = headers
with open_session(basic_session, mock_env, read_only=True):
warning = b'Outdated layout detected'
stderr = read_stderr(mock_env)
if expect_warning:
assert warning in stderr
else:
assert warning not in stderr
def test_outdated_layout_mixed(basic_session, mock_env):
with open_raw_session(basic_session) as raw_session:
raw_session['headers'] = {'Foo': 'Bar'}
raw_session['cookies'] = {
'cookie': {
'value': 'value'
}
}
with open_session(basic_session, mock_env, read_only=True):
stderr = read_stderr(mock_env)
# We should only see 1 warning.
assert stderr.count(b'Outdated layout') == 1
def test_old_session_header_layout_loading(basic_session, httpbin, mock_env):
with open_session(basic_session, mock_env) as session:
# Use the old layout & set a header
session['headers'] = {}
session._headers.add('Foo', 'Bar')
response = http(
'--session', str(basic_session),
httpbin + '/get'
)
assert response.json['headers']['Foo'] == 'Bar'
@pytest.mark.parametrize('layout_type', [
dict, list
])
def test_session_header_layout_preservation(basic_session, mock_env, layout_type):
with open_session(basic_session, mock_env) as session:
session['headers'] = layout_type()
session._headers.add('Foo', 'Bar')
with open_session(basic_session, mock_env, read_only=True) as session:
assert isinstance(session['headers'], layout_type)
@pytest.mark.parametrize('layout_type', [
dict, list
])
def test_session_header_layout_preservation_on_new_headers(basic_session, httpbin, mock_env, layout_type):
with open_session(basic_session, mock_env) as session:
session['headers'] = layout_type()
session._headers.add('Foo', 'Bar')
http(
'--session', str(basic_session),
httpbin + '/get',
'Baz:Quux'
)
with open_session(basic_session, mock_env, read_only=True) as session:
assert isinstance(session['headers'], layout_type)
def test_session_multiple_headers_with_same_name(basic_session, httpbin):
http(
'--session', str(basic_session),
httpbin + '/get',
'Foo:bar',
'Foo:baz',
'Foo:bar'
)
r = http(
'--offline',
'--session', str(basic_session),
httpbin + '/get',
)
assert r.count('Foo: bar') == 2
assert 'Foo: baz' in r
@pytest.mark.parametrize(
'server, expected_cookies',
[
(
pytest.lazy_fixture('localhost_http_server'),
{'secure_cookie': 'foo', 'insecure_cookie': 'bar'}
),
(
pytest.lazy_fixture('remote_httpbin'),
{'insecure_cookie': 'bar'}
)
]
)
def test_secure_cookies_on_localhost(mock_env, tmp_path, server, expected_cookies):
session_path = tmp_path / 'session.json'
http(
'--session', str(session_path),
server + '/cookies/set',
'secure_cookie==foo',
'insecure_cookie==bar'
)
with open_session(session_path, mock_env) as session:
for cookie in session.cookies:
if cookie.name == 'secure_cookie':
cookie.secure = True
r = http(
'--session', str(session_path),
server + '/cookies'
)
assert r.json == {'cookies': expected_cookies}