1197 lines
38 KiB
Python
1197 lines
38 KiB
Python
import io
|
|
import pickle
|
|
import tempfile
|
|
import typing as t
|
|
from contextlib import contextmanager
|
|
from copy import copy
|
|
from copy import deepcopy
|
|
|
|
import pytest
|
|
|
|
from werkzeug import datastructures as ds
|
|
from werkzeug import http
|
|
from werkzeug.exceptions import BadRequestKeyError
|
|
|
|
|
|
class TestNativeItermethods:
|
|
def test_basic(self):
|
|
class StupidDict:
|
|
def keys(self, multi=1):
|
|
return iter(["a", "b", "c"] * multi)
|
|
|
|
def values(self, multi=1):
|
|
return iter([1, 2, 3] * multi)
|
|
|
|
def items(self, multi=1):
|
|
return iter(
|
|
zip(iter(self.keys(multi=multi)), iter(self.values(multi=multi)))
|
|
)
|
|
|
|
d = StupidDict()
|
|
expected_keys = ["a", "b", "c"]
|
|
expected_values = [1, 2, 3]
|
|
expected_items = list(zip(expected_keys, expected_values))
|
|
|
|
assert list(d.keys()) == expected_keys
|
|
assert list(d.values()) == expected_values
|
|
assert list(d.items()) == expected_items
|
|
|
|
assert list(d.keys(2)) == expected_keys * 2
|
|
assert list(d.values(2)) == expected_values * 2
|
|
assert list(d.items(2)) == expected_items * 2
|
|
|
|
|
|
class _MutableMultiDictTests:
|
|
storage_class: t.Type["ds.MultiDict"]
|
|
|
|
def test_pickle(self):
|
|
cls = self.storage_class
|
|
|
|
def create_instance(module=None):
|
|
if module is None:
|
|
d = cls()
|
|
else:
|
|
old = cls.__module__
|
|
cls.__module__ = module
|
|
d = cls()
|
|
cls.__module__ = old
|
|
d.setlist(b"foo", [1, 2, 3, 4])
|
|
d.setlist(b"bar", b"foo bar baz".split())
|
|
return d
|
|
|
|
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
|
d = create_instance()
|
|
s = pickle.dumps(d, protocol)
|
|
ud = pickle.loads(s)
|
|
assert type(ud) == type(d) # noqa: E721
|
|
assert ud == d
|
|
alternative = pickle.dumps(create_instance("werkzeug"), protocol)
|
|
assert pickle.loads(alternative) == d
|
|
ud[b"newkey"] = b"bla"
|
|
assert ud != d
|
|
|
|
def test_multidict_dict_interop(self):
|
|
# https://github.com/pallets/werkzeug/pull/2043
|
|
md = self.storage_class([("a", 1), ("a", 2)])
|
|
assert dict(md)["a"] != [1, 2]
|
|
assert dict(md)["a"] == 1
|
|
assert dict(md) == {**md} == {"a": 1}
|
|
|
|
def test_basic_interface(self):
|
|
md = self.storage_class()
|
|
assert isinstance(md, dict)
|
|
|
|
mapping = [
|
|
("a", 1),
|
|
("b", 2),
|
|
("a", 2),
|
|
("d", 3),
|
|
("a", 1),
|
|
("a", 3),
|
|
("d", 4),
|
|
("c", 3),
|
|
]
|
|
md = self.storage_class(mapping)
|
|
|
|
# simple getitem gives the first value
|
|
assert md["a"] == 1
|
|
assert md["c"] == 3
|
|
with pytest.raises(KeyError):
|
|
md["e"]
|
|
assert md.get("a") == 1
|
|
|
|
# list getitem
|
|
assert md.getlist("a") == [1, 2, 1, 3]
|
|
assert md.getlist("d") == [3, 4]
|
|
# do not raise if key not found
|
|
assert md.getlist("x") == []
|
|
|
|
# simple setitem overwrites all values
|
|
md["a"] = 42
|
|
assert md.getlist("a") == [42]
|
|
|
|
# list setitem
|
|
md.setlist("a", [1, 2, 3])
|
|
assert md["a"] == 1
|
|
assert md.getlist("a") == [1, 2, 3]
|
|
|
|
# verify that it does not change original lists
|
|
l1 = [1, 2, 3]
|
|
md.setlist("a", l1)
|
|
del l1[:]
|
|
assert md["a"] == 1
|
|
|
|
# setdefault, setlistdefault
|
|
assert md.setdefault("u", 23) == 23
|
|
assert md.getlist("u") == [23]
|
|
del md["u"]
|
|
|
|
md.setlist("u", [-1, -2])
|
|
|
|
# delitem
|
|
del md["u"]
|
|
with pytest.raises(KeyError):
|
|
md["u"]
|
|
del md["d"]
|
|
assert md.getlist("d") == []
|
|
|
|
# keys, values, items, lists
|
|
assert list(sorted(md.keys())) == ["a", "b", "c"]
|
|
assert list(sorted(md.keys())) == ["a", "b", "c"]
|
|
|
|
assert list(sorted(md.values())) == [1, 2, 3]
|
|
assert list(sorted(md.values())) == [1, 2, 3]
|
|
|
|
assert list(sorted(md.items())) == [("a", 1), ("b", 2), ("c", 3)]
|
|
assert list(sorted(md.items(multi=True))) == [
|
|
("a", 1),
|
|
("a", 2),
|
|
("a", 3),
|
|
("b", 2),
|
|
("c", 3),
|
|
]
|
|
assert list(sorted(md.items())) == [("a", 1), ("b", 2), ("c", 3)]
|
|
assert list(sorted(md.items(multi=True))) == [
|
|
("a", 1),
|
|
("a", 2),
|
|
("a", 3),
|
|
("b", 2),
|
|
("c", 3),
|
|
]
|
|
|
|
assert list(sorted(md.lists())) == [("a", [1, 2, 3]), ("b", [2]), ("c", [3])]
|
|
assert list(sorted(md.lists())) == [("a", [1, 2, 3]), ("b", [2]), ("c", [3])]
|
|
|
|
# copy method
|
|
c = md.copy()
|
|
assert c["a"] == 1
|
|
assert c.getlist("a") == [1, 2, 3]
|
|
|
|
# copy method 2
|
|
c = copy(md)
|
|
assert c["a"] == 1
|
|
assert c.getlist("a") == [1, 2, 3]
|
|
|
|
# deepcopy method
|
|
c = md.deepcopy()
|
|
assert c["a"] == 1
|
|
assert c.getlist("a") == [1, 2, 3]
|
|
|
|
# deepcopy method 2
|
|
c = deepcopy(md)
|
|
assert c["a"] == 1
|
|
assert c.getlist("a") == [1, 2, 3]
|
|
|
|
# update with a multidict
|
|
od = self.storage_class([("a", 4), ("a", 5), ("y", 0)])
|
|
md.update(od)
|
|
assert md.getlist("a") == [1, 2, 3, 4, 5]
|
|
assert md.getlist("y") == [0]
|
|
|
|
# update with a regular dict
|
|
md = c
|
|
od = {"a": 4, "y": 0}
|
|
md.update(od)
|
|
assert md.getlist("a") == [1, 2, 3, 4]
|
|
assert md.getlist("y") == [0]
|
|
|
|
# pop, poplist, popitem, popitemlist
|
|
assert md.pop("y") == 0
|
|
assert "y" not in md
|
|
assert md.poplist("a") == [1, 2, 3, 4]
|
|
assert "a" not in md
|
|
assert md.poplist("missing") == []
|
|
|
|
# remaining: b=2, c=3
|
|
popped = md.popitem()
|
|
assert popped in [("b", 2), ("c", 3)]
|
|
popped = md.popitemlist()
|
|
assert popped in [("b", [2]), ("c", [3])]
|
|
|
|
# type conversion
|
|
md = self.storage_class({"a": "4", "b": ["2", "3"]})
|
|
assert md.get("a", type=int) == 4
|
|
assert md.getlist("b", type=int) == [2, 3]
|
|
|
|
# repr
|
|
md = self.storage_class([("a", 1), ("a", 2), ("b", 3)])
|
|
assert "('a', 1)" in repr(md)
|
|
assert "('a', 2)" in repr(md)
|
|
assert "('b', 3)" in repr(md)
|
|
|
|
# add and getlist
|
|
md.add("c", "42")
|
|
md.add("c", "23")
|
|
assert md.getlist("c") == ["42", "23"]
|
|
md.add("c", "blah")
|
|
assert md.getlist("c", type=int) == [42, 23]
|
|
|
|
# setdefault
|
|
md = self.storage_class()
|
|
md.setdefault("x", []).append(42)
|
|
md.setdefault("x", []).append(23)
|
|
assert md["x"] == [42, 23]
|
|
|
|
# to dict
|
|
md = self.storage_class()
|
|
md["foo"] = 42
|
|
md.add("bar", 1)
|
|
md.add("bar", 2)
|
|
assert md.to_dict() == {"foo": 42, "bar": 1}
|
|
assert md.to_dict(flat=False) == {"foo": [42], "bar": [1, 2]}
|
|
|
|
# popitem from empty dict
|
|
with pytest.raises(KeyError):
|
|
self.storage_class().popitem()
|
|
|
|
with pytest.raises(KeyError):
|
|
self.storage_class().popitemlist()
|
|
|
|
# key errors are of a special type
|
|
with pytest.raises(BadRequestKeyError):
|
|
self.storage_class()[42]
|
|
|
|
# setlist works
|
|
md = self.storage_class()
|
|
md["foo"] = 42
|
|
md.setlist("foo", [1, 2])
|
|
assert md.getlist("foo") == [1, 2]
|
|
|
|
|
|
class _ImmutableDictTests:
|
|
storage_class: t.Type[dict]
|
|
|
|
def test_follows_dict_interface(self):
|
|
cls = self.storage_class
|
|
|
|
data = {"foo": 1, "bar": 2, "baz": 3}
|
|
d = cls(data)
|
|
|
|
assert d["foo"] == 1
|
|
assert d["bar"] == 2
|
|
assert d["baz"] == 3
|
|
assert sorted(d.keys()) == ["bar", "baz", "foo"]
|
|
assert "foo" in d
|
|
assert "foox" not in d
|
|
assert len(d) == 3
|
|
|
|
def test_copies_are_mutable(self):
|
|
cls = self.storage_class
|
|
immutable = cls({"a": 1})
|
|
with pytest.raises(TypeError):
|
|
immutable.pop("a")
|
|
|
|
mutable = immutable.copy()
|
|
mutable.pop("a")
|
|
assert "a" in immutable
|
|
assert mutable is not immutable
|
|
assert copy(immutable) is immutable
|
|
|
|
def test_dict_is_hashable(self):
|
|
cls = self.storage_class
|
|
immutable = cls({"a": 1, "b": 2})
|
|
immutable2 = cls({"a": 2, "b": 2})
|
|
x = {immutable}
|
|
assert immutable in x
|
|
assert immutable2 not in x
|
|
x.discard(immutable)
|
|
assert immutable not in x
|
|
assert immutable2 not in x
|
|
x.add(immutable2)
|
|
assert immutable not in x
|
|
assert immutable2 in x
|
|
x.add(immutable)
|
|
assert immutable in x
|
|
assert immutable2 in x
|
|
|
|
|
|
class TestImmutableTypeConversionDict(_ImmutableDictTests):
|
|
storage_class = ds.ImmutableTypeConversionDict
|
|
|
|
|
|
class TestImmutableMultiDict(_ImmutableDictTests):
|
|
storage_class = ds.ImmutableMultiDict
|
|
|
|
def test_multidict_is_hashable(self):
|
|
cls = self.storage_class
|
|
immutable = cls({"a": [1, 2], "b": 2})
|
|
immutable2 = cls({"a": [1], "b": 2})
|
|
x = {immutable}
|
|
assert immutable in x
|
|
assert immutable2 not in x
|
|
x.discard(immutable)
|
|
assert immutable not in x
|
|
assert immutable2 not in x
|
|
x.add(immutable2)
|
|
assert immutable not in x
|
|
assert immutable2 in x
|
|
x.add(immutable)
|
|
assert immutable in x
|
|
assert immutable2 in x
|
|
|
|
|
|
class TestImmutableDict(_ImmutableDictTests):
|
|
storage_class = ds.ImmutableDict
|
|
|
|
|
|
class TestImmutableOrderedMultiDict(_ImmutableDictTests):
|
|
storage_class = ds.ImmutableOrderedMultiDict
|
|
|
|
def test_ordered_multidict_is_hashable(self):
|
|
a = self.storage_class([("a", 1), ("b", 1), ("a", 2)])
|
|
b = self.storage_class([("a", 1), ("a", 2), ("b", 1)])
|
|
assert hash(a) != hash(b)
|
|
|
|
|
|
class TestMultiDict(_MutableMultiDictTests):
|
|
storage_class = ds.MultiDict
|
|
|
|
def test_multidict_pop(self):
|
|
def make_d():
|
|
return self.storage_class({"foo": [1, 2, 3, 4]})
|
|
|
|
d = make_d()
|
|
assert d.pop("foo") == 1
|
|
assert not d
|
|
d = make_d()
|
|
assert d.pop("foo", 32) == 1
|
|
assert not d
|
|
d = make_d()
|
|
assert d.pop("foos", 32) == 32
|
|
assert d
|
|
|
|
with pytest.raises(KeyError):
|
|
d.pop("foos")
|
|
|
|
def test_multidict_pop_raise_badrequestkeyerror_for_empty_list_value(self):
|
|
mapping = [("a", "b"), ("a", "c")]
|
|
md = self.storage_class(mapping)
|
|
|
|
md.setlistdefault("empty", [])
|
|
|
|
with pytest.raises(KeyError):
|
|
md.pop("empty")
|
|
|
|
def test_multidict_popitem_raise_badrequestkeyerror_for_empty_list_value(self):
|
|
mapping = []
|
|
md = self.storage_class(mapping)
|
|
|
|
md.setlistdefault("empty", [])
|
|
|
|
with pytest.raises(BadRequestKeyError):
|
|
md.popitem()
|
|
|
|
def test_setlistdefault(self):
|
|
md = self.storage_class()
|
|
assert md.setlistdefault("u", [-1, -2]) == [-1, -2]
|
|
assert md.getlist("u") == [-1, -2]
|
|
assert md["u"] == -1
|
|
|
|
def test_iter_interfaces(self):
|
|
mapping = [
|
|
("a", 1),
|
|
("b", 2),
|
|
("a", 2),
|
|
("d", 3),
|
|
("a", 1),
|
|
("a", 3),
|
|
("d", 4),
|
|
("c", 3),
|
|
]
|
|
md = self.storage_class(mapping)
|
|
assert list(zip(md.keys(), md.listvalues())) == list(md.lists())
|
|
assert list(zip(md, md.listvalues())) == list(md.lists())
|
|
assert list(zip(md.keys(), md.listvalues())) == list(md.lists())
|
|
|
|
def test_getitem_raise_badrequestkeyerror_for_empty_list_value(self):
|
|
mapping = [("a", "b"), ("a", "c")]
|
|
md = self.storage_class(mapping)
|
|
|
|
md.setlistdefault("empty", [])
|
|
|
|
with pytest.raises(KeyError):
|
|
md["empty"]
|
|
|
|
|
|
class TestOrderedMultiDict(_MutableMultiDictTests):
|
|
storage_class = ds.OrderedMultiDict
|
|
|
|
def test_ordered_interface(self):
|
|
cls = self.storage_class
|
|
|
|
d = cls()
|
|
assert not d
|
|
d.add("foo", "bar")
|
|
assert len(d) == 1
|
|
d.add("foo", "baz")
|
|
assert len(d) == 1
|
|
assert list(d.items()) == [("foo", "bar")]
|
|
assert list(d) == ["foo"]
|
|
assert list(d.items(multi=True)) == [("foo", "bar"), ("foo", "baz")]
|
|
del d["foo"]
|
|
assert not d
|
|
assert len(d) == 0
|
|
assert list(d) == []
|
|
|
|
d.update([("foo", 1), ("foo", 2), ("bar", 42)])
|
|
d.add("foo", 3)
|
|
assert d.getlist("foo") == [1, 2, 3]
|
|
assert d.getlist("bar") == [42]
|
|
assert list(d.items()) == [("foo", 1), ("bar", 42)]
|
|
|
|
expected = ["foo", "bar"]
|
|
|
|
assert list(d.keys()) == expected
|
|
assert list(d) == expected
|
|
assert list(d.keys()) == expected
|
|
|
|
assert list(d.items(multi=True)) == [
|
|
("foo", 1),
|
|
("foo", 2),
|
|
("bar", 42),
|
|
("foo", 3),
|
|
]
|
|
assert len(d) == 2
|
|
|
|
assert d.pop("foo") == 1
|
|
assert d.pop("blafasel", None) is None
|
|
assert d.pop("blafasel", 42) == 42
|
|
assert len(d) == 1
|
|
assert d.poplist("bar") == [42]
|
|
assert not d
|
|
|
|
assert d.get("missingkey") is None
|
|
|
|
d.add("foo", 42)
|
|
d.add("foo", 23)
|
|
d.add("bar", 2)
|
|
d.add("foo", 42)
|
|
assert d == ds.MultiDict(d)
|
|
id = self.storage_class(d)
|
|
assert d == id
|
|
d.add("foo", 2)
|
|
assert d != id
|
|
|
|
d.update({"blah": [1, 2, 3]})
|
|
assert d["blah"] == 1
|
|
assert d.getlist("blah") == [1, 2, 3]
|
|
|
|
# setlist works
|
|
d = self.storage_class()
|
|
d["foo"] = 42
|
|
d.setlist("foo", [1, 2])
|
|
assert d.getlist("foo") == [1, 2]
|
|
with pytest.raises(BadRequestKeyError):
|
|
d.pop("missing")
|
|
|
|
with pytest.raises(BadRequestKeyError):
|
|
d["missing"]
|
|
|
|
# popping
|
|
d = self.storage_class()
|
|
d.add("foo", 23)
|
|
d.add("foo", 42)
|
|
d.add("foo", 1)
|
|
assert d.popitem() == ("foo", 23)
|
|
with pytest.raises(BadRequestKeyError):
|
|
d.popitem()
|
|
assert not d
|
|
|
|
d.add("foo", 23)
|
|
d.add("foo", 42)
|
|
d.add("foo", 1)
|
|
assert d.popitemlist() == ("foo", [23, 42, 1])
|
|
|
|
with pytest.raises(BadRequestKeyError):
|
|
d.popitemlist()
|
|
|
|
# Unhashable
|
|
d = self.storage_class()
|
|
d.add("foo", 23)
|
|
pytest.raises(TypeError, hash, d)
|
|
|
|
def test_iterables(self):
|
|
a = ds.MultiDict((("key_a", "value_a"),))
|
|
b = ds.MultiDict((("key_b", "value_b"),))
|
|
ab = ds.CombinedMultiDict((a, b))
|
|
|
|
assert sorted(ab.lists()) == [("key_a", ["value_a"]), ("key_b", ["value_b"])]
|
|
assert sorted(ab.listvalues()) == [["value_a"], ["value_b"]]
|
|
assert sorted(ab.keys()) == ["key_a", "key_b"]
|
|
|
|
assert sorted(ab.lists()) == [("key_a", ["value_a"]), ("key_b", ["value_b"])]
|
|
assert sorted(ab.listvalues()) == [["value_a"], ["value_b"]]
|
|
assert sorted(ab.keys()) == ["key_a", "key_b"]
|
|
|
|
def test_get_description(self):
|
|
data = ds.OrderedMultiDict()
|
|
|
|
with pytest.raises(BadRequestKeyError) as exc_info:
|
|
data["baz"]
|
|
|
|
assert "baz" not in exc_info.value.get_description()
|
|
exc_info.value.show_exception = True
|
|
assert "baz" in exc_info.value.get_description()
|
|
|
|
with pytest.raises(BadRequestKeyError) as exc_info:
|
|
data.pop("baz")
|
|
|
|
exc_info.value.show_exception = True
|
|
assert "baz" in exc_info.value.get_description()
|
|
exc_info.value.args = ()
|
|
assert "baz" not in exc_info.value.get_description()
|
|
|
|
|
|
class TestTypeConversionDict:
|
|
storage_class = ds.TypeConversionDict
|
|
|
|
def test_value_conversion(self):
|
|
d = self.storage_class(foo="1")
|
|
assert d.get("foo", type=int) == 1
|
|
|
|
def test_return_default_when_conversion_is_not_possible(self):
|
|
d = self.storage_class(foo="bar")
|
|
assert d.get("foo", default=-1, type=int) == -1
|
|
|
|
def test_propagate_exceptions_in_conversion(self):
|
|
d = self.storage_class(foo="bar")
|
|
switch = {"a": 1}
|
|
with pytest.raises(KeyError):
|
|
d.get("foo", type=lambda x: switch[x])
|
|
|
|
|
|
class TestCombinedMultiDict:
|
|
storage_class = ds.CombinedMultiDict
|
|
|
|
def test_basic_interface(self):
|
|
d1 = ds.MultiDict([("foo", "1")])
|
|
d2 = ds.MultiDict([("bar", "2"), ("bar", "3")])
|
|
d = self.storage_class([d1, d2])
|
|
|
|
# lookup
|
|
assert d["foo"] == "1"
|
|
assert d["bar"] == "2"
|
|
assert d.getlist("bar") == ["2", "3"]
|
|
|
|
assert sorted(d.items()) == [("bar", "2"), ("foo", "1")]
|
|
assert sorted(d.items(multi=True)) == [("bar", "2"), ("bar", "3"), ("foo", "1")]
|
|
assert "missingkey" not in d
|
|
assert "foo" in d
|
|
|
|
# type lookup
|
|
assert d.get("foo", type=int) == 1
|
|
assert d.getlist("bar", type=int) == [2, 3]
|
|
|
|
# get key errors for missing stuff
|
|
with pytest.raises(KeyError):
|
|
d["missing"]
|
|
|
|
# make sure that they are immutable
|
|
with pytest.raises(TypeError):
|
|
d["foo"] = "blub"
|
|
|
|
# copies are mutable
|
|
d = d.copy()
|
|
d["foo"] = "blub"
|
|
|
|
# make sure lists merges
|
|
md1 = ds.MultiDict((("foo", "bar"), ("foo", "baz")))
|
|
md2 = ds.MultiDict((("foo", "blafasel"),))
|
|
x = self.storage_class((md1, md2))
|
|
assert list(x.lists()) == [("foo", ["bar", "baz", "blafasel"])]
|
|
|
|
# make sure dicts are created properly
|
|
assert x.to_dict() == {"foo": "bar"}
|
|
assert x.to_dict(flat=False) == {"foo": ["bar", "baz", "blafasel"]}
|
|
|
|
def test_length(self):
|
|
d1 = ds.MultiDict([("foo", "1")])
|
|
d2 = ds.MultiDict([("bar", "2")])
|
|
assert len(d1) == len(d2) == 1
|
|
d = self.storage_class([d1, d2])
|
|
assert len(d) == 2
|
|
d1.clear()
|
|
assert len(d1) == 0
|
|
assert len(d) == 1
|
|
|
|
|
|
class TestHeaders:
|
|
storage_class = ds.Headers
|
|
|
|
def test_basic_interface(self):
|
|
headers = self.storage_class()
|
|
headers.add("Content-Type", "text/plain")
|
|
headers.add("X-Foo", "bar")
|
|
assert "x-Foo" in headers
|
|
assert "Content-type" in headers
|
|
|
|
with pytest.raises(ValueError):
|
|
headers.add("X-Example", "foo\r\n bar")
|
|
|
|
headers["Content-Type"] = "foo/bar"
|
|
assert headers["Content-Type"] == "foo/bar"
|
|
assert len(headers.getlist("Content-Type")) == 1
|
|
|
|
# list conversion
|
|
assert headers.to_wsgi_list() == [("Content-Type", "foo/bar"), ("X-Foo", "bar")]
|
|
assert str(headers) == "Content-Type: foo/bar\r\nX-Foo: bar\r\n\r\n"
|
|
assert str(self.storage_class()) == "\r\n"
|
|
|
|
# extended add
|
|
headers.add("Content-Disposition", "attachment", filename="foo")
|
|
assert headers["Content-Disposition"] == "attachment; filename=foo"
|
|
|
|
headers.add("x", "y", z='"')
|
|
assert headers["x"] == r'y; z="\""'
|
|
|
|
# string conversion
|
|
headers.add("a", 1)
|
|
assert headers["a"] == "1"
|
|
|
|
def test_defaults_and_conversion(self):
|
|
# defaults
|
|
headers = self.storage_class(
|
|
[
|
|
("Content-Type", "text/plain"),
|
|
("X-Foo", "bar"),
|
|
("X-Bar", "1"),
|
|
("X-Bar", "2"),
|
|
]
|
|
)
|
|
assert headers.getlist("x-bar") == ["1", "2"]
|
|
assert headers.get("x-Bar") == "1"
|
|
assert headers.get("Content-Type") == "text/plain"
|
|
|
|
assert headers.setdefault("X-Foo", "nope") == "bar"
|
|
assert headers.setdefault("X-Bar", "nope") == "1"
|
|
assert headers.setdefault("X-Baz", "quux") == "quux"
|
|
assert headers.setdefault("X-Baz", "nope") == "quux"
|
|
headers.pop("X-Baz")
|
|
|
|
# newlines are not allowed in values
|
|
with pytest.raises(ValueError):
|
|
self.storage_class([("X-Example", "foo\r\n bar")])
|
|
|
|
# type conversion
|
|
assert headers.get("x-bar", type=int) == 1
|
|
assert headers.getlist("x-bar", type=int) == [1, 2]
|
|
|
|
# list like operations
|
|
assert headers[0] == ("Content-Type", "text/plain")
|
|
assert headers[:1] == self.storage_class([("Content-Type", "text/plain")])
|
|
del headers[:2]
|
|
del headers[-1]
|
|
assert headers == self.storage_class([("X-Bar", "1")])
|
|
|
|
def test_copying(self):
|
|
a = self.storage_class([("foo", "bar")])
|
|
b = a.copy()
|
|
a.add("foo", "baz")
|
|
assert a.getlist("foo") == ["bar", "baz"]
|
|
assert b.getlist("foo") == ["bar"]
|
|
|
|
def test_popping(self):
|
|
headers = self.storage_class([("a", 1)])
|
|
# headers object expect string values. If a non string value
|
|
# is passed, it tries converting it to a string
|
|
assert headers.pop("a") == "1"
|
|
assert headers.pop("b", "2") == "2"
|
|
|
|
with pytest.raises(KeyError):
|
|
headers.pop("c")
|
|
|
|
def test_set_arguments(self):
|
|
a = self.storage_class()
|
|
a.set("Content-Disposition", "useless")
|
|
a.set("Content-Disposition", "attachment", filename="foo")
|
|
assert a["Content-Disposition"] == "attachment; filename=foo"
|
|
|
|
def test_reject_newlines(self):
|
|
h = self.storage_class()
|
|
|
|
for variation in "foo\nbar", "foo\r\nbar", "foo\rbar":
|
|
with pytest.raises(ValueError):
|
|
h["foo"] = variation
|
|
with pytest.raises(ValueError):
|
|
h.add("foo", variation)
|
|
with pytest.raises(ValueError):
|
|
h.add("foo", "test", option=variation)
|
|
with pytest.raises(ValueError):
|
|
h.set("foo", variation)
|
|
with pytest.raises(ValueError):
|
|
h.set("foo", "test", option=variation)
|
|
|
|
def test_slicing(self):
|
|
# there's nothing wrong with these being native strings
|
|
# Headers doesn't care about the data types
|
|
h = self.storage_class()
|
|
h.set("X-Foo-Poo", "bleh")
|
|
h.set("Content-Type", "application/whocares")
|
|
h.set("X-Forwarded-For", "192.168.0.123")
|
|
h[:] = [(k, v) for k, v in h if k.startswith("X-")]
|
|
assert list(h) == [("X-Foo-Poo", "bleh"), ("X-Forwarded-For", "192.168.0.123")]
|
|
|
|
def test_extend(self):
|
|
h = self.storage_class([("a", "0"), ("b", "1"), ("c", "2")])
|
|
h.extend(ds.Headers([("a", "3"), ("a", "4")]))
|
|
assert h.getlist("a") == ["0", "3", "4"]
|
|
h.extend(b=["5", "6"])
|
|
assert h.getlist("b") == ["1", "5", "6"]
|
|
h.extend({"c": "7", "d": ["8", "9"]}, c="10")
|
|
assert h.getlist("c") == ["2", "7", "10"]
|
|
assert h.getlist("d") == ["8", "9"]
|
|
|
|
with pytest.raises(TypeError):
|
|
h.extend({"x": "x"}, {"x": "x"})
|
|
|
|
def test_update(self):
|
|
h = self.storage_class([("a", "0"), ("b", "1"), ("c", "2")])
|
|
h.update(ds.Headers([("a", "3"), ("a", "4")]))
|
|
assert h.getlist("a") == ["3", "4"]
|
|
h.update(b=["5", "6"])
|
|
assert h.getlist("b") == ["5", "6"]
|
|
h.update({"c": "7", "d": ["8", "9"]})
|
|
assert h.getlist("c") == ["7"]
|
|
assert h.getlist("d") == ["8", "9"]
|
|
h.update({"c": "10"}, c="11")
|
|
assert h.getlist("c") == ["11"]
|
|
|
|
with pytest.raises(TypeError):
|
|
h.extend({"x": "x"}, {"x": "x"})
|
|
|
|
def test_setlist(self):
|
|
h = self.storage_class([("a", "0"), ("b", "1"), ("c", "2")])
|
|
h.setlist("b", ["3", "4"])
|
|
assert h[1] == ("b", "3")
|
|
assert h[-1] == ("b", "4")
|
|
h.setlist("b", [])
|
|
assert "b" not in h
|
|
h.setlist("d", ["5"])
|
|
assert h["d"] == "5"
|
|
|
|
def test_setlistdefault(self):
|
|
h = self.storage_class([("a", "0"), ("b", "1"), ("c", "2")])
|
|
assert h.setlistdefault("a", ["3"]) == ["0"]
|
|
assert h.setlistdefault("d", ["4", "5"]) == ["4", "5"]
|
|
|
|
def test_to_wsgi_list(self):
|
|
h = self.storage_class()
|
|
h.set("Key", "Value")
|
|
for key, value in h.to_wsgi_list():
|
|
assert key == "Key"
|
|
assert value == "Value"
|
|
|
|
def test_equality(self):
|
|
# test equality, given keys are case insensitive
|
|
h1 = self.storage_class()
|
|
h1.add("X-Foo", "foo")
|
|
h1.add("X-Bar", "bah")
|
|
h1.add("X-Bar", "humbug")
|
|
|
|
h2 = self.storage_class()
|
|
h2.add("x-foo", "foo")
|
|
h2.add("x-bar", "bah")
|
|
h2.add("x-bar", "humbug")
|
|
|
|
assert h1 == h2
|
|
|
|
|
|
class TestEnvironHeaders:
|
|
storage_class = ds.EnvironHeaders
|
|
|
|
def test_basic_interface(self):
|
|
# this happens in multiple WSGI servers because they
|
|
# use a vary naive way to convert the headers;
|
|
broken_env = {
|
|
"HTTP_CONTENT_TYPE": "text/html",
|
|
"CONTENT_TYPE": "text/html",
|
|
"HTTP_CONTENT_LENGTH": "0",
|
|
"CONTENT_LENGTH": "0",
|
|
"HTTP_ACCEPT": "*",
|
|
"wsgi.version": (1, 0),
|
|
}
|
|
headers = self.storage_class(broken_env)
|
|
assert headers
|
|
assert len(headers) == 3
|
|
assert sorted(headers) == [
|
|
("Accept", "*"),
|
|
("Content-Length", "0"),
|
|
("Content-Type", "text/html"),
|
|
]
|
|
assert not self.storage_class({"wsgi.version": (1, 0)})
|
|
assert len(self.storage_class({"wsgi.version": (1, 0)})) == 0
|
|
assert 42 not in headers
|
|
|
|
def test_skip_empty_special_vars(self):
|
|
env = {"HTTP_X_FOO": "42", "CONTENT_TYPE": "", "CONTENT_LENGTH": ""}
|
|
headers = self.storage_class(env)
|
|
assert dict(headers) == {"X-Foo": "42"}
|
|
|
|
env = {"HTTP_X_FOO": "42", "CONTENT_TYPE": "", "CONTENT_LENGTH": "0"}
|
|
headers = self.storage_class(env)
|
|
assert dict(headers) == {"X-Foo": "42", "Content-Length": "0"}
|
|
|
|
def test_return_type_is_str(self):
|
|
headers = self.storage_class({"HTTP_FOO": "\xe2\x9c\x93"})
|
|
assert headers["Foo"] == "\xe2\x9c\x93"
|
|
assert next(iter(headers)) == ("Foo", "\xe2\x9c\x93")
|
|
|
|
|
|
class TestHeaderSet:
|
|
storage_class = ds.HeaderSet
|
|
|
|
def test_basic_interface(self):
|
|
hs = self.storage_class()
|
|
hs.add("foo")
|
|
hs.add("bar")
|
|
assert "Bar" in hs
|
|
assert hs.find("foo") == 0
|
|
assert hs.find("BAR") == 1
|
|
assert hs.find("baz") < 0
|
|
hs.discard("missing")
|
|
hs.discard("foo")
|
|
assert hs.find("foo") < 0
|
|
assert hs.find("bar") == 0
|
|
|
|
with pytest.raises(IndexError):
|
|
hs.index("missing")
|
|
|
|
assert hs.index("bar") == 0
|
|
assert hs
|
|
hs.clear()
|
|
assert not hs
|
|
|
|
|
|
class TestImmutableList:
|
|
storage_class = ds.ImmutableList
|
|
|
|
def test_list_hashable(self):
|
|
data = (1, 2, 3, 4)
|
|
store = self.storage_class(data)
|
|
assert hash(data) == hash(store)
|
|
assert data != store
|
|
|
|
|
|
def make_call_asserter(func=None):
|
|
"""Utility to assert a certain number of function calls.
|
|
|
|
:param func: Additional callback for each function call.
|
|
|
|
.. code-block:: python
|
|
assert_calls, func = make_call_asserter()
|
|
with assert_calls(2):
|
|
func()
|
|
func()
|
|
"""
|
|
calls = [0]
|
|
|
|
@contextmanager
|
|
def asserter(count, msg=None):
|
|
calls[0] = 0
|
|
yield
|
|
assert calls[0] == count
|
|
|
|
def wrapped(*args, **kwargs):
|
|
calls[0] += 1
|
|
if func is not None:
|
|
return func(*args, **kwargs)
|
|
|
|
return asserter, wrapped
|
|
|
|
|
|
class TestCallbackDict:
|
|
storage_class = ds.CallbackDict
|
|
|
|
def test_callback_dict_reads(self):
|
|
assert_calls, func = make_call_asserter()
|
|
initial = {"a": "foo", "b": "bar"}
|
|
dct = self.storage_class(initial=initial, on_update=func)
|
|
with assert_calls(0, "callback triggered by read-only method"):
|
|
# read-only methods
|
|
dct["a"]
|
|
dct.get("a")
|
|
pytest.raises(KeyError, lambda: dct["x"])
|
|
assert "a" in dct
|
|
list(iter(dct))
|
|
dct.copy()
|
|
with assert_calls(0, "callback triggered without modification"):
|
|
# methods that may write but don't
|
|
dct.pop("z", None)
|
|
dct.setdefault("a")
|
|
|
|
def test_callback_dict_writes(self):
|
|
assert_calls, func = make_call_asserter()
|
|
initial = {"a": "foo", "b": "bar"}
|
|
dct = self.storage_class(initial=initial, on_update=func)
|
|
with assert_calls(8, "callback not triggered by write method"):
|
|
# always-write methods
|
|
dct["z"] = 123
|
|
dct["z"] = 123 # must trigger again
|
|
del dct["z"]
|
|
dct.pop("b", None)
|
|
dct.setdefault("x")
|
|
dct.popitem()
|
|
dct.update([])
|
|
dct.clear()
|
|
with assert_calls(0, "callback triggered by failed del"):
|
|
pytest.raises(KeyError, lambda: dct.__delitem__("x"))
|
|
with assert_calls(0, "callback triggered by failed pop"):
|
|
pytest.raises(KeyError, lambda: dct.pop("x"))
|
|
|
|
|
|
class TestCacheControl:
|
|
def test_repr(self):
|
|
cc = ds.RequestCacheControl([("max-age", "0"), ("private", "True")])
|
|
assert repr(cc) == "<RequestCacheControl max-age='0' private='True'>"
|
|
|
|
def test_set_none(self):
|
|
cc = ds.ResponseCacheControl([("max-age", "0")])
|
|
assert cc.no_cache is None
|
|
cc.no_cache = None
|
|
assert cc.no_cache is None
|
|
cc.no_cache = False
|
|
assert cc.no_cache is False
|
|
|
|
|
|
class TestContentSecurityPolicy:
|
|
def test_construct(self):
|
|
csp = ds.ContentSecurityPolicy([("font-src", "'self'"), ("media-src", "*")])
|
|
assert csp.font_src == "'self'"
|
|
assert csp.media_src == "*"
|
|
policies = [policy.strip() for policy in csp.to_header().split(";")]
|
|
assert "font-src 'self'" in policies
|
|
assert "media-src *" in policies
|
|
|
|
def test_properties(self):
|
|
csp = ds.ContentSecurityPolicy()
|
|
csp.default_src = "* 'self' quart.com"
|
|
csp.img_src = "'none'"
|
|
policies = [policy.strip() for policy in csp.to_header().split(";")]
|
|
assert "default-src * 'self' quart.com" in policies
|
|
assert "img-src 'none'" in policies
|
|
|
|
|
|
class TestAccept:
|
|
storage_class = ds.Accept
|
|
|
|
def test_accept_basic(self):
|
|
accept = self.storage_class(
|
|
[("tinker", 0), ("tailor", 0.333), ("soldier", 0.667), ("sailor", 1)]
|
|
)
|
|
# check __getitem__ on indices
|
|
assert accept[3] == ("tinker", 0)
|
|
assert accept[2] == ("tailor", 0.333)
|
|
assert accept[1] == ("soldier", 0.667)
|
|
assert accept[0], ("sailor", 1)
|
|
# check __getitem__ on string
|
|
assert accept["tinker"] == 0
|
|
assert accept["tailor"] == 0.333
|
|
assert accept["soldier"] == 0.667
|
|
assert accept["sailor"] == 1
|
|
assert accept["spy"] == 0
|
|
# check quality method
|
|
assert accept.quality("tinker") == 0
|
|
assert accept.quality("tailor") == 0.333
|
|
assert accept.quality("soldier") == 0.667
|
|
assert accept.quality("sailor") == 1
|
|
assert accept.quality("spy") == 0
|
|
# check __contains__
|
|
assert "sailor" in accept
|
|
assert "spy" not in accept
|
|
# check index method
|
|
assert accept.index("tinker") == 3
|
|
assert accept.index("tailor") == 2
|
|
assert accept.index("soldier") == 1
|
|
assert accept.index("sailor") == 0
|
|
with pytest.raises(ValueError):
|
|
accept.index("spy")
|
|
# check find method
|
|
assert accept.find("tinker") == 3
|
|
assert accept.find("tailor") == 2
|
|
assert accept.find("soldier") == 1
|
|
assert accept.find("sailor") == 0
|
|
assert accept.find("spy") == -1
|
|
# check to_header method
|
|
assert accept.to_header() == "sailor,soldier;q=0.667,tailor;q=0.333,tinker;q=0"
|
|
# check best_match method
|
|
assert (
|
|
accept.best_match(["tinker", "tailor", "soldier", "sailor"], default=None)
|
|
== "sailor"
|
|
)
|
|
assert (
|
|
accept.best_match(["tinker", "tailor", "soldier"], default=None)
|
|
== "soldier"
|
|
)
|
|
assert accept.best_match(["tinker", "tailor"], default=None) == "tailor"
|
|
assert accept.best_match(["tinker"], default=None) is None
|
|
assert accept.best_match(["tinker"], default="x") == "x"
|
|
|
|
def test_accept_wildcard(self):
|
|
accept = self.storage_class([("*", 0), ("asterisk", 1)])
|
|
assert "*" in accept
|
|
assert accept.best_match(["asterisk", "star"], default=None) == "asterisk"
|
|
assert accept.best_match(["star"], default=None) is None
|
|
|
|
def test_accept_keep_order(self):
|
|
accept = self.storage_class([("*", 1)])
|
|
assert accept.best_match(["alice", "bob"]) == "alice"
|
|
assert accept.best_match(["bob", "alice"]) == "bob"
|
|
accept = self.storage_class([("alice", 1), ("bob", 1)])
|
|
assert accept.best_match(["alice", "bob"]) == "alice"
|
|
assert accept.best_match(["bob", "alice"]) == "bob"
|
|
|
|
def test_accept_wildcard_specificity(self):
|
|
accept = self.storage_class([("asterisk", 0), ("star", 0.5), ("*", 1)])
|
|
assert accept.best_match(["star", "asterisk"], default=None) == "star"
|
|
assert accept.best_match(["asterisk", "star"], default=None) == "star"
|
|
assert accept.best_match(["asterisk", "times"], default=None) == "times"
|
|
assert accept.best_match(["asterisk"], default=None) is None
|
|
|
|
def test_accept_equal_quality(self):
|
|
accept = self.storage_class([("a", 1), ("b", 1)])
|
|
assert accept.best == "a"
|
|
|
|
|
|
class TestMIMEAccept:
|
|
@pytest.mark.parametrize(
|
|
("values", "matches", "default", "expect"),
|
|
[
|
|
([("text/*", 1)], ["text/html"], None, "text/html"),
|
|
([("text/*", 1)], ["image/png"], "text/plain", "text/plain"),
|
|
([("text/*", 1)], ["image/png"], None, None),
|
|
(
|
|
[("*/*", 1), ("text/html", 1)],
|
|
["image/png", "text/html"],
|
|
None,
|
|
"text/html",
|
|
),
|
|
(
|
|
[("*/*", 1), ("text/html", 1)],
|
|
["image/png", "text/plain"],
|
|
None,
|
|
"image/png",
|
|
),
|
|
(
|
|
[("*/*", 1), ("text/html", 1), ("image/*", 1)],
|
|
["image/png", "text/html"],
|
|
None,
|
|
"text/html",
|
|
),
|
|
(
|
|
[("*/*", 1), ("text/html", 1), ("image/*", 1)],
|
|
["text/plain", "image/png"],
|
|
None,
|
|
"image/png",
|
|
),
|
|
(
|
|
[("text/html", 1), ("text/html; level=1", 1)],
|
|
["text/html;level=1"],
|
|
None,
|
|
"text/html;level=1",
|
|
),
|
|
],
|
|
)
|
|
def test_mime_accept(self, values, matches, default, expect):
|
|
accept = ds.MIMEAccept(values)
|
|
match = accept.best_match(matches, default=default)
|
|
assert match == expect
|
|
|
|
|
|
class TestLanguageAccept:
|
|
@pytest.mark.parametrize(
|
|
("values", "matches", "default", "expect"),
|
|
(
|
|
([("en-us", 1)], ["en"], None, "en"),
|
|
([("en", 1)], ["en_US"], None, "en_US"),
|
|
([("en-GB", 1)], ["en-US"], None, None),
|
|
([("de_AT", 1), ("de", 0.9)], ["en"], None, None),
|
|
([("de_AT", 1), ("de", 0.9), ("en-US", 0.8)], ["de", "en"], None, "de"),
|
|
([("de_AT", 0.9), ("en-US", 1)], ["en"], None, "en"),
|
|
([("en-us", 1)], ["en-us"], None, "en-us"),
|
|
([("en-us", 1)], ["en-us", "en"], None, "en-us"),
|
|
([("en-GB", 1)], ["en-US", "en"], "en-US", "en"),
|
|
([("de_AT", 1)], ["en-US", "en"], "en-US", "en-US"),
|
|
([("aus-EN", 1)], ["aus"], None, "aus"),
|
|
([("aus", 1)], ["aus-EN"], None, "aus-EN"),
|
|
),
|
|
)
|
|
def test_best_match_fallback(self, values, matches, default, expect):
|
|
accept = ds.LanguageAccept(values)
|
|
best = accept.best_match(matches, default=default)
|
|
assert best == expect
|
|
|
|
|
|
class TestFileStorage:
|
|
storage_class = ds.FileStorage
|
|
|
|
def test_mimetype_always_lowercase(self):
|
|
file_storage = self.storage_class(content_type="APPLICATION/JSON")
|
|
assert file_storage.mimetype == "application/json"
|
|
|
|
@pytest.mark.parametrize("data", [io.StringIO("one\ntwo"), io.BytesIO(b"one\ntwo")])
|
|
def test_bytes_proper_sentinel(self, data):
|
|
# iterate over new lines and don't enter an infinite loop
|
|
storage = self.storage_class(data)
|
|
idx = -1
|
|
|
|
for idx, _line in enumerate(storage):
|
|
assert idx < 2
|
|
|
|
assert idx == 1
|
|
|
|
@pytest.mark.parametrize("stream", (tempfile.SpooledTemporaryFile, io.BytesIO))
|
|
def test_proxy_can_access_stream_attrs(self, stream):
|
|
"""``SpooledTemporaryFile`` doesn't implement some of
|
|
``IOBase``. Ensure that ``FileStorage`` can still access the
|
|
attributes from the backing file object.
|
|
|
|
https://github.com/pallets/werkzeug/issues/1344
|
|
https://github.com/python/cpython/pull/3249
|
|
"""
|
|
file_storage = self.storage_class(stream=stream())
|
|
|
|
for name in ("fileno", "writable", "readable", "seekable"):
|
|
assert hasattr(file_storage, name)
|
|
|
|
file_storage.close()
|
|
|
|
def test_save_to_pathlib_dst(self, tmp_path):
|
|
src = tmp_path / "src.txt"
|
|
src.write_text("test")
|
|
dst = tmp_path / "dst.txt"
|
|
|
|
with src.open("rb") as f:
|
|
storage = self.storage_class(f)
|
|
storage.save(dst)
|
|
|
|
assert dst.read_text() == "test"
|
|
|
|
def test_save_to_bytes_io(self):
|
|
storage = self.storage_class(io.BytesIO(b"one\ntwo"))
|
|
dst = io.BytesIO()
|
|
storage.save(dst)
|
|
assert dst.getvalue() == b"one\ntwo"
|
|
|
|
def test_save_to_file(self, tmp_path):
|
|
path = tmp_path / "file.data"
|
|
storage = self.storage_class(io.BytesIO(b"one\ntwo"))
|
|
with path.open("wb") as dst:
|
|
storage.save(dst)
|
|
with path.open("rb") as src:
|
|
assert src.read() == b"one\ntwo"
|
|
|
|
|
|
@pytest.mark.parametrize("ranges", ([(0, 1), (-5, None)], [(5, None)]))
|
|
def test_range_to_header(ranges):
|
|
header = ds.Range("byes", ranges).to_header()
|
|
r = http.parse_range_header(header)
|
|
assert r.ranges == ranges
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ranges", ([(0, 0)], [(None, 1)], [(1, 0)], [(0, 1), (-5, 10)])
|
|
)
|
|
def test_range_validates_ranges(ranges):
|
|
with pytest.raises(ValueError):
|
|
ds.Range("bytes", ranges)
|