1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
|
import datetime
import io
import pathlib
import pytest
from werkzeug.exceptions import NotFound
from werkzeug.http import http_date
from werkzeug.test import EnvironBuilder
from werkzeug.utils import send_file
from werkzeug.utils import send_from_directory
res_path = pathlib.Path(__file__).parent / "res"
html_path = res_path / "index.html"
txt_path = res_path / "test.txt"
environ = EnvironBuilder().get_environ()
@pytest.mark.parametrize("path", [html_path, str(html_path)])
def test_path(path):
rv = send_file(path, environ)
assert rv.mimetype == "text/html"
assert rv.direct_passthrough
rv.direct_passthrough = False
assert rv.data == html_path.read_bytes()
rv.close()
def test_x_sendfile():
rv = send_file(html_path, environ, use_x_sendfile=True)
assert rv.headers["x-sendfile"] == str(html_path)
assert rv.data == b""
rv.close()
def test_last_modified():
last_modified = datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc)
rv = send_file(txt_path, environ, last_modified=last_modified)
assert rv.last_modified == last_modified
rv.close()
@pytest.mark.parametrize(
"file_factory", [lambda: txt_path.open("rb"), lambda: io.BytesIO(b"test")]
)
def test_object(file_factory):
rv = send_file(file_factory(), environ, mimetype="text/plain", use_x_sendfile=True)
rv.direct_passthrough = False
assert rv.data
assert rv.mimetype == "text/plain"
assert "x-sendfile" not in rv.headers
rv.close()
def test_object_without_mimetype():
with pytest.raises(TypeError, match="detect the MIME type"):
send_file(io.BytesIO(b"test"), environ)
def test_object_mimetype_from_name():
rv = send_file(io.BytesIO(b"test"), environ, download_name="test.txt")
assert rv.mimetype == "text/plain"
rv.close()
@pytest.mark.parametrize(
"file_factory", [lambda: txt_path.open(), lambda: io.StringIO("test")]
)
def test_text_mode_fails(file_factory):
with file_factory() as f, pytest.raises(ValueError, match="binary mode"):
send_file(f, environ, mimetype="text/plain")
@pytest.mark.parametrize(
("as_attachment", "value"), [(False, "inline"), (True, "attachment")]
)
def test_disposition_name(as_attachment, value):
rv = send_file(txt_path, environ, as_attachment=as_attachment)
assert rv.headers["Content-Disposition"] == f"{value}; filename=test.txt"
rv.close()
def test_object_attachment_requires_name():
with pytest.raises(TypeError, match="attachment"):
send_file(
io.BytesIO(b"test"), environ, mimetype="text/plain", as_attachment=True
)
rv = send_file(
io.BytesIO(b"test"), environ, as_attachment=True, download_name="test.txt"
)
assert rv.headers["Content-Disposition"] == "attachment; filename=test.txt"
rv.close()
@pytest.mark.parametrize(
("name", "ascii", "utf8"),
(
("index.html", "index.html", None),
(
"Ñandú/pingüino.txt",
'"Nandu/pinguino.txt"',
"%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt",
),
# latin-1 isn't ascii, should be quoted
("Vögel.txt", "Vogel.txt", "V%C3%B6gel.txt"),
# ":/" are not safe in filename* value
("те:/ст", '":/"', "%D1%82%D0%B5%3A%2F%D1%81%D1%82"),
),
)
def test_non_ascii_name(name, ascii, utf8):
rv = send_file(html_path, environ, as_attachment=True, download_name=name)
rv.close()
content_disposition = rv.headers["Content-Disposition"]
assert f"filename={ascii}" in content_disposition
if utf8:
assert f"filename*=UTF-8''{utf8}" in content_disposition
else:
assert "filename*=UTF-8''" not in content_disposition
def test_no_cache_conditional_default():
rv = send_file(
txt_path,
EnvironBuilder(
headers={"If-Modified-Since": http_date(datetime.datetime(2020, 7, 12))}
).get_environ(),
last_modified=datetime.datetime(2020, 7, 11),
)
rv.close()
assert "no-cache" in rv.headers["Cache-Control"]
assert not rv.cache_control.public
assert not rv.cache_control.max_age
assert not rv.expires
assert rv.status_code == 304
@pytest.mark.parametrize(("value", "public"), [(0, False), (60, True)])
def test_max_age(value, public):
rv = send_file(txt_path, environ, max_age=value)
rv.close()
assert ("no-cache" in rv.headers["Cache-Control"]) != public
assert rv.cache_control.public == public
assert rv.cache_control.max_age == value
assert rv.expires
assert rv.status_code == 200
def test_etag():
rv = send_file(txt_path, environ)
rv.close()
assert rv.headers["ETag"].count("-") == 2
rv = send_file(txt_path, environ, etag=False)
rv.close()
assert "ETag" not in rv.headers
rv = send_file(txt_path, environ, etag="unique")
rv.close()
assert rv.headers["ETag"] == '"unique"'
@pytest.mark.parametrize("as_attachment", (True, False))
def test_content_encoding(as_attachment):
rv = send_file(
txt_path, environ, download_name="logo.svgz", as_attachment=as_attachment
)
rv.close()
assert rv.mimetype == "image/svg+xml"
assert rv.content_encoding == ("gzip" if not as_attachment else None)
@pytest.mark.parametrize(
("directory", "path"),
[(str(res_path), "test.txt"), (res_path, pathlib.Path("test.txt"))],
)
def test_from_directory(directory, path):
rv = send_from_directory(directory, path, environ)
rv.direct_passthrough = False
assert rv.data.strip() == b"FOUND"
rv.close()
@pytest.mark.parametrize("path", ["../res/test.txt", "nothing.txt", "null\x00.txt"])
def test_from_directory_not_found(path):
with pytest.raises(NotFound):
send_from_directory(res_path, path, environ)
def test_root_path(tmp_path):
# This is a private API, it should only be used by Flask.
d = tmp_path / "d"
d.mkdir()
(d / "test.txt").write_bytes(b"test")
rv = send_file("d/test.txt", environ, _root_path=tmp_path)
rv.direct_passthrough = False
assert rv.data == b"test"
rv.close()
rv = send_from_directory("d", "test.txt", environ, _root_path=tmp_path)
rv.direct_passthrough = False
assert rv.data == b"test"
rv.close()
def test_max_age_callable():
# This is a private API, it should only be used by Flask.
rv = send_file(txt_path, environ, max_age=lambda p: 10)
rv.close()
assert rv.cache_control.max_age == 10
|