diff --git a/pyproject.toml b/pyproject.toml index 8c6668f..35dda01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ addopts = ["--maxfail=2", "-rf"] [tool.coverage.run] branch = true -omit = ["conftest.py"] +omit = ["conftest.py", "spiderweb/tests/*"] [tool.coverage.report] # Regexes for lines to exclude from consideration @@ -81,6 +81,8 @@ exclude_also = [ # Don't complain about abstract methods, they aren't run: "@(abc\\.)?abstractmethod", - ] + # Type checking lines are never run: + "if TYPE_CHECKING:", +] ignore_errors = true \ No newline at end of file diff --git a/spiderweb/main.py b/spiderweb/main.py index ddc6a37..b5a935c 100644 --- a/spiderweb/main.py +++ b/spiderweb/main.py @@ -175,6 +175,8 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi ) if self.staticfiles_dirs: + if not isinstance(self.staticfiles_dirs, list): + self.staticfiles_dirs = [self.staticfiles_dirs] for static_dir in self.staticfiles_dirs: static_dir = pathlib.Path(static_dir) if not pathlib.Path(self.BASE_DIR / static_dir).exists(): diff --git a/spiderweb/tests/staticfiles/.gitkeep b/spiderweb/tests/staticfiles/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/spiderweb/tests/staticfiles/file_for_testing_fileresponse.txt b/spiderweb/tests/staticfiles/file_for_testing_fileresponse.txt new file mode 100644 index 0000000..32f95c0 --- /dev/null +++ b/spiderweb/tests/staticfiles/file_for_testing_fileresponse.txt @@ -0,0 +1 @@ +hi \ No newline at end of file diff --git a/spiderweb/tests/staticfiles/style.css b/spiderweb/tests/staticfiles/style.css new file mode 100644 index 0000000..dc462a9 --- /dev/null +++ b/spiderweb/tests/staticfiles/style.css @@ -0,0 +1,6 @@ +.body { + background-color: #f0f0f0; + font-family: Arial, sans-serif; + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/spiderweb/tests/test_cookies.py b/spiderweb/tests/test_cookies.py new file mode 100644 index 0000000..9ecbea2 --- /dev/null +++ b/spiderweb/tests/test_cookies.py @@ -0,0 +1,149 @@ +from datetime import datetime + +import pytest + +from spiderweb import HttpResponse +from spiderweb.exceptions import GeneralException +from spiderweb.tests.helpers import setup + + +def test_valid_cookie(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse("Hello, World!") + resp.set_cookie("cookie", "value") + return resp + + response = app(environ, start_response) + assert response == [b"Hello, World!"] + assert start_response.get_headers()["set-cookie"] == "cookie=value" + + +def test_invalid_cookie_name(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse("Hello, World!") + resp.set_cookie("cookie$%^&*name", "value") + return resp + + with pytest.raises(GeneralException) as exc: + app(environ, start_response) + + assert str(exc.value) == ( + "GeneralException() - Cookie name has illegal characters." + " See https://developer.mozilla.org/en-US/docs/Web/HTTP/" + "Headers/Set-Cookie#attributes for information on allowed" + " characters." + ) + + +def test_cookie_with_domain(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse("Hello, World!") + resp.set_cookie("cookie", "value", domain="example.com") + return resp + + response = app(environ, start_response) + assert response == [b"Hello, World!"] + assert ( + start_response.get_headers()["set-cookie"] == "cookie=value; Domain=example.com" + ) + + +def test_cookie_with_expires(): + app, environ, start_response = setup() + expiry_time = datetime(2024, 10, 22, 7, 28) + expiry_time_str = expiry_time.strftime("%a, %d %b %Y %H:%M:%S GMT") + + @app.route("/") + def index(request): + resp = HttpResponse("Hello, World!") + resp.set_cookie("cookie", "value", expires=expiry_time) + return resp + + response = app(environ, start_response) + assert response == [b"Hello, World!"] + assert ( + start_response.get_headers()["set-cookie"] + == f"cookie=value; Expires={expiry_time_str}" + ) + + +def test_cookie_with_max_age(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse("Hello, World!") + resp.set_cookie("cookie", "value", max_age=3600) + return resp + + response = app(environ, start_response) + assert response == [b"Hello, World!"] + assert start_response.get_headers()["set-cookie"] == "cookie=value; Max-Age=3600" + + +def test_cookie_with_invalid_samesite_attr(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse("Hello, World!") + resp.set_cookie("cookie", "value", same_site="invalid") + return resp + + with pytest.raises(GeneralException) as exc: + app(environ, start_response) + + assert str(exc.value) == ( + "GeneralException() - Invalid value invalid for `same_site` cookie" + " attribute. Valid options are 'strict', 'lax', or 'none'." + ) + + +def test_cookie_partitioned_attr(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse() + resp.set_cookie("cookie", "value", partitioned=True) + return resp + + app(environ, start_response) + assert start_response.get_headers()["set-cookie"] == "cookie=value; Partitioned" + + +def test_cookie_secure_attr(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse() + resp.set_cookie("cookie", "value", secure=True) + return resp + + app(environ, start_response) + assert start_response.get_headers()["set-cookie"] == "cookie=value; Secure" + + +def test_setting_multiple_cookies(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse() + resp.set_cookie("cookie1", "value1") + resp.set_cookie("cookie2", "value2") + return resp + + app(environ, start_response) + assert start_response.headers[-1] == ("set-cookie", "cookie2=value2") + assert start_response.headers[-2] == ("set-cookie", "cookie1=value1") diff --git a/spiderweb/tests/test_jinja_extras.py b/spiderweb/tests/test_jinja_extras.py new file mode 100644 index 0000000..e218c7f --- /dev/null +++ b/spiderweb/tests/test_jinja_extras.py @@ -0,0 +1,35 @@ +from spiderweb.constants import DEFAULT_ENCODING +from spiderweb.response import TemplateResponse +from spiderweb.tests.helpers import setup + + +def test_str_template_with_static_tag(): + # test that the static tag works + template = """ + + + {{ title }} + + + +

{{ title }}

+

{{ content }}

+ + + """ + context = {"title": "Test", "content": "This is a test."} + app, environ, start_response = setup( + staticfiles_dirs=["spiderweb/tests/staticfiles"], static_url="blorp" + ) + + @app.route("/") + def index(request): + return TemplateResponse(request, template_string=template, context=context) + + rendered_template = ( + template.replace("{% static 'style.css' %}", "/blorp/style.css") + .replace("{{ title }}", "Test") + .replace("{{ content }}", "This is a test.") + ) + + assert app(environ, start_response) == [bytes(rendered_template, DEFAULT_ENCODING)] diff --git a/spiderweb/tests/test_responses.py b/spiderweb/tests/test_responses.py index cffa607..768a79b 100644 --- a/spiderweb/tests/test_responses.py +++ b/spiderweb/tests/test_responses.py @@ -7,12 +7,14 @@ from spiderweb.exceptions import ( SpiderwebNetworkException, SpiderwebException, ReverseNotFound, + GeneralException, ) from spiderweb.response import ( HttpResponse, JsonResponse, TemplateResponse, RedirectResponse, + FileResponse, ) from hypothesis import given, strategies as st @@ -240,3 +242,94 @@ def test_reverse_nonexistent_view(): with pytest.raises(ReverseNotFound): app.reverse("qwer") + + +def test_setting_content_type_header(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + resp = HttpResponse("Hello, World!", headers={"content-type": "text/html"}) + return resp + + response = app(environ, start_response) + assert response == [b"Hello, World!"] + assert start_response.get_headers()["content-type"] == "text/html" + + +def test_httpresponse_str_returns_body(): + resp = HttpResponse("Hello, World!") + assert str(resp) == "Hello, World!" + + +def test_template_response_with_no_templates_raises_errors(): + app, environ, start_response = setup() + + @app.route("/") + def index(request): + return TemplateResponse(request, "") + + with pytest.raises(GeneralException) as exc: + app(environ, start_response) + + assert ( + str(exc.value) == "GeneralException() - TemplateResponse requires a template." + ) + + +def test_template_response_with_no_template_dirs(): + + template = TemplateResponse("", "test.html") + + with pytest.raises(GeneralException) as exc: + template.render() + + assert str(exc.value) == ( + "GeneralException() - TemplateResponse has no loader. Did you set templates_dirs?" + ) + + +def test_file_response(): + resp = FileResponse("spiderweb/tests/staticfiles/file_for_testing_fileresponse.txt") + assert resp.headers["content-type"] == "text/plain" + assert resp.render() == [b"hi"] + + +def test_requesting_static_file(): + app, environ, start_response = setup( + staticfiles_dirs=["spiderweb/tests/staticfiles"], debug=True + ) + + environ["PATH_INFO"] = "/static/file_for_testing_fileresponse.txt" + + assert app(environ, start_response) == [b"hi"] + + +def test_requesting_nonexistent_static_file(): + app, environ, start_response = setup( + staticfiles_dirs=["spiderweb/tests/staticfiles"], debug=True + ) + + environ["PATH_INFO"] = "/static/does_not_exist.txt" + + assert app(environ, start_response) == [ + b"Something went wrong.\n\n" + b"Code: 404\n\n" + b"Msg: Not Found\n\n" + b"Desc: The requested resource could not be found" + ] + + +def test_static_file_with_unsafe_path(): + app, environ, start_response = setup( + staticfiles_dirs=["spiderweb/tests/staticfiles"], debug=True + ) + + environ["PATH_INFO"] = "/static/../__init__.py" + + assert app(environ, start_response) == [ + b"Something went wrong.\n\n" + b"Code: 404\n\n" + b"Msg: Not Found\n\n" + b"Desc: The requested resource could not be found" + ] diff --git a/spiderweb/tests/test_server.py b/spiderweb/tests/test_server.py new file mode 100644 index 0000000..e950920 --- /dev/null +++ b/spiderweb/tests/test_server.py @@ -0,0 +1,15 @@ +import pytest + +from spiderweb.exceptions import ConfigError +from spiderweb.tests.helpers import setup + + +def test_staticfiles_dirs_option(): + app, environ, start_response = setup(staticfiles_dirs="spiderweb/tests/staticfiles") + + assert app.staticfiles_dirs == ["spiderweb/tests/staticfiles"] + + +def test_staticfiles_dirs_not_found(): + with pytest.raises(ConfigError): + app, environ, start_response = setup(staticfiles_dirs="not/a/real/path")