1a63b9bec0
* Add convenience methods for cookie creation and deletion * Restore del * Backwards compat, forward thinking * Add delitem deprecation notice * Add get full deprecation notice * Add deprecation docstring * Better deprecation docstring * Add has_cookie * Same defaults * Better deprecation message * Accessor annotations * make pretty * parse cookies * Revert quote translator * make pretty * make pretty * Add unit tests * Make pretty * Fix unit tests * Directly include unquote * Add some more unit tests * Move modules into their own dir * make pretty * cleanup test imports * Add test for cookie accessor * Make test consistent * Remove file * Remove additional escaping * Add header style getattr for hyphens * Add test for cookie accessor with hyphens * Add new translator * Parametrize test_request_with_duplicate_cookie_key * make pretty * Add deprecation of direct cookie encoding * Speedup Cookie creation * Implement prefixes on delete_cookie * typing changes * Add passthru functions on response objects for setting cookies * Add test for passthru --------- Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com>
521 lines
16 KiB
Python
521 lines
16 KiB
Python
from datetime import datetime, timedelta
|
|
from http.cookies import SimpleCookie
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from sanic import Request, Sanic
|
|
from sanic.compat import Header
|
|
from sanic.cookies import Cookie, CookieJar
|
|
from sanic.cookies.request import CookieRequestParameters
|
|
from sanic.exceptions import ServerError
|
|
from sanic.response import text
|
|
from sanic.response.convenience import json
|
|
|
|
|
|
# ------------------------------------------------------------ #
|
|
# GET
|
|
# ------------------------------------------------------------ #
|
|
|
|
|
|
def test_cookies(app):
|
|
@app.route("/")
|
|
def handler(request):
|
|
cookie_value = request.cookies["test"]
|
|
response = text(f"Cookies are: {cookie_value}")
|
|
response.cookies["right_back"] = "at you"
|
|
return response
|
|
|
|
request, response = app.test_client.get("/", cookies={"test": "working!"})
|
|
response_cookies = SimpleCookie()
|
|
response_cookies.load(response.headers.get("Set-Cookie", {}))
|
|
|
|
assert response.text == "Cookies are: working!"
|
|
assert response_cookies["right_back"].value == "at you"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cookies_asgi(app):
|
|
@app.route("/")
|
|
def handler(request):
|
|
cookie_value = request.cookies["test"]
|
|
response = text(f"Cookies are: {cookie_value}")
|
|
response.cookies["right_back"] = "at you"
|
|
return response
|
|
|
|
request, response = await app.asgi_client.get(
|
|
"/", cookies={"test": "working!"}
|
|
)
|
|
response_cookies = SimpleCookie()
|
|
response_cookies.load(response.headers.get("set-cookie", {}))
|
|
|
|
assert response.body == b"Cookies are: working!"
|
|
assert response_cookies["right_back"].value == "at you"
|
|
|
|
|
|
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
|
|
def test_false_cookies_encoded(app, httponly, expected):
|
|
@app.route("/")
|
|
def handler(request):
|
|
response = text("hello cookies")
|
|
response.cookies["hello"] = "world"
|
|
response.cookies["hello"]["httponly"] = httponly
|
|
return text(response.cookies["hello"].encode("utf8").decode())
|
|
|
|
request, response = app.test_client.get("/")
|
|
|
|
assert ("HttpOnly" in response.text) == expected
|
|
|
|
|
|
@pytest.mark.parametrize("httponly,expected", [(False, False), (True, True)])
|
|
def test_false_cookies(app, httponly, expected):
|
|
@app.route("/")
|
|
def handler(request):
|
|
response = text("hello cookies")
|
|
response.cookies["right_back"] = "at you"
|
|
response.cookies["right_back"]["httponly"] = httponly
|
|
return response
|
|
|
|
request, response = app.test_client.get("/")
|
|
response_cookies = SimpleCookie()
|
|
response_cookies.load(response.headers.get("Set-Cookie", {}))
|
|
|
|
assert ("HttpOnly" in response_cookies["right_back"].output()) == expected
|
|
|
|
|
|
def test_http2_cookies(app):
|
|
@app.route("/")
|
|
async def handler(request):
|
|
cookie_value = request.cookies["test"]
|
|
response = text(f"Cookies are: {cookie_value}")
|
|
return response
|
|
|
|
headers = {"cookie": "test=working!"}
|
|
request, response = app.test_client.get("/", headers=headers)
|
|
|
|
assert response.text == "Cookies are: working!"
|
|
|
|
|
|
def test_cookie_options(app):
|
|
@app.route("/")
|
|
def handler(request):
|
|
response = text("OK")
|
|
response.cookies["test"] = "at you"
|
|
response.cookies["test"]["httponly"] = True
|
|
response.cookies["test"]["expires"] = datetime.now() + timedelta(
|
|
seconds=10
|
|
)
|
|
return response
|
|
|
|
request, response = app.test_client.get("/")
|
|
response_cookies = SimpleCookie()
|
|
response_cookies.load(response.headers.get("Set-Cookie", {}))
|
|
|
|
assert response_cookies["test"].value == "at you"
|
|
assert response_cookies["test"]["httponly"] is True
|
|
|
|
|
|
def test_cookie_deletion(app):
|
|
cookie_jar = None
|
|
|
|
@app.route("/")
|
|
def handler(request):
|
|
nonlocal cookie_jar
|
|
response = text("OK")
|
|
del response.cookies["one"]
|
|
response.cookies["two"] = "testing"
|
|
del response.cookies["two"]
|
|
cookie_jar = response.cookies
|
|
return response
|
|
|
|
_, response = app.test_client.get("/")
|
|
|
|
assert cookie_jar.get_cookie("one").max_age == 0
|
|
assert cookie_jar.get_cookie("two").max_age == 0
|
|
assert len(response.cookies) == 0
|
|
|
|
|
|
def test_cookie_reserved_cookie():
|
|
with pytest.raises(expected_exception=KeyError) as e:
|
|
Cookie("domain", "testdomain.com")
|
|
assert e.message == "Cookie name is a reserved word"
|
|
|
|
|
|
def test_cookie_illegal_key_format():
|
|
with pytest.raises(expected_exception=KeyError) as e:
|
|
Cookie("testå", "test")
|
|
assert e.message == "Cookie key contains illegal characters"
|
|
|
|
|
|
def test_cookie_set_unknown_property():
|
|
c = Cookie("test_cookie", "value")
|
|
with pytest.raises(expected_exception=KeyError) as e:
|
|
c["invalid"] = "value"
|
|
assert e.message == "Unknown cookie property"
|
|
|
|
|
|
def test_cookie_set_same_key(app):
|
|
cookies = {"test": "wait"}
|
|
|
|
@app.get("/")
|
|
def handler(request):
|
|
response = text("pass")
|
|
response.cookies["test"] = "modified"
|
|
response.cookies["test"] = "pass"
|
|
return response
|
|
|
|
request, response = app.test_client.get("/", cookies=cookies)
|
|
assert response.status == 200
|
|
assert response.cookies["test"] == "pass"
|
|
|
|
|
|
@pytest.mark.parametrize("max_age", ["0", 30, "30"])
|
|
def test_cookie_max_age(app, max_age):
|
|
cookies = {"test": "wait"}
|
|
|
|
@app.get("/")
|
|
def handler(request):
|
|
response = text("pass")
|
|
response.cookies["test"] = "pass"
|
|
response.cookies["test"]["max-age"] = max_age
|
|
return response
|
|
|
|
request, response = app.test_client.get(
|
|
"/", cookies=cookies, raw_cookies=True
|
|
)
|
|
assert response.status == 200
|
|
|
|
cookie = response.cookies.get("test")
|
|
if (
|
|
str(max_age).isdigit()
|
|
and int(max_age) == float(max_age)
|
|
and int(max_age) != 0
|
|
):
|
|
cookie_expires = datetime.utcfromtimestamp(
|
|
response.raw_cookies["test"].expires
|
|
).replace(microsecond=0)
|
|
|
|
# Grabbing utcnow after the response may lead to it being off slightly.
|
|
# Therefore, we 0 out the microseconds, and accept the test if there
|
|
# is a 1 second difference.
|
|
expires = datetime.utcnow().replace(microsecond=0) + timedelta(
|
|
seconds=int(max_age)
|
|
)
|
|
|
|
assert cookie == "pass"
|
|
assert (
|
|
cookie_expires == expires
|
|
or cookie_expires == expires + timedelta(seconds=-1)
|
|
)
|
|
else:
|
|
assert cookie is None
|
|
|
|
|
|
@pytest.mark.parametrize("max_age", [30.0, 30.1, "test"])
|
|
def test_cookie_bad_max_age(app, max_age):
|
|
cookies = {"test": "wait"}
|
|
|
|
@app.get("/")
|
|
def handler(request):
|
|
response = text("pass")
|
|
response.cookies["test"] = "pass"
|
|
response.cookies["test"]["max-age"] = max_age
|
|
return response
|
|
|
|
request, response = app.test_client.get(
|
|
"/", cookies=cookies, raw_cookies=True
|
|
)
|
|
assert response.status == 500
|
|
|
|
|
|
@pytest.mark.parametrize("expires", [timedelta(seconds=60)])
|
|
def test_cookie_expires(app: Sanic, expires: timedelta):
|
|
expires_time = datetime.utcnow().replace(microsecond=0) + expires
|
|
cookies = {"test": "wait"}
|
|
|
|
@app.get("/")
|
|
def handler(request):
|
|
response = text("pass")
|
|
response.cookies["test"] = "pass"
|
|
response.cookies["test"]["expires"] = expires_time
|
|
return response
|
|
|
|
request, response = app.test_client.get(
|
|
"/", cookies=cookies, raw_cookies=True
|
|
)
|
|
|
|
cookie_expires = datetime.utcfromtimestamp(
|
|
response.raw_cookies["test"].expires
|
|
).replace(microsecond=0)
|
|
|
|
assert response.status == 200
|
|
assert response.cookies["test"] == "pass"
|
|
assert cookie_expires == expires_time
|
|
|
|
|
|
@pytest.mark.parametrize("expires", ["Fri, 21-Dec-2018 15:30:00 GMT"])
|
|
def test_cookie_expires_illegal_instance_type(expires):
|
|
c = Cookie("test_cookie", "value")
|
|
with pytest.raises(expected_exception=TypeError) as e:
|
|
c["expires"] = expires
|
|
assert e.message == "Cookie 'expires' property must be a datetime"
|
|
|
|
|
|
@pytest.mark.parametrize("value", ("foo=one; foo=two", "foo=one;foo=two"))
|
|
def test_request_with_duplicate_cookie_key(value):
|
|
headers = Header({"Cookie": value})
|
|
request = Request(b"/", headers, "1.1", "GET", Mock(), Mock())
|
|
|
|
assert request.cookies["foo"] == "one"
|
|
assert request.cookies.get("foo") == "one"
|
|
assert request.cookies.getlist("foo") == ["one", "two"]
|
|
assert request.cookies.get("bar") is None
|
|
|
|
|
|
def test_cookie_jar_cookies():
|
|
headers = Header()
|
|
jar = CookieJar(headers)
|
|
jar.add_cookie("foo", "one")
|
|
jar.add_cookie("foo", "two", domain="example.com")
|
|
|
|
assert len(jar.cookies) == 2
|
|
assert len(headers) == 2
|
|
|
|
|
|
def test_cookie_jar_has_cookie():
|
|
headers = Header()
|
|
jar = CookieJar(headers)
|
|
jar.add_cookie("foo", "one")
|
|
jar.add_cookie("foo", "two", domain="example.com")
|
|
|
|
assert jar.has_cookie("foo")
|
|
assert jar.has_cookie("foo", domain="example.com")
|
|
assert not jar.has_cookie("foo", path="/unknown")
|
|
assert not jar.has_cookie("bar")
|
|
|
|
|
|
def test_cookie_jar_get_cookie():
|
|
headers = Header()
|
|
jar = CookieJar(headers)
|
|
cookie1 = jar.add_cookie("foo", "one")
|
|
cookie2 = jar.add_cookie("foo", "two", domain="example.com")
|
|
|
|
assert jar.get_cookie("foo") is cookie1
|
|
assert jar.get_cookie("foo", domain="example.com") is cookie2
|
|
assert jar.get_cookie("foo", path="/unknown") is None
|
|
assert jar.get_cookie("bar") is None
|
|
|
|
|
|
def test_cookie_jar_add_cookie_encode():
|
|
headers = Header()
|
|
jar = CookieJar(headers)
|
|
jar.add_cookie("foo", "one")
|
|
jar.add_cookie(
|
|
"foo",
|
|
"two",
|
|
domain="example.com",
|
|
path="/something",
|
|
secure=True,
|
|
max_age=999,
|
|
httponly=True,
|
|
samesite="strict",
|
|
)
|
|
jar.add_cookie("foo", "three", secure_prefix=True)
|
|
jar.add_cookie("foo", "four", host_prefix=True)
|
|
jar.add_cookie("foo", "five", host_prefix=True, partitioned=True)
|
|
|
|
encoded = [cookie.encode("ascii") for cookie in jar.cookies]
|
|
assert encoded == [
|
|
b"foo=one; Path=/; SameSite=Lax; Secure",
|
|
b"foo=two; Path=/something; Domain=example.com; Max-Age=999; SameSite=Strict; Secure; HttpOnly", # noqa
|
|
b"__Secure-foo=three; Path=/; SameSite=Lax; Secure",
|
|
b"__Host-foo=four; Path=/; SameSite=Lax; Secure",
|
|
b"__Host-foo=five; Path=/; SameSite=Lax; Secure; Partitioned",
|
|
]
|
|
|
|
|
|
def test_cookie_jar_old_school_cookie_encode():
|
|
headers = Header()
|
|
jar = CookieJar(headers)
|
|
jar["foo"] = "one"
|
|
jar["bar"] = "two"
|
|
jar["bar"]["domain"] = "example.com"
|
|
jar["bar"]["path"] = "/something"
|
|
jar["bar"]["secure"] = True
|
|
jar["bar"]["max-age"] = 999
|
|
jar["bar"]["httponly"] = True
|
|
jar["bar"]["samesite"] = "strict"
|
|
|
|
encoded = [cookie.encode("ascii") for cookie in jar.cookies]
|
|
assert encoded == [
|
|
b"foo=one; Path=/",
|
|
b"bar=two; Path=/something; Domain=example.com; Max-Age=999; SameSite=Strict; Secure; HttpOnly", # noqa
|
|
]
|
|
|
|
|
|
def test_cookie_jar_delete_cookie_encode():
|
|
headers = Header()
|
|
jar = CookieJar(headers)
|
|
jar.delete_cookie("foo")
|
|
jar.delete_cookie("foo", domain="example.com")
|
|
|
|
encoded = [cookie.encode("ascii") for cookie in jar.cookies]
|
|
assert encoded == [
|
|
b'foo=""; Path=/; Max-Age=0; Secure',
|
|
b'foo=""; Path=/; Domain=example.com; Max-Age=0; Secure',
|
|
]
|
|
|
|
|
|
def test_cookie_jar_old_school_delete_encode():
|
|
headers = Header()
|
|
jar = CookieJar(headers)
|
|
del jar["foo"]
|
|
|
|
encoded = [cookie.encode("ascii") for cookie in jar.cookies]
|
|
assert encoded == [
|
|
b'foo=""; Path=/; Max-Age=0; Secure',
|
|
]
|
|
|
|
|
|
def test_bad_cookie_prarms():
|
|
headers = Header()
|
|
jar = CookieJar(headers)
|
|
|
|
with pytest.raises(
|
|
ServerError,
|
|
match=(
|
|
"Both host_prefix and secure_prefix were requested. "
|
|
"A cookie should have only one prefix."
|
|
),
|
|
):
|
|
jar.add_cookie("foo", "bar", host_prefix=True, secure_prefix=True)
|
|
|
|
with pytest.raises(
|
|
ServerError,
|
|
match="Cannot set host_prefix on a cookie without secure=True",
|
|
):
|
|
jar.add_cookie("foo", "bar", host_prefix=True, secure=False)
|
|
|
|
with pytest.raises(
|
|
ServerError,
|
|
match="Cannot set host_prefix on a cookie unless path='/'",
|
|
):
|
|
jar.add_cookie(
|
|
"foo", "bar", host_prefix=True, secure=True, path="/foo"
|
|
)
|
|
|
|
with pytest.raises(
|
|
ServerError,
|
|
match="Cannot set host_prefix on a cookie with a defined domain",
|
|
):
|
|
jar.add_cookie(
|
|
"foo", "bar", host_prefix=True, secure=True, domain="foo.bar"
|
|
)
|
|
|
|
with pytest.raises(
|
|
ServerError,
|
|
match="Cannot set secure_prefix on a cookie without secure=True",
|
|
):
|
|
jar.add_cookie("foo", "bar", secure_prefix=True, secure=False)
|
|
|
|
with pytest.raises(
|
|
ServerError,
|
|
match=(
|
|
"Cannot create a partitioned cookie without "
|
|
"also setting host_prefix=True"
|
|
),
|
|
):
|
|
jar.add_cookie("foo", "bar", partitioned=True)
|
|
|
|
|
|
def test_cookie_accessors(app: Sanic):
|
|
@app.get("/")
|
|
async def handler(request: Request):
|
|
return json(
|
|
{
|
|
"getitem": {
|
|
"one": request.cookies["one"],
|
|
"two": request.cookies["two"],
|
|
"three": request.cookies["three"],
|
|
},
|
|
"get": {
|
|
"one": request.cookies.get("one", "fallback"),
|
|
"two": request.cookies.get("two", "fallback"),
|
|
"three": request.cookies.get("three", "fallback"),
|
|
"four": request.cookies.get("four", "fallback"),
|
|
},
|
|
"getlist": {
|
|
"one": request.cookies.getlist("one", ["fallback"]),
|
|
"two": request.cookies.getlist("two", ["fallback"]),
|
|
"three": request.cookies.getlist("three", ["fallback"]),
|
|
"four": request.cookies.getlist("four", ["fallback"]),
|
|
},
|
|
"getattr": {
|
|
"one": request.cookies.one,
|
|
"two": request.cookies.two,
|
|
"three": request.cookies.three,
|
|
"four": request.cookies.four,
|
|
},
|
|
}
|
|
)
|
|
|
|
_, response = app.test_client.get(
|
|
"/",
|
|
cookies={
|
|
"__Host-one": "1",
|
|
"__Secure-two": "2",
|
|
"three": "3",
|
|
},
|
|
)
|
|
|
|
assert response.json == {
|
|
"getitem": {
|
|
"one": "1",
|
|
"two": "2",
|
|
"three": "3",
|
|
},
|
|
"get": {
|
|
"one": "1",
|
|
"two": "2",
|
|
"three": "3",
|
|
"four": "fallback",
|
|
},
|
|
"getlist": {
|
|
"one": ["1"],
|
|
"two": ["2"],
|
|
"three": ["3"],
|
|
"four": ["fallback"],
|
|
},
|
|
"getattr": {
|
|
"one": "1",
|
|
"two": "2",
|
|
"three": "3",
|
|
"four": "",
|
|
},
|
|
}
|
|
|
|
|
|
def test_cookie_accessor_hyphens():
|
|
cookies = CookieRequestParameters({"session-token": ["abc123"]})
|
|
|
|
assert cookies.get("session-token") == cookies.session_token
|
|
|
|
|
|
def test_cookie_passthru(app):
|
|
cookie_jar = None
|
|
|
|
@app.route("/")
|
|
def handler(request):
|
|
nonlocal cookie_jar
|
|
response = text("OK")
|
|
response.add_cookie("one", "1", host_prefix=True)
|
|
response.delete_cookie("two", secure_prefix=True)
|
|
cookie_jar = response.cookies
|
|
return response
|
|
|
|
_, response = app.test_client.get("/")
|
|
|
|
assert cookie_jar.get_cookie("two", secure_prefix=True).max_age == 0
|
|
assert len(response.cookies) == 1
|
|
assert response.cookies["__Host-one"] == "1"
|