sanic/tests/test_url_building.py

363 lines
10 KiB
Python
Raw Normal View History

import string
from urllib.parse import parse_qsl, urlsplit
import pytest
2021-01-19 13:54:20 +00:00
from sanic_testing.testing import HOST as test_host
from sanic_testing.testing import PORT as test_port
2021-02-08 10:18:29 +00:00
from sanic import Sanic
2017-02-02 17:21:14 +00:00
from sanic.blueprints import Blueprint
from sanic.exceptions import URLBuildError
from sanic.response import text
from sanic.views import HTTPMethodView
2017-02-02 17:21:14 +00:00
URL_FOR_ARGS1 = {"arg1": ["v1", "v2"]}
2018-12-30 11:18:06 +00:00
URL_FOR_VALUE1 = "/myurl?arg1=v1&arg1=v2"
URL_FOR_ARGS2 = {"arg1": ["v1", "v2"], "_anchor": "anchor"}
2018-12-30 11:18:06 +00:00
URL_FOR_VALUE2 = "/myurl?arg1=v1&arg1=v2#anchor"
URL_FOR_ARGS3 = {
"arg1": "v1",
"_anchor": "anchor",
"_scheme": "http",
"_server": f"{test_host}:{test_port}",
"_external": True,
}
URL_FOR_VALUE3 = f"http://{test_host}:{test_port}/myurl?arg1=v1#anchor"
URL_FOR_ARGS4 = {
"arg1": "v1",
"_anchor": "anchor",
"_external": True,
"_server": f"http://{test_host}:{test_port}",
}
URL_FOR_VALUE4 = f"http://{test_host}:{test_port}/myurl?arg1=v1#anchor"
2017-02-02 17:21:14 +00:00
def _generate_handlers_from_names(app, l):
for name in l:
# this is the easiest way to generate functions with dynamic names
2018-12-30 11:18:06 +00:00
exec(
f'@app.route(name)\ndef {name}(request):\n\treturn text("{name}")'
2018-12-30 11:18:06 +00:00
)
2017-02-02 17:21:14 +00:00
@pytest.fixture
2018-08-26 15:43:14 +01:00
def simple_app(app):
2017-02-02 17:21:14 +00:00
handler_names = list(string.ascii_letters)
_generate_handlers_from_names(app, handler_names)
return app
def test_simple_url_for_getting(simple_app):
for letter in string.ascii_letters:
url = simple_app.url_for(letter)
assert url == f"/{letter}"
2017-02-14 19:51:20 +00:00
request, response = simple_app.test_client.get(url)
2017-02-02 17:21:14 +00:00
assert response.status == 200
assert response.text == letter
2018-12-30 11:18:06 +00:00
@pytest.mark.parametrize(
"args,url",
[
(URL_FOR_ARGS1, URL_FOR_VALUE1),
(URL_FOR_ARGS2, URL_FOR_VALUE2),
(URL_FOR_ARGS3, URL_FOR_VALUE3),
(URL_FOR_ARGS4, URL_FOR_VALUE4),
],
)
2018-08-26 15:43:14 +01:00
def test_simple_url_for_getting_with_more_params(app, args, url):
2018-12-30 11:18:06 +00:00
@app.route("/myurl")
def passes(request):
2018-12-30 11:18:06 +00:00
return text("this should pass")
2018-12-30 11:18:06 +00:00
assert url == app.url_for("passes", **args)
2017-02-14 19:51:20 +00:00
request, response = app.test_client.get(url)
assert response.status == 200
2018-12-30 11:18:06 +00:00
assert response.text == "this should pass"
2018-12-13 17:50:50 +00:00
def test_url_for_with_server_name(app):
server_name = f"{test_host}:{test_port}"
2018-12-30 11:18:06 +00:00
app.config.update({"SERVER_NAME": server_name})
path = "/myurl"
2018-12-13 17:50:50 +00:00
@app.route(path)
def passes(request):
2018-12-30 11:18:06 +00:00
return text("this should pass")
2018-12-13 17:50:50 +00:00
url = f"http://{server_name}{path}"
2018-12-30 11:18:06 +00:00
assert url == app.url_for("passes", _server=None, _external=True)
2018-12-13 17:50:50 +00:00
request, response = app.test_client.get(url)
assert response.status == 200
2018-12-30 11:18:06 +00:00
assert response.text == "this should pass"
2018-12-13 17:50:50 +00:00
2021-02-08 10:18:29 +00:00
def test_fails_if_endpoint_not_found():
app = Sanic("app")
2018-12-30 11:18:06 +00:00
@app.route("/fail")
2018-01-22 06:52:30 +00:00
def fail(request):
2018-12-30 11:18:06 +00:00
return text("this should fail")
2017-02-02 17:21:14 +00:00
with pytest.raises(URLBuildError) as e:
2018-12-30 11:18:06 +00:00
app.url_for("passes")
2021-02-08 10:18:29 +00:00
e.match("Endpoint with name `app.passes` was not found")
2017-02-02 17:21:14 +00:00
2018-08-26 15:43:14 +01:00
def test_fails_url_build_if_param_not_passed(app):
2018-12-30 11:18:06 +00:00
url = "/"
2017-02-02 17:21:14 +00:00
2021-02-07 09:38:37 +00:00
for letter in string.ascii_lowercase:
url += f"<{letter}>/"
2017-02-02 17:21:14 +00:00
@app.route(url)
2018-01-22 06:52:30 +00:00
def fail(request):
2018-12-30 11:18:06 +00:00
return text("this should fail")
2017-02-02 17:21:14 +00:00
2021-02-07 09:38:37 +00:00
fail_args = list(string.ascii_lowercase)
2017-02-02 17:21:14 +00:00
fail_args.pop()
fail_kwargs = {l: l for l in fail_args}
with pytest.raises(URLBuildError) as e:
2018-12-30 11:18:06 +00:00
app.url_for("fail", **fail_kwargs)
2021-02-07 09:38:37 +00:00
assert e.match("Required parameter `z` was not passed to url_for")
2017-02-02 17:21:14 +00:00
2018-08-26 15:43:14 +01:00
def test_fails_url_build_if_params_not_passed(app):
2018-12-30 11:18:06 +00:00
@app.route("/fail")
2018-01-22 06:52:30 +00:00
def fail(request):
2018-12-30 11:18:06 +00:00
return text("this should fail")
with pytest.raises(ValueError) as e:
2018-12-30 11:18:06 +00:00
app.url_for("fail", _scheme="http")
2021-02-07 09:38:37 +00:00
assert e.match("When specifying _scheme, _external must be True")
2017-02-02 17:21:14 +00:00
COMPLEX_PARAM_URL = (
2018-12-30 11:18:06 +00:00
"/<foo:int>/<four_letter_string:[A-z]{4}>/"
"<two_letter_string:[A-z]{2}>/<normal_string>/<some_number:float>"
2018-12-30 11:18:06 +00:00
)
2017-02-02 17:21:14 +00:00
PASSING_KWARGS = {
2018-12-30 11:18:06 +00:00
"foo": 4,
"four_letter_string": "woof",
"two_letter_string": "ba",
"normal_string": "normal",
"some_number": "1.001",
}
EXPECTED_BUILT_URL = "/4/woof/ba/normal/1.001"
2017-02-02 17:21:14 +00:00
2018-08-26 15:43:14 +01:00
def test_fails_with_int_message(app):
2017-02-02 17:21:14 +00:00
@app.route(COMPLEX_PARAM_URL)
2018-01-22 06:52:30 +00:00
def fail(request):
2018-12-30 11:18:06 +00:00
return text("this should fail")
2017-02-02 17:21:14 +00:00
failing_kwargs = dict(PASSING_KWARGS)
2018-12-30 11:18:06 +00:00
failing_kwargs["foo"] = "not_int"
2017-02-02 17:21:14 +00:00
with pytest.raises(URLBuildError) as e:
2018-12-30 11:18:06 +00:00
app.url_for("fail", **failing_kwargs)
2017-02-02 17:21:14 +00:00
expected_error = (
r'Value "not_int" for parameter `foo` '
r"does not match pattern for type `int`: ^-?\d+$"
2018-12-30 11:18:06 +00:00
)
2017-02-02 17:21:14 +00:00
assert str(e.value) == expected_error
def test_passes_with_negative_int_message(app):
@app.route("path/<possibly_neg:int>/another-word")
def good(request, possibly_neg):
assert isinstance(possibly_neg, int)
return text(f"this should pass with `{possibly_neg}`")
u_plus_3 = app.url_for("good", possibly_neg=3)
assert u_plus_3 == "/path/3/another-word", u_plus_3
request, response = app.test_client.get(u_plus_3)
assert response.text == "this should pass with `3`"
u_neg_3 = app.url_for("good", possibly_neg=-3)
assert u_neg_3 == "/path/-3/another-word", u_neg_3
request, response = app.test_client.get(u_neg_3)
assert response.text == "this should pass with `-3`"
2018-08-26 15:43:14 +01:00
def test_fails_with_two_letter_string_message(app):
2017-02-02 17:21:14 +00:00
@app.route(COMPLEX_PARAM_URL)
2018-01-22 06:52:30 +00:00
def fail(request):
2018-12-30 11:18:06 +00:00
return text("this should fail")
2017-02-02 17:21:14 +00:00
failing_kwargs = dict(PASSING_KWARGS)
2018-12-30 11:18:06 +00:00
failing_kwargs["two_letter_string"] = "foobar"
2017-02-02 17:21:14 +00:00
with pytest.raises(URLBuildError) as e:
2018-12-30 11:18:06 +00:00
app.url_for("fail", **failing_kwargs)
2021-02-07 09:38:37 +00:00
e.match(
'Value "foobar" for parameter `two_letter_string` '
"does not satisfy pattern ^[A-z]{2}$"
)
2017-02-02 17:21:14 +00:00
2018-08-26 15:43:14 +01:00
def test_fails_with_number_message(app):
2017-02-02 17:21:14 +00:00
@app.route(COMPLEX_PARAM_URL)
2018-01-22 06:52:30 +00:00
def fail(request):
2018-12-30 11:18:06 +00:00
return text("this should fail")
2017-02-02 17:21:14 +00:00
failing_kwargs = dict(PASSING_KWARGS)
2018-12-30 11:18:06 +00:00
failing_kwargs["some_number"] = "foo"
2017-02-02 17:21:14 +00:00
with pytest.raises(URLBuildError) as e:
2018-12-30 11:18:06 +00:00
app.url_for("fail", **failing_kwargs)
2021-02-07 09:38:37 +00:00
e.match(
'Value "foo" for parameter `some_number` '
r"does not match pattern for type `float`: ^-?(?:\d+(?:\.\d*)?|\.\d+)$"
)
2017-02-02 17:21:14 +00:00
@pytest.mark.parametrize("number", [3, -3, 13.123, -13.123])
def test_passes_with_negative_number_message(app, number):
@app.route("path/<possibly_neg:float>/another-word")
def good(request, possibly_neg):
assert isinstance(possibly_neg, (int, float))
return text(f"this should pass with `{possibly_neg}`")
u = app.url_for("good", possibly_neg=number)
assert u == f"/path/{number}/another-word", u
request, response = app.test_client.get(u)
# For ``number``, it has been cast to a float - so a ``3`` becomes a ``3.0``
assert response.text == f"this should pass with `{float(number)}`"
2018-08-26 15:43:14 +01:00
def test_adds_other_supplied_values_as_query_string(app):
2017-02-02 17:21:14 +00:00
@app.route(COMPLEX_PARAM_URL)
2018-01-22 06:52:30 +00:00
def passes(request):
2018-12-30 11:18:06 +00:00
return text("this should pass")
2017-02-02 17:21:14 +00:00
new_kwargs = dict(PASSING_KWARGS)
2018-12-30 11:18:06 +00:00
new_kwargs["added_value_one"] = "one"
new_kwargs["added_value_two"] = "two"
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
url = app.url_for("passes", **new_kwargs)
2017-02-02 17:21:14 +00:00
query = dict(parse_qsl(urlsplit(url).query))
2018-12-30 11:18:06 +00:00
assert query["added_value_one"] == "one"
assert query["added_value_two"] == "two"
2017-02-02 17:21:14 +00:00
@pytest.fixture
2021-02-08 10:18:29 +00:00
def blueprint_app():
app = Sanic("app")
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
first_print = Blueprint("first", url_prefix="/first")
second_print = Blueprint("second", url_prefix="/second")
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
@first_print.route("/foo")
2018-01-22 06:52:30 +00:00
def foo(request):
2018-12-30 11:18:06 +00:00
return text("foo from first")
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
@first_print.route("/foo/<param>")
2017-02-02 17:21:14 +00:00
def foo_with_param(request, param):
return text(f"foo from first : {param}")
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
@second_print.route("/foo") # noqa
2021-02-07 09:38:37 +00:00
def bar(request):
2018-12-30 11:18:06 +00:00
return text("foo from second")
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
@second_print.route("/foo/<param>") # noqa
2021-02-07 09:38:37 +00:00
def bar_with_param(request, param):
return text(f"foo from second : {param}")
2017-02-02 17:21:14 +00:00
app.blueprint(first_print)
app.blueprint(second_print)
return app
def test_blueprints_are_named_correctly(blueprint_app):
2018-12-30 11:18:06 +00:00
first_url = blueprint_app.url_for("first.foo")
assert first_url == "/first/foo"
2017-02-02 17:21:14 +00:00
2021-02-07 09:38:37 +00:00
second_url = blueprint_app.url_for("second.bar")
2018-12-30 11:18:06 +00:00
assert second_url == "/second/foo"
2017-02-02 17:21:14 +00:00
def test_blueprints_work_with_params(blueprint_app):
2018-12-30 11:18:06 +00:00
first_url = blueprint_app.url_for("first.foo_with_param", param="bar")
assert first_url == "/first/foo/bar"
2017-02-02 17:21:14 +00:00
2021-02-07 09:38:37 +00:00
second_url = blueprint_app.url_for("second.bar_with_param", param="bar")
2018-12-30 11:18:06 +00:00
assert second_url == "/second/foo/bar"
2017-02-02 17:21:14 +00:00
@pytest.fixture
2018-08-26 15:43:14 +01:00
def methodview_app(app):
2017-02-02 17:21:14 +00:00
class ViewOne(HTTPMethodView):
def get(self, request):
2018-12-30 11:18:06 +00:00
return text("I am get method")
2017-02-02 17:21:14 +00:00
def post(self, request):
2018-12-30 11:18:06 +00:00
return text("I am post method")
2017-02-02 17:21:14 +00:00
def put(self, request):
2018-12-30 11:18:06 +00:00
return text("I am put method")
2017-02-02 17:21:14 +00:00
def patch(self, request):
2018-12-30 11:18:06 +00:00
return text("I am patch method")
2017-02-02 17:21:14 +00:00
def delete(self, request):
2018-12-30 11:18:06 +00:00
return text("I am delete method")
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
app.add_route(ViewOne.as_view("view_one"), "/view_one")
2017-02-02 17:21:14 +00:00
class ViewTwo(HTTPMethodView):
def get(self, request):
2018-12-30 11:18:06 +00:00
return text("I am get method")
2017-02-02 17:21:14 +00:00
def post(self, request):
2018-12-30 11:18:06 +00:00
return text("I am post method")
2017-02-02 17:21:14 +00:00
def put(self, request):
2018-12-30 11:18:06 +00:00
return text("I am put method")
2017-02-02 17:21:14 +00:00
def patch(self, request):
2018-12-30 11:18:06 +00:00
return text("I am patch method")
2017-02-02 17:21:14 +00:00
def delete(self, request):
2018-12-30 11:18:06 +00:00
return text("I am delete method")
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
app.add_route(ViewTwo.as_view(), "/view_two")
2017-02-02 17:21:14 +00:00
return app
def test_methodview_naming(methodview_app):
2018-12-30 11:18:06 +00:00
viewone_url = methodview_app.url_for("ViewOne")
viewtwo_url = methodview_app.url_for("ViewTwo")
2017-02-02 17:21:14 +00:00
2018-12-30 11:18:06 +00:00
assert viewone_url == "/view_one"
assert viewtwo_url == "/view_two"
@pytest.mark.parametrize(
"path,version,expected",
(
("/foo", 1, "/v1/foo"),
("/foo", 1.1, "/v1.1/foo"),
("/foo", "1", "/v1/foo"),
("/foo", "1.1", "/v1.1/foo"),
("/foo", "1.0.1", "/v1.0.1/foo"),
("/foo", "v1.0.1", "/v1.0.1/foo"),
),
)
def test_versioning(app, path, version, expected):
@app.route(path, version=version)
def handler(*_):
...
url = app.url_for("handler")
assert url == expected