1
0
mirror of https://github.com/httpie/cli.git synced 2024-11-28 08:38:44 +02:00

Implement support for multiple headers with the same name in sessions (#1335)

* Properly remove duplicate Cookie headers

* Implement support for multiple headers with the same name in sessions

* More testing

* Cleanup

* Remove duplicated test, cleanup

* Fix pycodestyle

* CHANGELOG

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
This commit is contained in:
Batuhan Taskaya 2022-04-03 16:48:31 +03:00 committed by GitHub
parent c157948531
commit d03e3f4e14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 618 additions and 71 deletions

View File

@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## [3.1.1.dev0](https://github.com/httpie/httpie/compare/3.1.0...HEAD) (Unreleased) ## [3.1.1.dev0](https://github.com/httpie/httpie/compare/3.1.0...HEAD) (Unreleased)
- Added support for session persistence of repeated headers with the same name. ([#1335](https://github.com/httpie/httpie/pull/1335))
- Changed `httpie plugins` to the new `httpie cli` namespace as `httpie cli plugins` (`httpie plugins` continues to work as a hidden alias). ([#1320](https://github.com/httpie/httpie/issues/1320)) - Changed `httpie plugins` to the new `httpie cli` namespace as `httpie cli plugins` (`httpie plugins` continues to work as a hidden alias). ([#1320](https://github.com/httpie/httpie/issues/1320))
- Fixed redundant creation of `Content-Length` header on `OPTIONS` requests. ([#1310](https://github.com/httpie/httpie/issues/1310)) - Fixed redundant creation of `Content-Length` header on `OPTIONS` requests. ([#1310](https://github.com/httpie/httpie/issues/1310))

View File

@ -35,6 +35,16 @@ class HTTPHeadersDict(CIMultiDict, BaseMultiDict):
super().add(key, value) super().add(key, value)
def remove_item(self, key, value):
"""
Remove a (key, value) pair from the dict.
"""
existing_values = self.popall(key)
existing_values.remove(value)
for value in existing_values:
self.add(key, value)
class RequestJSONDataDict(OrderedDict): class RequestJSONDataDict(OrderedDict):
pass pass

View File

@ -4,6 +4,7 @@ from typing import Any, Type, List, Dict, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from httpie.sessions import Session from httpie.sessions import Session
INSECURE_COOKIE_JAR_WARNING = '''\ INSECURE_COOKIE_JAR_WARNING = '''\
Outdated layout detected for the current session. Please consider updating it, Outdated layout detected for the current session. Please consider updating it,
in order to not get affected by potential security problems. in order to not get affected by potential security problems.
@ -53,16 +54,12 @@ def pre_process(session: 'Session', cookies: Any) -> List[Dict[str, Any]]:
for cookie in normalized_cookies for cookie in normalized_cookies
) )
if should_issue_warning and not session.refactor_mode: if should_issue_warning:
warning = INSECURE_COOKIE_JAR_WARNING.format(hostname=session.bound_host, session_id=session.session_id) warning = INSECURE_COOKIE_JAR_WARNING.format(hostname=session.bound_host, session_id=session.session_id)
if not session.is_anonymous: if not session.is_anonymous:
warning += INSECURE_COOKIE_JAR_WARNING_FOR_NAMED_SESSIONS warning += INSECURE_COOKIE_JAR_WARNING_FOR_NAMED_SESSIONS
warning += INSECURE_COOKIE_SECURITY_LINK warning += INSECURE_COOKIE_SECURITY_LINK
session.warn_legacy_usage(warning)
session.env.log_error(
warning,
level='warning'
)
return normalized_cookies return normalized_cookies

View File

@ -0,0 +1,73 @@
from typing import Any, Type, List, Dict, TYPE_CHECKING
if TYPE_CHECKING:
from httpie.sessions import Session
OLD_HEADER_STORE_WARNING = '''\
Outdated layout detected for the current session. Please consider updating it,
in order to use the latest features regarding the header layout.
For fixing the current session:
$ httpie cli sessions upgrade {hostname} {session_id}
'''
OLD_HEADER_STORE_WARNING_FOR_NAMED_SESSIONS = '''\
For fixing all named sessions:
$ httpie cli sessions upgrade-all
'''
OLD_HEADER_STORE_LINK = '\nSee $INSERT_LINK for more information.'
def pre_process(session: 'Session', headers: Any) -> List[Dict[str, Any]]:
"""Serialize the headers into a unified form and issue a warning if
the session file is using the old layout."""
is_old_style = isinstance(headers, dict)
if is_old_style:
normalized_headers = list(headers.items())
else:
normalized_headers = [
(item['name'], item['value'])
for item in headers
]
if is_old_style:
warning = OLD_HEADER_STORE_WARNING.format(hostname=session.bound_host, session_id=session.session_id)
if not session.is_anonymous:
warning += OLD_HEADER_STORE_WARNING_FOR_NAMED_SESSIONS
warning += OLD_HEADER_STORE_LINK
session.warn_legacy_usage(warning)
return normalized_headers
def post_process(
normalized_headers: List[Dict[str, Any]],
*,
original_type: Type[Any]
) -> Any:
"""Deserialize given header store into the original form it was
used in."""
if issubclass(original_type, dict):
# For the legacy behavior, preserve the last value.
return {
item['name']: item['value']
for item in normalized_headers
}
else:
return normalized_headers
def fix_layout(session: 'Session', *args, **kwargs) -> None:
from httpie.sessions import materialize_headers
if not isinstance(session['headers'], dict):
return None
session['headers'] = materialize_headers(session['headers'])

View File

@ -4,10 +4,16 @@ from typing import Tuple
from httpie.sessions import SESSIONS_DIR_NAME, get_httpie_session from httpie.sessions import SESSIONS_DIR_NAME, get_httpie_session
from httpie.status import ExitStatus from httpie.status import ExitStatus
from httpie.context import Environment from httpie.context import Environment
from httpie.legacy import cookie_format as legacy_cookies from httpie.legacy import v3_1_0_session_cookie_format, v3_2_0_session_header_format
from httpie.manager.cli import missing_subcommand, parser from httpie.manager.cli import missing_subcommand, parser
FIXERS_TO_VERSIONS = {
'3.1.0': v3_1_0_session_cookie_format.fix_layout,
'3.2.0': v3_2_0_session_header_format.fix_layout,
}
def cli_sessions(env: Environment, args: argparse.Namespace) -> ExitStatus: def cli_sessions(env: Environment, args: argparse.Namespace) -> ExitStatus:
action = args.cli_sessions_action action = args.cli_sessions_action
if action is None: if action is None:
@ -22,7 +28,7 @@ def cli_sessions(env: Environment, args: argparse.Namespace) -> ExitStatus:
def is_version_greater(version_1: str, version_2: str) -> bool: def is_version_greater(version_1: str, version_2: str) -> bool:
# In an ideal scenerio, we would depend on `packaging` in order # In an ideal scenario, we would depend on `packaging` in order
# to offer PEP 440 compatible parsing. But since it might not be # to offer PEP 440 compatible parsing. But since it might not be
# commonly available for outside packages, and since we are only # commonly available for outside packages, and since we are only
# going to parse HTTPie's own version it should be fine to compare # going to parse HTTPie's own version it should be fine to compare
@ -40,11 +46,6 @@ def is_version_greater(version_1: str, version_2: str) -> bool:
return split_version(version_1) > split_version(version_2) return split_version(version_1) > split_version(version_2)
FIXERS_TO_VERSIONS = {
'3.1.0': legacy_cookies.fix_layout
}
def upgrade_session(env: Environment, args: argparse.Namespace, hostname: str, session_name: str): def upgrade_session(env: Environment, args: argparse.Namespace, hostname: str, session_name: str):
session = get_httpie_session( session = get_httpie_session(
env=env, env=env,
@ -52,7 +53,7 @@ def upgrade_session(env: Environment, args: argparse.Namespace, hostname: str, s
session_name=session_name, session_name=session_name,
host=hostname, host=hostname,
url=hostname, url=hostname,
refactor_mode=True suppress_legacy_warnings=True
) )
session_name = session.path.stem session_name = session.path.stem

View File

@ -13,12 +13,16 @@ from typing import Any, Dict, List, Optional, Union
from requests.auth import AuthBase from requests.auth import AuthBase
from requests.cookies import RequestsCookieJar, remove_cookie_by_name from requests.cookies import RequestsCookieJar, remove_cookie_by_name
from .context import Environment from .context import Environment, Levels
from .cli.dicts import HTTPHeadersDict from .cli.dicts import HTTPHeadersDict
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
from .utils import url_as_host from .utils import url_as_host
from .plugins.registry import plugin_manager from .plugins.registry import plugin_manager
from .legacy import cookie_format as legacy_cookies
from .legacy import (
v3_1_0_session_cookie_format as legacy_cookies,
v3_2_0_session_header_format as legacy_headers
)
SESSIONS_DIR_NAME = 'sessions' SESSIONS_DIR_NAME = 'sessions'
@ -67,6 +71,23 @@ def materialize_cookie(cookie: Cookie) -> Dict[str, Any]:
return materialized_cookie return materialized_cookie
def materialize_cookies(jar: RequestsCookieJar) -> List[Dict[str, Any]]:
return [
materialize_cookie(cookie)
for cookie in jar
]
def materialize_headers(headers: Dict[str, str]) -> List[Dict[str, Any]]:
return [
{
'name': name,
'value': value
}
for name, value in headers.copy().items()
]
def get_httpie_session( def get_httpie_session(
env: Environment, env: Environment,
config_dir: Path, config_dir: Path,
@ -74,7 +95,7 @@ def get_httpie_session(
host: Optional[str], host: Optional[str],
url: str, url: str,
*, *,
refactor_mode: bool = False suppress_legacy_warnings: bool = False
) -> 'Session': ) -> 'Session':
bound_hostname = host or url_as_host(url) bound_hostname = host or url_as_host(url)
if not bound_hostname: if not bound_hostname:
@ -93,7 +114,7 @@ def get_httpie_session(
env=env, env=env,
session_id=session_id, session_id=session_id,
bound_host=strip_port(bound_hostname), bound_host=strip_port(bound_hostname),
refactor_mode=refactor_mode suppress_legacy_warnings=suppress_legacy_warnings
) )
session.load() session.load()
return session return session
@ -109,30 +130,29 @@ class Session(BaseConfigDict):
env: Environment, env: Environment,
bound_host: str, bound_host: str,
session_id: str, session_id: str,
refactor_mode: bool = False, suppress_legacy_warnings: bool = False,
): ):
super().__init__(path=Path(path)) super().__init__(path=Path(path))
self['headers'] = {}
# Default values for the session files
self['headers'] = []
self['cookies'] = [] self['cookies'] = []
self['auth'] = { self['auth'] = {
'type': None, 'type': None,
'username': None, 'username': None,
'password': None 'password': None
} }
# Runtime state of the Session objects.
self.env = env self.env = env
self._headers = HTTPHeadersDict()
self.cookie_jar = RequestsCookieJar() self.cookie_jar = RequestsCookieJar()
self.session_id = session_id self.session_id = session_id
self.bound_host = bound_host self.bound_host = bound_host
self.refactor_mode = refactor_mode self.suppress_legacy_warnings = suppress_legacy_warnings
def pre_process_data(self, data: Dict[str, Any]) -> Dict[str, Any]: def _add_cookies(self, cookies: List[Dict[str, Any]]) -> None:
cookies = data.get('cookies') for cookie in cookies:
if cookies:
normalized_cookies = legacy_cookies.pre_process(self, cookies)
else:
normalized_cookies = []
for cookie in normalized_cookies:
domain = cookie.get('domain', '') domain = cookie.get('domain', '')
if domain is None: if domain is None:
# domain = None means explicitly lack of cookie, though # domain = None means explicitly lack of cookie, though
@ -143,29 +163,38 @@ class Session(BaseConfigDict):
self.cookie_jar.set(**cookie) self.cookie_jar.set(**cookie)
def pre_process_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
for key, deserializer, importer in [
('cookies', legacy_cookies.pre_process, self._add_cookies),
('headers', legacy_headers.pre_process, self._headers.update),
]:
values = data.get(key)
if values:
normalized_values = deserializer(self, values)
else:
normalized_values = []
importer(normalized_values)
return data return data
def post_process_data(self, data: Dict[str, Any]) -> Dict[str, Any]: def post_process_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
cookies = data.get('cookies') for key, store, serializer, exporter in [
('cookies', self.cookie_jar, materialize_cookies, legacy_cookies.post_process),
('headers', self._headers, materialize_headers, legacy_headers.post_process),
]:
original_type = type(data.get(key))
values = serializer(store)
normalized_cookies = [ data[key] = exporter(
materialize_cookie(cookie) values,
for cookie in self.cookie_jar original_type=original_type
]
data['cookies'] = legacy_cookies.post_process(
normalized_cookies,
original_type=type(cookies)
) )
return data return data
def update_headers(self, request_headers: HTTPHeadersDict): def _compute_new_headers(self, request_headers: HTTPHeadersDict) -> HTTPHeadersDict:
""" new_headers = HTTPHeadersDict()
Update the session headers with the request ones while ignoring
certain name prefixes.
"""
headers = self.headers
for name, value in request_headers.copy().items(): for name, value in request_headers.copy().items():
if value is None: if value is None:
continue # Ignore explicitly unset headers continue # Ignore explicitly unset headers
@ -183,24 +212,40 @@ class Session(BaseConfigDict):
morsel['path'] = DEFAULT_COOKIE_PATH morsel['path'] = DEFAULT_COOKIE_PATH
self.cookie_jar.set(cookie_name, morsel) self.cookie_jar.set(cookie_name, morsel)
all_cookie_headers = request_headers.getall(name) request_headers.remove_item(name, original_value)
if len(all_cookie_headers) > 1:
all_cookie_headers.remove(original_value)
else:
request_headers.popall(name)
continue continue
for prefix in SESSION_IGNORED_HEADER_PREFIXES: for prefix in SESSION_IGNORED_HEADER_PREFIXES:
if name.lower().startswith(prefix.lower()): if name.lower().startswith(prefix.lower()):
break break
else: else:
headers[name] = value new_headers.add(name, value)
self['headers'] = dict(headers) return new_headers
def update_headers(self, request_headers: HTTPHeadersDict):
"""
Update the session headers with the request ones while ignoring
certain name prefixes.
"""
new_headers = self._compute_new_headers(request_headers)
new_keys = new_headers.copy().keys()
# New headers will take priority over the existing ones, and override
# them directly instead of extending them.
for key, value in self._headers.copy().items():
if key in new_keys:
continue
new_headers.add(key, value)
self._headers = new_headers
@property @property
def headers(self) -> HTTPHeadersDict: def headers(self) -> HTTPHeadersDict:
return HTTPHeadersDict(self['headers']) return self._headers.copy()
@property @property
def cookies(self) -> RequestsCookieJar: def cookies(self) -> RequestsCookieJar:
@ -257,3 +302,17 @@ class Session(BaseConfigDict):
@property @property
def is_anonymous(self): def is_anonymous(self):
return is_anonymous_session(self.session_id) return is_anonymous_session(self.session_id)
def warn_legacy_usage(self, warning: str) -> None:
if self.suppress_legacy_warnings:
return None
self.env.log_error(
warning,
level=Levels.WARNING
)
# We don't want to spam multiple warnings on each usage,
# so if there is already a warning for the legacy usage
# we'll skip the next ones.
self.suppress_legacy_warnings = True

View File

@ -27,5 +27,5 @@
"value": "bar" "value": "bar"
} }
], ],
"headers": {} "headers": []
} }

View File

@ -27,5 +27,5 @@
"value": "bar" "value": "bar"
} }
], ],
"headers": {} "headers": []
} }

View File

@ -26,8 +26,14 @@
"value": "bar" "value": "bar"
} }
], ],
"headers": { "headers": [
"X-Data": "value", {
"X-Foo": "bar" "name": "X-Data",
"value": "value"
},
{
"name": "X-Foo",
"value": "bar"
} }
]
} }

View File

@ -10,5 +10,5 @@
"username": null "username": null
}, },
"cookies": [], "cookies": [],
"headers": {} "headers": []
} }

View File

@ -10,5 +10,5 @@
"username": null "username": null
}, },
"cookies": [], "cookies": [],
"headers": {} "headers": []
} }

View File

@ -0,0 +1,14 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "__version__"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [],
"headers": []
}

View File

@ -0,0 +1,14 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "__version__"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [],
"headers": []
}

View File

@ -0,0 +1,40 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "__version__"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [
{
"domain": __host__,
"expires": null,
"name": "baz",
"path": "/",
"secure": false,
"value": "quux"
},
{
"domain": __host__,
"expires": null,
"name": "foo",
"path": "/",
"secure": false,
"value": "bar"
}
],
"headers": [
{
"name": "X-Data",
"value": "value"
},
{
"name": "X-Foo",
"value": "bar"
}
]
}

View File

@ -0,0 +1,23 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "__version__"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [],
"headers": [
{
"name": "foo",
"value": "bar"
},
{
"name": "baz",
"value": "quux"
}
]
}

View File

@ -0,0 +1,39 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "__version__"
},
"auth": {
"raw_auth": "foo:bar",
"type": "basic"
},
"cookies": [
{
"domain": null,
"expires": null,
"name": "baz",
"path": "/",
"secure": false,
"value": "quux"
},
{
"domain": null,
"expires": null,
"name": "foo",
"path": "/",
"secure": false,
"value": "bar"
}
],
"headers": [
{
"name": "X-Data",
"value": "value"
},
{
"name": "X-Foo",
"value": "bar"
}
]
}

View File

@ -0,0 +1,23 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "3.2.0"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [],
"headers": [
{
"name": "X-Data",
"value": "value"
},
{
"name": "X-Foo",
"value": "bar"
}
]
}

View File

@ -23,5 +23,5 @@
"value": "bar" "value": "bar"
} }
}, },
"headers": {} "headers": []
} }

View File

@ -23,5 +23,5 @@
"value": "bar" "value": "bar"
} }
}, },
"headers": {} "headers": []
} }

View File

@ -22,8 +22,14 @@
"value": "bar" "value": "bar"
} }
}, },
"headers": { "headers": [
"X-Data": "value", {
"X-Foo": "bar" "name": "X-Data",
"value": "value"
},
{
"name": "X-Foo",
"value": "bar"
} }
]
} }

View File

@ -10,5 +10,5 @@
"username": null "username": null
}, },
"cookies": {}, "cookies": {},
"headers": {} "headers": []
} }

View File

@ -10,5 +10,5 @@
"username": null "username": null
}, },
"cookies": [], "cookies": [],
"headers": {} "headers": []
} }

View File

@ -0,0 +1,14 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "3.0.2"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [],
"headers": {}
}

View File

@ -0,0 +1,14 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "3.0.2"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [],
"headers": []
}

View File

@ -0,0 +1,30 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "3.0.2"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": {
"baz": {
"expires": null,
"path": "/",
"secure": false,
"value": "quux"
},
"foo": {
"expires": null,
"path": "/",
"secure": false,
"value": "bar"
}
},
"headers": {
"X-Data": "value",
"X-Foo": "bar"
}
}

View File

@ -0,0 +1,17 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "3.1.0"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [],
"headers": {
"foo": "bar",
"baz": "quux"
}
}

View File

@ -0,0 +1,33 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "3.1.0"
},
"auth": {
"raw_auth": "foo:bar",
"type": "basic"
},
"cookies": [
{
"domain": null,
"name": "baz",
"expires": null,
"path": "/",
"secure": false,
"value": "quux"
},
{
"domain": null,
"name": "foo",
"expires": null,
"path": "/",
"secure": false,
"value": "bar"
}
],
"headers": {
"X-Data": "value",
"X-Foo": "bar"
}
}

View File

@ -0,0 +1,23 @@
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "3.2.0"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": [],
"headers": [
{
"name": "X-Data",
"value": "value"
},
{
"name": "X-Foo",
"value": "bar"
}
]
}

View File

@ -664,7 +664,7 @@ def test_old_session_cookie_layout_loading(basic_session, httpbin, mock_env):
@pytest.mark.parametrize('layout_type', [ @pytest.mark.parametrize('layout_type', [
dict, list dict, list
]) ])
def test_session_cookie_layout_preservance(basic_session, mock_env, layout_type): def test_session_cookie_layout_preservation(basic_session, mock_env, layout_type):
with open_session(basic_session, mock_env) as session: with open_session(basic_session, mock_env) as session:
session['cookies'] = layout_type() session['cookies'] = layout_type()
session.cookies.set('foo', 'bar') session.cookies.set('foo', 'bar')
@ -677,7 +677,7 @@ def test_session_cookie_layout_preservance(basic_session, mock_env, layout_type)
@pytest.mark.parametrize('layout_type', [ @pytest.mark.parametrize('layout_type', [
dict, list dict, list
]) ])
def test_session_cookie_layout_preservance_on_new_cookies(basic_session, httpbin, mock_env, layout_type): def test_session_cookie_layout_preservation_on_new_cookies(basic_session, httpbin, mock_env, layout_type):
with open_session(basic_session, mock_env) as session: with open_session(basic_session, mock_env) as session:
session['cookies'] = layout_type() session['cookies'] = layout_type()
session.cookies.set('foo', 'bar') session.cookies.set('foo', 'bar')
@ -690,3 +690,113 @@ def test_session_cookie_layout_preservance_on_new_cookies(basic_session, httpbin
with open_session(basic_session, mock_env, read_only=True) as session: with open_session(basic_session, mock_env, read_only=True) as session:
assert isinstance(session['cookies'], layout_type) 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