|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from typing import Any |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +from isaacus._utils._path import path_template |
| 8 | + |
| 9 | + |
| 10 | +@pytest.mark.parametrize( |
| 11 | + "template, kwargs, expected", |
| 12 | + [ |
| 13 | + ("/v1/{id}", dict(id="abc"), "/v1/abc"), |
| 14 | + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), |
| 15 | + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), |
| 16 | + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), |
| 17 | + ("/v1/static", {}, "/v1/static"), |
| 18 | + ("", {}, ""), |
| 19 | + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), |
| 20 | + ("/v1/{v}", dict(v=None), "/v1/null"), |
| 21 | + ("/v1/{v}", dict(v=True), "/v1/true"), |
| 22 | + ("/v1/{v}", dict(v=False), "/v1/false"), |
| 23 | + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok |
| 24 | + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok |
| 25 | + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok |
| 26 | + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok |
| 27 | + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine |
| 28 | + ( |
| 29 | + "/v1/{a}?query={b}", |
| 30 | + dict(a="../../other/endpoint", b="a&bad=true"), |
| 31 | + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", |
| 32 | + ), |
| 33 | + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), |
| 34 | + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), |
| 35 | + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), |
| 36 | + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input |
| 37 | + # Query: slash and ? are safe, # is not |
| 38 | + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), |
| 39 | + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), |
| 40 | + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), |
| 41 | + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), |
| 42 | + # Fragment: slash and ? are safe |
| 43 | + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), |
| 44 | + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), |
| 45 | + # Path: slash, ? and # are all encoded |
| 46 | + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), |
| 47 | + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), |
| 48 | + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), |
| 49 | + # same var encoded differently by component |
| 50 | + ( |
| 51 | + "/v1/{v}?q={v}#{v}", |
| 52 | + dict(v="a/b?c#d"), |
| 53 | + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", |
| 54 | + ), |
| 55 | + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection |
| 56 | + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection |
| 57 | + ], |
| 58 | +) |
| 59 | +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: |
| 60 | + assert path_template(template, **kwargs) == expected |
| 61 | + |
| 62 | + |
| 63 | +def test_missing_kwarg_raises_key_error() -> None: |
| 64 | + with pytest.raises(KeyError, match="org_id"): |
| 65 | + path_template("/v1/{org_id}") |
| 66 | + |
| 67 | + |
| 68 | +@pytest.mark.parametrize( |
| 69 | + "template, kwargs", |
| 70 | + [ |
| 71 | + ("{a}/path", dict(a=".")), |
| 72 | + ("{a}/path", dict(a="..")), |
| 73 | + ("/v1/{a}", dict(a=".")), |
| 74 | + ("/v1/{a}", dict(a="..")), |
| 75 | + ("/v1/{a}/path", dict(a=".")), |
| 76 | + ("/v1/{a}/path", dict(a="..")), |
| 77 | + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." |
| 78 | + ("/v1/{a}.", dict(a=".")), # var + static → ".." |
| 79 | + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." |
| 80 | + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text |
| 81 | + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static |
| 82 | + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static |
| 83 | + ("/v1/{v}?q=1", dict(v="..")), |
| 84 | + ("/v1/{v}#frag", dict(v="..")), |
| 85 | + ], |
| 86 | +) |
| 87 | +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: |
| 88 | + with pytest.raises(ValueError, match="dot-segment"): |
| 89 | + path_template(template, **kwargs) |
0 commit comments