import inspect import os import sys from pathlib import Path from time import gmtime, strftime import pytest from sanic.app import Sanic @pytest.fixture(scope="module") def static_file_directory(): """The static directory to serve""" current_file = inspect.getfile(inspect.currentframe()) current_directory = os.path.dirname(os.path.abspath(current_file)) static_directory = os.path.join(current_directory, "static") return static_directory @pytest.fixture(scope="module") def double_dotted_directory_file(static_file_directory: str): """Generate double dotted directory and its files""" if sys.platform == "win32": raise Exception("Windows doesn't support double dotted directories") file_path = Path(static_file_directory) / "dotted.." / "dot.txt" double_dotted_dir = file_path.parent Path.mkdir(double_dotted_dir, exist_ok=True) with open(file_path, "w") as f: f.write("DOT\n") yield file_path Path.unlink(file_path) Path.rmdir(double_dotted_dir) def get_file_path(static_file_directory, file_name): return os.path.join(static_file_directory, file_name) def get_file_content(static_file_directory, file_name): """The content of the static file to check""" with open(get_file_path(static_file_directory, file_name), "rb") as file: return file.read() @pytest.fixture(scope="module") def large_file(static_file_directory): large_file_path = os.path.join(static_file_directory, "large.file") size = 2 * 1024 * 1024 with open(large_file_path, "w") as f: f.write("a" * size) yield large_file_path os.remove(large_file_path) @pytest.fixture(autouse=True, scope="module") def symlink(static_file_directory): src = os.path.abspath( os.path.join(os.path.dirname(static_file_directory), "conftest.py") ) symlink = "symlink" dist = os.path.join(static_file_directory, symlink) os.symlink(src, dist) yield symlink os.remove(dist) @pytest.fixture(autouse=True, scope="module") def hard_link(static_file_directory): src = os.path.abspath( os.path.join(os.path.dirname(static_file_directory), "conftest.py") ) hard_link = "hard_link" dist = os.path.join(static_file_directory, hard_link) os.link(src, dist) yield hard_link os.remove(dist) @pytest.mark.parametrize( "file_name", ["test.file", "decode me.txt", "python.png", "symlink", "hard_link"], ) def test_static_file(app, static_file_directory, file_name): app.static( "/testing.file", get_file_path(static_file_directory, file_name) ) request, response = app.test_client.get("/testing.file") assert response.status == 200 assert response.body == get_file_content(static_file_directory, file_name) @pytest.mark.parametrize("file_name", ["test.html"]) def test_static_file_content_type(app, static_file_directory, file_name): app.static( "/testing.file", get_file_path(static_file_directory, file_name), content_type="text/html; charset=utf-8", ) request, response = app.test_client.get("/testing.file") assert response.status == 200 assert response.body == get_file_content(static_file_directory, file_name) assert response.headers["Content-Type"] == "text/html; charset=utf-8" @pytest.mark.parametrize( "file_name", ["test.file", "decode me.txt", "symlink", "hard_link"] ) @pytest.mark.parametrize("base_uri", ["/static", "", "/dir"]) def test_static_directory(app, file_name, base_uri, static_file_directory): app.static(base_uri, static_file_directory) request, response = app.test_client.get(uri=f"{base_uri}/{file_name}") assert response.status == 200 assert response.body == get_file_content(static_file_directory, file_name) @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_head_request(app, file_name, static_file_directory): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) request, response = app.test_client.head("/testing.file") assert response.status == 200 assert "Accept-Ranges" in response.headers assert "Content-Length" in response.headers assert int(response.headers["Content-Length"]) == len( get_file_content(static_file_directory, file_name) ) @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_correct(app, file_name, static_file_directory): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) headers = {"Range": "bytes=12-19"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 206 assert "Content-Length" in response.headers assert "Content-Range" in response.headers static_content = bytes(get_file_content(static_file_directory, file_name))[ 12:20 ] assert int(response.headers["Content-Length"]) == len(static_content) assert response.body == static_content @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_front(app, file_name, static_file_directory): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) headers = {"Range": "bytes=12-"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 206 assert "Content-Length" in response.headers assert "Content-Range" in response.headers static_content = bytes(get_file_content(static_file_directory, file_name))[ 12: ] assert int(response.headers["Content-Length"]) == len(static_content) assert response.body == static_content @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_back(app, file_name, static_file_directory): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) headers = {"Range": "bytes=-12"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 206 assert "Content-Length" in response.headers assert "Content-Range" in response.headers static_content = bytes(get_file_content(static_file_directory, file_name))[ -12: ] assert int(response.headers["Content-Length"]) == len(static_content) assert response.body == static_content @pytest.mark.parametrize("use_modified_since", [True, False]) @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_empty( app, file_name, static_file_directory, use_modified_since ): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, use_modified_since=use_modified_since, ) request, response = app.test_client.get("/testing.file") assert response.status == 200 assert "Content-Length" in response.headers assert "Content-Range" not in response.headers assert int(response.headers["Content-Length"]) == len( get_file_content(static_file_directory, file_name) ) assert response.body == bytes( get_file_content(static_file_directory, file_name) ) @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_error(app, file_name, static_file_directory): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) headers = {"Range": "bytes=1-0"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 416 assert "Content-Length" in response.headers assert "Content-Range" in response.headers assert response.headers["Content-Range"] == "bytes */%s" % ( len(get_file_content(static_file_directory, file_name)), ) @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_invalid_unit( app, file_name, static_file_directory ): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) unit = "bit" headers = {"Range": f"{unit}=1-0"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 416 assert f"{unit} is not a valid Range Type" in response.text @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_invalid_start( app, file_name, static_file_directory ): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) start = "start" headers = {"Range": f"bytes={start}-0"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 416 assert f"'{start}' is invalid for Content Range" in response.text @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_invalid_end( app, file_name, static_file_directory ): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) end = "end" headers = {"Range": f"bytes=1-{end}"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 416 assert f"'{end}' is invalid for Content Range" in response.text @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"]) def test_static_content_range_invalid_parameters( app, file_name, static_file_directory ): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_content_range=True, ) headers = {"Range": "bytes=-"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 416 assert "Invalid for Content Range parameters" in response.text @pytest.mark.parametrize( "file_name", ["test.file", "decode me.txt", "python.png"] ) def test_static_file_specified_host(app, static_file_directory, file_name): app.static( "/testing.file", get_file_path(static_file_directory, file_name), host="www.example.com", ) headers = {"Host": "www.example.com"} request, response = app.test_client.get("/testing.file", headers=headers) assert response.status == 200 assert response.body == get_file_content(static_file_directory, file_name) request, response = app.test_client.get("/testing.file") assert response.status == 404 @pytest.mark.parametrize("use_modified_since", [True, False]) @pytest.mark.parametrize("stream_large_files", [True, 1024]) @pytest.mark.parametrize("file_name", ["test.file", "large.file"]) def test_static_stream_large_file( app, static_file_directory, file_name, use_modified_since, stream_large_files, large_file, ): app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_modified_since=use_modified_since, stream_large_files=stream_large_files, ) request, response = app.test_client.get("/testing.file") assert response.status == 200 assert response.body == get_file_content(static_file_directory, file_name) @pytest.mark.parametrize( "file_name", ["test.file", "decode me.txt", "python.png"] ) def test_use_modified_since(app, static_file_directory, file_name): file_stat = os.stat(get_file_path(static_file_directory, file_name)) modified_since = strftime( "%a, %d %b %Y %H:%M:%S GMT", gmtime(file_stat.st_mtime) ) app.static( "/testing.file", get_file_path(static_file_directory, file_name), use_modified_since=True, ) request, response = app.test_client.get( "/testing.file", headers={"If-Modified-Since": modified_since} ) assert response.status == 304 def test_file_not_found(app, static_file_directory): app.static("/static", static_file_directory) request, response = app.test_client.get("/static/not_found") assert response.status == 404 assert "File not found" in response.text @pytest.mark.parametrize("static_name", ["_static_name", "static"]) @pytest.mark.parametrize("file_name", ["test.html"]) def test_static_name(app, static_file_directory, static_name, file_name): app.static("/static", static_file_directory, name=static_name) request, response = app.test_client.get(f"/static/{file_name}") assert response.status == 200 @pytest.mark.skipif( sys.platform == "win32", reason="Windows does not support double dotted directories", ) def test_dotted_dir_ok( app: Sanic, static_file_directory: str, double_dotted_directory_file: Path ): app.static("/foo", static_file_directory) url = ( "/foo" + str(double_dotted_directory_file)[len(static_file_directory) :] ) _, response = app.test_client.get(url) assert response.status == 200 assert response.body == b"DOT\n" def test_breakout(app: Sanic, static_file_directory: str): app.static("/foo", static_file_directory) _, response = app.test_client.get("/foo/..%2Fstatic/test.file") assert response.status == 400 @pytest.mark.skipif( sys.platform != "win32", reason="Block backslash on Windows only" ) def test_double_backslash_prohibited_on_win32( app: Sanic, static_file_directory: str ): app.static("/foo", static_file_directory) _, response = app.test_client.get("/foo/static/..\\static/test.file") assert response.status == 400 _, response = app.test_client.get("/foo/static\\../static/test.file") assert response.status == 400