Skip to content

Commit 3c95321

Browse files
namsnathgaurpulkit
andcommitted
update: makes url path normalization case sensitive
- Updates `normalise_url_path_or_throw_error` to be case sensitive - URL paths will not be converted to lower-case, and will be kept as-is. Co-authored-by: Pulkit Gaur <[email protected]>
1 parent d00c0a6 commit 3c95321

File tree

4 files changed

+111
-113
lines changed

4 files changed

+111
-113
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88

99
## [unreleased]
10+
11+
## [0.29.0] - 2025-02-17
12+
### Breaking changes
13+
- Makes URL path normalization case sensitive
14+
- Updates `normalise_url_path_or_throw_error` to be case sensitive
15+
- URL paths will not be converted to lower-case, and will be kept as-is.
16+
17+
### Changes
1018
- Upgrades `pip` and `setuptools` in CI publish job
11-
- Also upgrades `poetry` and it's dependency - `clikit`
19+
- Also upgrades `poetry` and it's dependency - `clikit`
1220

1321
## [0.29.0] - 2025-03-03
1422
- Migrates unit tests to use a containerized core

supertokens_python/normalised_url_path.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,35 @@ def is_a_recipe_path(self) -> bool:
4545

4646

4747
def normalise_url_path_or_throw_error(input_str: str) -> str:
48-
input_str = input_str.strip().lower()
48+
input_str = input_str.strip()
49+
input_str_lower = input_str.lower()
50+
4951
try:
50-
if not input_str.startswith("http://") and not input_str.startswith("https://"):
52+
if not input_str_lower.startswith(("http://", "https://")):
5153
raise Exception("converting to proper URL")
54+
5255
url_obj = urlparse(input_str)
53-
input_str = url_obj.path
54-
if input_str.endswith("/"):
55-
return input_str[:-1]
56-
return input_str
56+
url_path = url_obj.path
57+
58+
if url_path.endswith("/"):
59+
return url_path[:-1]
60+
61+
return url_path
5762
except Exception:
5863
pass
5964

6065
if (
61-
(domain_given(input_str) or input_str.startswith("localhost"))
62-
and not input_str.startswith("http://")
63-
and not input_str.startswith("https://")
64-
):
66+
domain_given(input_str_lower) or input_str_lower.startswith("localhost")
67+
) and not input_str_lower.startswith(("http://", "https://")):
6568
input_str = "http://" + input_str
6669
return normalise_url_path_or_throw_error(input_str)
6770

6871
if not input_str.startswith("/"):
6972
input_str = "/" + input_str
7073

7174
try:
72-
urlparse("http://example.com" + input_str)
73-
return normalise_url_path_or_throw_error("http://example.com" + input_str)
75+
urlparse(f"http://example.com{input_str}")
76+
return normalise_url_path_or_throw_error(f"http://example.com{input_str}")
7477
except Exception:
7578
raise_general_exception("Please provide a valid URL path")
7679

tests/test_config.py

Lines changed: 64 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14-
from typing import Any, Dict, Optional
1514
from unittest.mock import MagicMock
1615

1716
import pytest
@@ -24,8 +23,9 @@
2423
from supertokens_python.recipe.session import SessionRecipe
2524
from supertokens_python.recipe.session.asyncio import create_new_session
2625
from supertokens_python.types import RecipeUserId
26+
from typing_extensions import Any, Dict, Optional
2727

28-
from tests.utils import get_new_core_app_url, reset
28+
from tests.utils import get_new_core_app_url, outputs, reset
2929

3030

3131
# Tests do not rely on the core.
@@ -35,109 +35,73 @@ def st_config() -> SupertokensConfig:
3535
return SupertokensConfig(get_new_core_app_url())
3636

3737

38-
def testing_URL_path_normalisation():
38+
@mark.parametrize(
39+
("input", "expectation"),
40+
[
41+
("exists?email=john.doe%40gmail.com", outputs("/exists")),
42+
(
43+
"/auth/email/exists?email=john.doe%40gmail.com",
44+
outputs("/auth/email/exists"),
45+
),
46+
("http://api.example.com", outputs("")),
47+
("https://api.example.com", outputs("")),
48+
("http://api.example.com?hello=1", outputs("")),
49+
("http://api.example.com/hello", outputs("/hello")),
50+
("http://api.example.com/HellO", outputs("/HellO")),
51+
("http://api.example.com/", outputs("")),
52+
("http://api.example.com:8080", outputs("")),
53+
("api.example.com/", outputs("")),
54+
("api.example.com#random", outputs("")),
55+
(".example.com", outputs("")),
56+
("api.example.com/?hello=1&bye=2", outputs("")),
57+
("exists", outputs("/exists")),
58+
("eXiStS", outputs("/eXiStS")),
59+
("/exists", outputs("/exists")),
60+
("/eXiStS", outputs("/eXiStS")),
61+
("/exists?email=john.doe%40gmail.com", outputs("/exists")),
62+
("http://api.example.com/one/two", outputs("/one/two")),
63+
("http://1.2.3.4/one/two", outputs("/one/two")),
64+
("1.2.3.4/one/two", outputs("/one/two")),
65+
("https://api.example.com/one/two/", outputs("/one/two")),
66+
("http://api.example.com/one/two?hello=1", outputs("/one/two")),
67+
("http://api.example.com/hello/", outputs("/hello")),
68+
("http://api.example.com/one/two/", outputs("/one/two")),
69+
("http://api.example.com/one/two#random2", outputs("/one/two")),
70+
("api.example.com/one/two", outputs("/one/two")),
71+
(".example.com/one/two", outputs("/one/two")),
72+
("api.example.com/one/two?hello=1&bye=2", outputs("/one/two")),
73+
("/one/two", outputs("/one/two")),
74+
("one/two", outputs("/one/two")),
75+
("one/two/", outputs("/one/two")),
76+
("/one", outputs("/one")),
77+
("one", outputs("/one")),
78+
("one/", outputs("/one")),
79+
("/one/two/", outputs("/one/two")),
80+
("/one/two?hello=1", outputs("/one/two")),
81+
("one/two?hello=1", outputs("/one/two")),
82+
("/one/two/#randm,", outputs("/one/two")),
83+
("one/two#random", outputs("/one/two")),
84+
("localhost:4000/one/two", outputs("/one/two")),
85+
("127.0.0.1:4000/one/two", outputs("/one/two")),
86+
("127.0.0.1/one/two", outputs("/one/two")),
87+
("https://127.0.0.1:80/one/two", outputs("/one/two")),
88+
("/", outputs("")),
89+
("", outputs("")),
90+
("/.netlify/functions/api", outputs("/.netlify/functions/api")),
91+
("/netlify/.functions/api", outputs("/netlify/.functions/api")),
92+
("app.example.com/.netlify/functions/api", outputs("/.netlify/functions/api")),
93+
("app.example.com/netlify/.functions/api", outputs("/netlify/.functions/api")),
94+
("/app.example.com", outputs("/app.example.com")),
95+
],
96+
)
97+
def testing_URL_path_normalisation(input: str, expectation: Any) -> None:
3998
def normalise_url_path_or_throw_error(
4099
input: str,
41100
): # pylint: disable=redefined-builtin
42101
return NormalisedURLPath(input).get_as_string_dangerous()
43102

44-
assert (
45-
normalise_url_path_or_throw_error("exists?email=john.doe%40gmail.com")
46-
== "/exists"
47-
)
48-
assert (
49-
normalise_url_path_or_throw_error(
50-
"/auth/email/exists?email=john.doe%40gmail.com"
51-
)
52-
== "/auth/email/exists"
53-
)
54-
assert normalise_url_path_or_throw_error("exists") == "/exists"
55-
assert normalise_url_path_or_throw_error("/exists") == "/exists"
56-
assert (
57-
normalise_url_path_or_throw_error("/exists?email=john.doe%40gmail.com")
58-
== "/exists"
59-
)
60-
assert normalise_url_path_or_throw_error("http://api.example.com") == ""
61-
assert normalise_url_path_or_throw_error("https://api.example.com") == ""
62-
assert normalise_url_path_or_throw_error("http://api.example.com?hello=1") == ""
63-
assert normalise_url_path_or_throw_error("http://api.example.com/hello") == "/hello"
64-
assert normalise_url_path_or_throw_error("http://api.example.com/") == ""
65-
assert normalise_url_path_or_throw_error("http://api.example.com:8080") == ""
66-
assert normalise_url_path_or_throw_error("api.example.com/") == ""
67-
assert normalise_url_path_or_throw_error("api.example.com#random") == ""
68-
assert normalise_url_path_or_throw_error(".example.com") == ""
69-
assert normalise_url_path_or_throw_error("api.example.com/?hello=1&bye=2") == ""
70-
71-
assert (
72-
normalise_url_path_or_throw_error("http://api.example.com/one/two")
73-
== "/one/two"
74-
)
75-
assert normalise_url_path_or_throw_error("http://1.2.3.4/one/two") == "/one/two"
76-
assert normalise_url_path_or_throw_error("1.2.3.4/one/two") == "/one/two"
77-
assert (
78-
normalise_url_path_or_throw_error("https://api.example.com/one/two/")
79-
== "/one/two"
80-
)
81-
assert (
82-
normalise_url_path_or_throw_error("http://api.example.com/one/two?hello=1")
83-
== "/one/two"
84-
)
85-
assert (
86-
normalise_url_path_or_throw_error("http://api.example.com/hello/") == "/hello"
87-
)
88-
assert (
89-
normalise_url_path_or_throw_error("http://api.example.com/one/two/")
90-
== "/one/two"
91-
)
92-
assert (
93-
normalise_url_path_or_throw_error("http://api.example.com/one/two#random2")
94-
== "/one/two"
95-
)
96-
assert normalise_url_path_or_throw_error("api.example.com/one/two") == "/one/two"
97-
assert normalise_url_path_or_throw_error(".example.com/one/two") == "/one/two"
98-
assert (
99-
normalise_url_path_or_throw_error("api.example.com/one/two?hello=1&bye=2")
100-
== "/one/two"
101-
)
102-
103-
assert normalise_url_path_or_throw_error("/one/two") == "/one/two"
104-
assert normalise_url_path_or_throw_error("one/two") == "/one/two"
105-
assert normalise_url_path_or_throw_error("one/two/") == "/one/two"
106-
assert normalise_url_path_or_throw_error("/one") == "/one"
107-
assert normalise_url_path_or_throw_error("one") == "/one"
108-
assert normalise_url_path_or_throw_error("one/") == "/one"
109-
assert normalise_url_path_or_throw_error("/one/two/") == "/one/two"
110-
assert normalise_url_path_or_throw_error("/one/two?hello=1") == "/one/two"
111-
assert normalise_url_path_or_throw_error("one/two?hello=1") == "/one/two"
112-
assert normalise_url_path_or_throw_error("/one/two/#randm,") == "/one/two"
113-
assert normalise_url_path_or_throw_error("one/two#random") == "/one/two"
114-
115-
assert normalise_url_path_or_throw_error("localhost:4000/one/two") == "/one/two"
116-
assert normalise_url_path_or_throw_error("127.0.0.1:4000/one/two") == "/one/two"
117-
assert normalise_url_path_or_throw_error("127.0.0.1/one/two") == "/one/two"
118-
assert (
119-
normalise_url_path_or_throw_error("https://127.0.0.1:80/one/two") == "/one/two"
120-
)
121-
assert normalise_url_path_or_throw_error("/") == ""
122-
assert normalise_url_path_or_throw_error("") == ""
123-
124-
assert (
125-
normalise_url_path_or_throw_error("/.netlify/functions/api")
126-
== "/.netlify/functions/api"
127-
)
128-
assert (
129-
normalise_url_path_or_throw_error("/netlify/.functions/api")
130-
== "/netlify/.functions/api"
131-
)
132-
assert (
133-
normalise_url_path_or_throw_error("app.example.com/.netlify/functions/api")
134-
== "/.netlify/functions/api"
135-
)
136-
assert (
137-
normalise_url_path_or_throw_error("app.example.com/netlify/.functions/api")
138-
== "/netlify/.functions/api"
139-
)
140-
assert normalise_url_path_or_throw_error("/app.example.com") == "/app.example.com"
103+
with expectation as output:
104+
assert normalise_url_path_or_throw_error(input) == output
141105

142106

143107
def testing_URL_domain_normalisation():

tests/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
# Import AsyncMock
1818
import sys
19+
from contextlib import contextmanager
1920
from datetime import datetime
2021
from functools import lru_cache
2122
from http.cookies import SimpleCookie
@@ -487,3 +488,25 @@ async def create_users(
487488
await manually_create_or_update_user(
488489
"public", user["provider"], user["userId"], user["email"], True, None
489490
)
491+
492+
493+
@contextmanager
494+
def outputs(value: Any):
495+
"""
496+
Context manager that can be used for a common interface in test parameters.
497+
498+
Example:
499+
```python
500+
@mark.parametrize(
501+
("input", "expectation"),
502+
[
503+
(1, outputs(1)),
504+
(0, raises(Exception)),
505+
]
506+
)
507+
def test(input, expectation):
508+
with expectation as output:
509+
assert 1 / input == output
510+
```
511+
"""
512+
yield value

0 commit comments

Comments
 (0)