blob: a375e25335a051a4c2fe0d368ec482c70433fbbd [file] [log] [blame]
import pytest
from datetime import timedelta
from webob import cookies
from webob.compat import text_
from webob.compat import native_
py2only = pytest.mark.skipif("sys.version_info >= (3, 0)")
py3only = pytest.mark.skipif("sys.version_info < (3, 0)")
def setup_module(module):
cookies._should_raise = True
def teardown_module(module):
cookies._should_raise = False
def test_cookie_empty():
c = cookies.Cookie() # empty cookie
assert repr(c) == "<Cookie: []>"
def test_cookie_one_value():
c = cookies.Cookie("dismiss-top=6")
vals = list(c.values())
assert len(vals) == 1
assert vals[0].name == b"dismiss-top"
assert vals[0].value == b"6"
def test_cookie_one_value_with_trailing_semi():
c = cookies.Cookie("dismiss-top=6;")
vals = list(c.values())
assert len(vals) == 1
assert vals[0].name == b"dismiss-top"
assert vals[0].value == b"6"
c = cookies.Cookie("dismiss-top=6;")
def test_cookie_escaped_unquoted():
assert list(cookies.parse_cookie("x=\\040")) == [(b"x", b" ")]
def test_cookie_complex():
c = cookies.Cookie(
"dismiss-top=6; CP=null*, "
'PHPSESSID=0a539d42abc001cdc762809248d4beed, a="42,"'
)
def d(v):
return v.decode("ascii")
c_dict = dict((d(k), d(v.value)) for k, v in c.items())
assert c_dict == {
"a": "42,",
"CP": "null*",
"PHPSESSID": "0a539d42abc001cdc762809248d4beed",
"dismiss-top": "6",
}
def test_cookie_complex_serialize():
c = cookies.Cookie(
"dismiss-top=6; CP=null*, " 'PHPSESSID=0a539d42abc001cdc762809248d4beed, a="42"'
)
assert (
c.serialize() == "CP=null*; PHPSESSID=0a539d42abc001cdc762809248d4beed;"
" a=42; dismiss-top=6"
)
def test_cookie_load_multiple():
c = cookies.Cookie("a=1; Secure=true")
vals = list(c.values())
assert len(vals) == 1
assert c[b"a"][b"secure"] == b"true"
def test_cookie_secure():
c = cookies.Cookie()
c[text_("foo")] = b"bar"
c[b"foo"].secure = True
assert c.serialize() == "foo=bar; secure"
def test_cookie_httponly():
c = cookies.Cookie()
c["foo"] = b"bar"
c[b"foo"].httponly = True
assert c.serialize() == "foo=bar; HttpOnly"
def test_cookie_samesite_strict():
c = cookies.Cookie()
c[b"foo"] = b"bar"
c[b"foo"].samesite = b"Strict"
assert c.serialize() == "foo=bar; SameSite=Strict"
def test_cookie_samesite_lax():
c = cookies.Cookie()
c[b"foo"] = b"bar"
c[b"foo"].samesite = b"Lax"
assert c.serialize() == "foo=bar; SameSite=Lax"
def test_cookie_reserved_keys():
c = cookies.Cookie("dismiss-top=6; CP=null*; $version=42; a=42")
assert "$version" not in c
c = cookies.Cookie("$reserved=42; a=$42")
assert list(c.keys()) == [b"a"]
def test_serialize_cookie_date():
"""
Testing webob.cookies.serialize_cookie_date.
Missing scenarios:
* input value is an str, should be returned verbatim
* input value is an int, should be converted to timedelta and we
should continue the rest of the process
"""
date_one = cookies.serialize_cookie_date(b"Tue, 04-Jan-2011 13:43:50 GMT")
assert date_one == b"Tue, 04-Jan-2011 13:43:50 GMT"
date_two = cookies.serialize_cookie_date(text_("Tue, 04-Jan-2011 13:43:50 GMT"))
assert date_two == b"Tue, 04-Jan-2011 13:43:50 GMT"
assert cookies.serialize_cookie_date(None) is None
cdate_delta = cookies.serialize_cookie_date(timedelta(seconds=10))
cdate_int = cookies.serialize_cookie_date(10)
assert cdate_delta == cdate_int
def test_serialize_samesite():
assert cookies.serialize_samesite(b"Lax") == b"Lax"
assert cookies.serialize_samesite(b"Strict") == b"Strict"
with pytest.raises(ValueError):
cookies.serialize_samesite(b"SomethingElse")
def test_ch_unquote():
assert cookies._unquote(b'"hello world') == b'"hello world'
assert cookies._unquote(b"hello world") == b"hello world"
assert cookies._unquote(b'"hello world"') == b"hello world"
# Spaces are not valid in cookies, we support getting them, but won't
# support sending them
with pytest.raises(ValueError):
cookies._value_quote(b"hello world")
# quotation mark escaped w/ backslash is unquoted correctly (support
# pre webob 1.3 cookies)
assert cookies._unquote(b'"\\""') == b'"'
# we also are able to unquote the newer \\042 serialization of quotation
# mark
assert cookies._unquote(b'"\\042"') == b'"'
# New cookies can not contain quotes.
with pytest.raises(ValueError):
cookies._value_quote(b'"')
# backslash escaped w/ backslash is unquoted correctly (support
# pre webob 1.3 cookies)
assert cookies._unquote(b'"\\\\"') == b"\\"
# we also are able to unquote the newer \\134 serialization of backslash
assert cookies._unquote(b'"\\134"') == b"\\"
# Cookies may not contain a backslash
with pytest.raises(ValueError):
cookies._value_quote(b"\\")
# misc byte escaped as octal
assert cookies._unquote(b'"\\377"') == b"\xff"
with pytest.raises(ValueError):
cookies._value_quote(b"\xff")
# combination
assert cookies._unquote(b'"a\\"\\377"') == b'a"\xff'
with pytest.raises(ValueError):
cookies._value_quote(b'a"\xff')
def test_cookie_invalid_name():
c = cookies.Cookie()
c["La Pe\xc3\xb1a"] = "1"
assert len(c) == 0
def test_morsel_serialize_with_expires():
morsel = cookies.Morsel(b"bleh", b"blah")
morsel.expires = b"Tue, 04-Jan-2011 13:43:50 GMT"
result = morsel.serialize()
assert result, "bleh=blah; expires=Tue, 04-Jan-2011 13:43:50 GMT"
def test_serialize_max_age_timedelta():
import datetime
val = datetime.timedelta(86400)
result = cookies.serialize_max_age(val)
assert result == b"7464960000"
def test_serialize_max_age_int():
val = 86400
result = cookies.serialize_max_age(val)
assert result == b"86400"
def test_serialize_max_age_str():
val = "86400"
result = cookies.serialize_max_age(val)
assert result == b"86400"
def test_parse_qmark_in_val():
v = r'x="\"\073\054\""; expires=Sun, 12-Jun-2011 23:16:01 GMT'
c = cookies.Cookie(v)
assert c[b"x"].value == b'";,"'
assert c[b"x"].expires, b"Sun == 12-Jun-2011 23:16:01 GMT"
def test_morsel_repr():
v = cookies.Morsel(b"a", b"b")
result = repr(v)
assert result == "<Morsel: a='b'>"
class TestRequestCookies(object):
def _makeOne(self, environ):
from webob.cookies import RequestCookies
return RequestCookies(environ)
def test_get_no_cache_key_in_environ_no_http_cookie_header(self):
environ = {}
inst = self._makeOne(environ)
assert inst.get("a") is None
parsed = environ["webob._parsed_cookies"]
assert parsed == ({}, "")
def test_get_no_cache_key_in_environ_has_http_cookie_header(self):
header = "a=1; b=2"
environ = {"HTTP_COOKIE": header}
inst = self._makeOne(environ)
assert inst.get("a") == "1"
parsed = environ["webob._parsed_cookies"][0]
assert parsed["a"] == "1"
assert parsed["b"] == "2"
assert environ["HTTP_COOKIE"] == header # no change
def test_get_cache_key_in_environ_no_http_cookie_header(self):
environ = {"webob._parsed_cookies": ({}, "")}
inst = self._makeOne(environ)
assert inst.get("a") is None
parsed = environ["webob._parsed_cookies"]
assert parsed == ({}, "")
def test_get_cache_key_in_environ_has_http_cookie_header(self):
header = "a=1; b=2"
environ = {"HTTP_COOKIE": header, "webob._parsed_cookies": ({}, "")}
inst = self._makeOne(environ)
assert inst.get("a") == "1"
parsed = environ["webob._parsed_cookies"][0]
assert parsed["a"] == "1"
assert parsed["b"] == "2"
assert environ["HTTP_COOKIE"] == header # no change
def test_get_missing_with_default(self):
environ = {}
inst = self._makeOne(environ)
assert inst.get("a", "") == ""
def test___setitem__name_not_string_type(self):
inst = self._makeOne({})
with pytest.raises(TypeError):
inst.__setitem__(None, 1)
def test___setitem__name_not_encodeable_to_ascii(self):
name = native_(b"La Pe\xc3\xb1a", "utf-8")
inst = self._makeOne({})
with pytest.raises(TypeError):
inst.__setitem__(name, "abc")
def test___setitem__name_not_rfc2109_valid(self):
name = "$a"
inst = self._makeOne({})
with pytest.raises(TypeError):
inst.__setitem__(name, "abc")
def test___setitem__value_not_string_type(self):
inst = self._makeOne({})
with pytest.raises(ValueError):
inst.__setitem__("a", None)
def test___setitem__value_not_utf_8_decodeable(self):
value = text_(b"La Pe\xc3\xb1a", "utf-8")
value = value.encode("utf-16")
inst = self._makeOne({})
with pytest.raises(ValueError):
inst.__setitem__("a", value)
def test__setitem__success_no_existing_headers(self):
value = native_(b"test_cookie", "utf-8")
environ = {}
inst = self._makeOne(environ)
inst["a"] = value
assert environ["HTTP_COOKIE"] == "a=test_cookie"
def test__setitem__success_append(self):
value = native_(b"test_cookie", "utf-8")
environ = {"HTTP_COOKIE": "a=1; b=2"}
inst = self._makeOne(environ)
inst["c"] = value
assert environ["HTTP_COOKIE"] == "a=1; b=2; c=test_cookie"
def test__setitem__success_replace(self):
environ = {"HTTP_COOKIE": 'a=1; b="La Pe\\303\\261a"; c=3'}
inst = self._makeOne(environ)
inst["b"] = "abc"
assert environ["HTTP_COOKIE"] == "a=1; b=abc; c=3"
inst["c"] = "4"
assert environ["HTTP_COOKIE"] == "a=1; b=abc; c=4"
def test__delitem__fail_no_http_cookie(self):
environ = {}
inst = self._makeOne(environ)
with pytest.raises(KeyError):
inst.__delitem__("a")
assert environ == {}
def test__delitem__fail_with_http_cookie(self):
environ = {"HTTP_COOKIE": ""}
inst = self._makeOne(environ)
with pytest.raises(KeyError):
inst.__delitem__("a")
assert environ == {"HTTP_COOKIE": ""}
def test__delitem__success(self):
environ = {"HTTP_COOKIE": "a=1"}
inst = self._makeOne(environ)
del inst["a"]
assert environ["HTTP_COOKIE"] == ""
assert inst._cache == {}
def test_keys(self):
environ = {"HTTP_COOKIE": 'a=1; b="La Pe\\303\\261a"; c=3'}
inst = self._makeOne(environ)
assert sorted(list(inst.keys())) == ["a", "b", "c"]
def test_values(self):
val = text_(b"La Pe\xc3\xb1a", "utf-8")
environ = {"HTTP_COOKIE": 'a=1; b="La Pe\\303\\261a"; c=3'}
inst = self._makeOne(environ)
assert sorted(list(inst.values())) == ["1", "3", val]
def test_items(self):
val = text_(b"La Pe\xc3\xb1a", "utf-8")
environ = {"HTTP_COOKIE": 'a=1; b="La Pe\\303\\261a"; c=3'}
inst = self._makeOne(environ)
assert sorted(list(inst.items())) == [("a", "1"), ("b", val), ("c", "3")]
@py2only
def test_iterkeys(self):
environ = {"HTTP_COOKIE": 'a=1; b="La Pe\\303\\261a"; c=3'}
inst = self._makeOne(environ)
assert sorted(list(inst.iterkeys())) == ["a", "b", "c"]
@py3only
def test_iterkeys_py3(self):
environ = {"HTTP_COOKIE": b'a=1; b="La Pe\\303\\261a"; c=3'.decode("utf-8")}
inst = self._makeOne(environ)
assert sorted(list(inst.keys())) == ["a", "b", "c"]
@py2only
def test_itervalues(self):
val = text_(b"La Pe\xc3\xb1a", "utf-8")
environ = {"HTTP_COOKIE": 'a=1; b="La Pe\\303\\261a"; c=3'}
inst = self._makeOne(environ)
sorted(list(inst.itervalues())) == ["1", "3", val]
@py3only
def test_itervalues_py3(self):
val = text_(b"La Pe\xc3\xb1a", "utf-8")
environ = {"HTTP_COOKIE": b'a=1; b="La Pe\\303\\261a"; c=3'.decode("utf-8")}
inst = self._makeOne(environ)
sorted(list(inst.values())) == ["1", "3", val]
@py2only
def test_iteritems(self):
val = text_(b"La Pe\xc3\xb1a", "utf-8")
environ = {"HTTP_COOKIE": 'a=1; b="La Pe\\303\\261a"; c=3'}
inst = self._makeOne(environ)
assert sorted(list(inst.iteritems())) == [("a", "1"), ("b", val), ("c", "3")]
@py3only
def test_iteritems_py3(self):
val = text_(b"La Pe\xc3\xb1a", "utf-8")
environ = {"HTTP_COOKIE": b'a=1; b="La Pe\\303\\261a"; c=3'.decode("utf-8")}
inst = self._makeOne(environ)
assert sorted(list(inst.items())) == [("a", "1"), ("b", val), ("c", "3")]
def test___contains__(self):
environ = {"HTTP_COOKIE": "a=1"}
inst = self._makeOne(environ)
assert "a" in inst
assert "b" not in inst
def test___iter__(self):
environ = {"HTTP_COOKIE": "a=1; b=2; c=3"}
inst = self._makeOne(environ)
assert sorted(list(iter(inst))) == ["a", "b", "c"]
def test___len__(self):
environ = {"HTTP_COOKIE": "a=1; b=2; c=3"}
inst = self._makeOne(environ)
assert len(inst) == 3
del inst["a"]
assert len(inst) == 2
def test_clear(self):
environ = {"HTTP_COOKIE": "a=1; b=2; c=3"}
inst = self._makeOne(environ)
inst.clear()
assert environ["HTTP_COOKIE"] == ""
assert inst.get("a") is None
def test___repr__(self):
environ = {"HTTP_COOKIE": "a=1; b=2; c=3"}
inst = self._makeOne(environ)
r = repr(inst)
assert r.startswith("<RequestCookies (dict-like) with values ")
assert r.endswith(">")
class TestCookieMakeCookie(object):
def makeOne(self, name, value, **kw):
from webob.cookies import make_cookie
return make_cookie(name, value, **kw)
def test_make_cookie_max_age(self):
cookie = self.makeOne("test_cookie", "value", max_age=500)
assert "test_cookie=value" in cookie
assert "Max-Age=500;" in cookie
assert "expires" in cookie
def test_make_cookie_max_age_timedelta(self):
from datetime import timedelta
cookie = self.makeOne("test_cookie", "value", max_age=timedelta(seconds=500))
assert "test_cookie=value" in cookie
assert "Max-Age=500;" in cookie
assert "expires" in cookie
assert "expires=500" not in cookie
def test_make_cookie_max_age_str_valid_int(self):
cookie = self.makeOne("test_cookie", "value", max_age="500")
assert "test_cookie=value" in cookie
assert "Max-Age=500;" in cookie
assert "expires" in cookie
assert "expires=500" not in cookie
def test_make_cookie_max_age_str_invalid_int(self):
with pytest.raises(ValueError):
self.makeOne("test_cookie", "value", max_age="test")
def test_make_cookie_comment(self):
cookie = self.makeOne("test_cookie", "value", comment="lolwhy")
assert "test_cookie=value" in cookie
assert "Comment=lolwhy" in cookie
def test_make_cookie_path(self):
cookie = self.makeOne("test_cookie", "value", path="/foo/bar/baz")
assert "test_cookie=value" in cookie
assert "Path=/foo/bar/baz" in cookie
@pytest.mark.parametrize("samesite", ["Strict", "Lax"])
def test_make_cookie_samesite(self, samesite):
cookie = self.makeOne("test_cookie", "value", samesite=samesite)
assert "test_cookie=value" in cookie
assert "SameSite=" + samesite in cookie
class CommonCookieProfile(object):
def makeDummyRequest(self, **kw):
class Dummy(object):
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
d = Dummy(**kw)
d.response = Dummy()
d.response.headerlist = list()
return d
def makeOneRequest(self):
request = self.makeDummyRequest(environ=dict())
request.environ["HTTP_HOST"] = "www.example.net"
request.cookies = dict()
return request
class TestCookieProfile(CommonCookieProfile):
def makeOne(self, name="uns", **kw):
if "request" in kw:
request = kw["request"]
del kw["request"]
else:
request = self.makeOneRequest()
from webob.cookies import CookieProfile
return CookieProfile(name, **kw)(request)
def test_cookie_creation(self):
cookie = self.makeOne()
from webob.cookies import CookieProfile
assert isinstance(cookie, CookieProfile)
def test_cookie_name(self):
cookie = self.makeOne()
cookie_list = cookie.get_headers("test")
for cookie in cookie_list:
assert cookie[1].startswith("uns")
assert 'uns="";' not in cookie[1]
def test_cookie_no_request(self):
from webob.cookies import CookieProfile
cookie = CookieProfile("uns")
with pytest.raises(ValueError):
cookie.get_value()
def test_get_value_serializer_raises_value_error(self):
class RaisingSerializer(object):
def loads(self, val):
raise ValueError("foo")
cookie = self.makeOne(serializer=RaisingSerializer())
assert cookie.get_value() is None
def test_with_cookies(self):
request = self.makeOneRequest()
request.cookies["uns"] = "InRlc3Qi"
cookie = self.makeOne(request=request)
ret = cookie.get_value()
assert ret == "test"
def test_with_invalid_cookies(self):
request = self.makeOneRequest()
request.cookies["uns"] = "InRlc3Q"
cookie = self.makeOne(request=request)
ret = cookie.get_value()
assert ret is None
class TestSignedCookieProfile(CommonCookieProfile):
def makeOne(self, secret="seekrit", salt="salty", name="uns", **kw):
if "request" in kw:
request = kw["request"]
del kw["request"]
else:
request = self.makeOneRequest()
from webob.cookies import SignedCookieProfile as CookieProfile
return CookieProfile(secret, salt, name, **kw)(request)
def test_cookie_name(self):
cookie = self.makeOne()
cookie_list = cookie.get_headers("test")
for cookie in cookie_list:
assert cookie[1].startswith("uns")
assert 'uns="";' not in cookie[1]
def test_cookie_expire(self):
cookie = self.makeOne()
cookie_list = cookie.get_headers(None, max_age=0)
for cookie in cookie_list:
assert "Max-Age=0;" in cookie[1]
def test_cookie_max_age(self):
cookie = self.makeOne()
cookie_list = cookie.get_headers("test", max_age=60)
for cookie in cookie_list:
assert "Max-Age=60;" in cookie[1]
assert "expires=" in cookie[1]
def test_cookie_raw(self):
cookie = self.makeOne()
cookie_list = cookie.get_headers("test")
assert isinstance(cookie_list, list)
def test_set_cookie(self):
request = self.makeOneRequest()
cookie = self.makeOne(request=request)
ret = cookie.set_cookies(request.response, "test")
assert ret == request.response
def test_no_cookie(self):
cookie = self.makeOne()
ret = cookie.get_value()
assert ret is None
def test_with_cookies(self):
request = self.makeOneRequest()
request.cookies["uns"] = (
"FLIoEwZcKG6ITQSqbYcUNnPljwOcGNs25JRVCSoZcx_uX-OA1AhssA-CNeVKpWksQ"
"a0ktMhuQDdjzmDwgzbptiJ0ZXN0Ig"
)
cookie = self.makeOne(request=request)
ret = cookie.get_value()
assert ret == "test"
def test_with_bad_cookie_invalid_base64(self):
request = self.makeOneRequest()
request.cookies["uns"] = (
"gAJVBHRlc3RxAS4KjKfwGmCkliC4ba99rWUdpy_{}riHzK7MQFPsbSgYTgALHa"
"SHrRkd3lyE8c4w5ruxAKOyj2h5oF69Ix7ERZv_"
)
cookie = self.makeOne(request=request)
val = cookie.get_value()
assert val is None
def test_with_bad_cookie_invalid_signature(self):
request = self.makeOneRequest()
request.cookies["uns"] = (
"InRlc3QiFLIoEwZcKG6ITQSqbYcUNnPljwOcGNs25JRVCSoZcx/uX+OA1AhssA"
"+CNeVKpWksQa0ktMhuQDdjzmDwgzbptg=="
)
cookie = self.makeOne(secret="sekrit!", request=request)
val = cookie.get_value()
assert val is None
def test_with_domain(self):
cookie = self.makeOne(domains=("testing.example.net",))
ret = cookie.get_headers("test")
passed = False
for cookie in ret:
if "Domain=testing.example.net" in cookie[1]:
passed = True
assert passed
assert len(ret) == 1
def test_with_domains(self):
cookie = self.makeOne(domains=("testing.example.net", "testing2.example.net"))
ret = cookie.get_headers("test")
passed = 0
for cookie in ret:
if "Domain=testing.example.net" in cookie[1]:
passed += 1
if "Domain=testing2.example.net" in cookie[1]:
passed += 1
assert passed == 2
assert len(ret) == 2
def test_flag_secure(self):
cookie = self.makeOne(secure=True)
ret = cookie.get_headers("test")
for cookie in ret:
assert "; secure" in cookie[1]
def test_flag_http_only(self):
cookie = self.makeOne(httponly=True)
ret = cookie.get_headers("test")
for cookie in ret:
assert "; HttpOnly" in cookie[1]
@pytest.mark.parametrize("samesite", [b"Strict", b"Lax"])
def test_with_samesite_bytes(self, samesite):
cookie = self.makeOne(samesite=samesite)
ret = cookie.get_headers("test")
for cookie in ret:
assert "; SameSite=" + samesite.decode("ascii") in cookie[1]
@pytest.mark.parametrize("samesite", ["Strict", "Lax"])
def test_with_samesite(self, samesite):
cookie = self.makeOne(samesite=samesite)
ret = cookie.get_headers("test")
for cookie in ret:
assert "; SameSite=" + samesite in cookie[1]
def test_cookie_length(self):
cookie = self.makeOne()
longstring = "a" * 4096
with pytest.raises(ValueError):
cookie.get_headers(longstring)
def test_very_long_key(self):
longstring = "a" * 1024
cookie = self.makeOne(secret=longstring)
cookie.get_headers("test")
def serialize(secret, salt, data):
import hmac
import base64
import json
from hashlib import sha1
from webob.compat import bytes_
salted_secret = bytes_(salt or "", "utf-8") + bytes_(secret, "utf-8")
cstruct = bytes_(json.dumps(data))
sig = hmac.new(salted_secret, cstruct, sha1).digest()
return base64.urlsafe_b64encode(sig + cstruct).rstrip(b"=")
class TestSignedSerializer(object):
def makeOne(self, secret, salt, hashalg="sha1", **kw):
from webob.cookies import SignedSerializer
return SignedSerializer(secret, salt, hashalg=hashalg, **kw)
def test_serialize(self):
ser = self.makeOne("seekrit", "salty")
assert ser.dumps("test") == serialize("seekrit", "salty", "test")
def test_deserialize(self):
ser = self.makeOne("seekrit", "salty")
assert ser.loads(serialize("seekrit", "salty", "test")) == "test"
def test_with_highorder_secret(self):
secret = b"\xce\xb1\xce\xb2\xce\xb3\xce\xb4".decode("utf-8")
ser = self.makeOne(secret, "salty")
assert ser.loads(serialize(secret, "salty", "test")) == "test"
def test_with_highorder_salt(self):
salt = b"\xce\xb1\xce\xb2\xce\xb3\xce\xb4".decode("utf-8")
ser = self.makeOne("secret", salt)
assert ser.loads(serialize("secret", salt, "test")) == "test"
# bw-compat with webob <= 1.3.1 where secrets were encoded with latin-1
def test_with_latin1_secret(self):
secret = b"La Pe\xc3\xb1a"
ser = self.makeOne(secret.decode("latin-1"), "salty")
assert ser.loads(serialize(secret, "salty", "test")), "test"
# bw-compat with webob <= 1.3.1 where salts were encoded with latin-1
def test_with_latin1_salt(self):
salt = b"La Pe\xc3\xb1a"
ser = self.makeOne("secret", salt.decode("latin-1"))
assert ser.loads(serialize("secret", salt, "test")) == "test"