Skip to content

Commit 1fcd119

Browse files
Fix #636 (#637)
1 parent 9c6d06b commit 1fcd119

File tree

4 files changed

+171
-2
lines changed

4 files changed

+171
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.4.5] - 2025-11-15 :mount_fuji:
9+
10+
- Fix [#636](https://github.com/Neoteroi/BlackSheep/issues/636).
11+
812
## [2.4.4] - 2025-11-15 :mount_fuji:
913

1014
- Introduce `MiddlewareList` and `MiddlewareCategory` to simplify middleware management

blacksheep/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
__author__ = "Roberto Prevato <roberto.prevato@gmail.com>"
7-
__version__ = "2.4.4"
7+
__version__ = "2.4.5"
88

99
from .contents import Content as Content
1010
from .contents import FormContent as FormContent

blacksheep/server/redirects.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def default_trailing_slash_exclude(path: str) -> bool:
99

1010

1111
def get_trailing_slash_middleware(
12-
exclude: Callable[[str | None, bool]] = None,
12+
exclude: Callable[[str], bool] | None = None,
1313
) -> Callable[..., Awaitable[Response]]:
1414
"""
1515
Returns a middleware that redirects requests that do not end with a trailing slash

tests/test_redirects.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import pytest
2+
3+
from blacksheep.messages import Request
4+
from blacksheep.server.redirects import (
5+
default_trailing_slash_exclude,
6+
get_trailing_slash_middleware,
7+
)
8+
from blacksheep.server.responses import text
9+
10+
11+
class TestDefaultTrailingSlashExclude:
12+
def test_excludes_api_paths(self):
13+
assert default_trailing_slash_exclude("/api/users") is True
14+
assert default_trailing_slash_exclude("/api/") is True
15+
assert default_trailing_slash_exclude("/v1/api/endpoint") is True
16+
17+
def test_does_not_exclude_non_api_paths(self):
18+
assert default_trailing_slash_exclude("/home") is False
19+
assert default_trailing_slash_exclude("/about") is False
20+
assert default_trailing_slash_exclude("/") is False
21+
22+
23+
class TestTrailingSlashMiddleware:
24+
@pytest.mark.asyncio
25+
async def test_redirects_path_without_trailing_slash(self):
26+
middleware = get_trailing_slash_middleware()
27+
request = Request("GET", b"/home", [])
28+
29+
async def handler(req):
30+
return text("OK")
31+
32+
response = await middleware(request, handler)
33+
34+
assert response.status == 301
35+
assert response.headers.get_single(b"Location") == b"/home/"
36+
37+
@pytest.mark.asyncio
38+
async def test_does_not_redirect_path_with_trailing_slash(self):
39+
middleware = get_trailing_slash_middleware()
40+
request = Request("GET", b"/home/", [])
41+
42+
async def handler(req):
43+
return text("OK")
44+
45+
response = await middleware(request, handler)
46+
47+
assert response.status == 200
48+
49+
@pytest.mark.asyncio
50+
async def test_does_not_redirect_paths_with_file_extensions(self):
51+
middleware = get_trailing_slash_middleware()
52+
request = Request("GET", b"/style.css", [])
53+
54+
async def handler(req):
55+
return text("OK")
56+
57+
response = await middleware(request, handler)
58+
59+
assert response.status == 200
60+
61+
@pytest.mark.asyncio
62+
async def test_does_not_redirect_paths_with_file_extensions_in_subdirs(self):
63+
middleware = get_trailing_slash_middleware()
64+
request = Request("GET", b"/assets/script.js", [])
65+
66+
async def handler(req):
67+
return text("OK")
68+
69+
response = await middleware(request, handler)
70+
71+
assert response.status == 200
72+
73+
@pytest.mark.asyncio
74+
async def test_excludes_api_paths_by_default(self):
75+
middleware = get_trailing_slash_middleware()
76+
request = Request("GET", b"/api/users", [])
77+
78+
async def handler(req):
79+
return text("OK")
80+
81+
response = await middleware(request, handler)
82+
83+
assert response.status == 200
84+
85+
@pytest.mark.asyncio
86+
async def test_custom_exclude_function(self):
87+
def custom_exclude(path: str) -> bool:
88+
return path.startswith("/admin")
89+
90+
middleware = get_trailing_slash_middleware(exclude=custom_exclude)
91+
request = Request("GET", b"/admin/dashboard", [])
92+
93+
async def handler(req):
94+
return text("OK")
95+
96+
response = await middleware(request, handler)
97+
98+
assert response.status == 200
99+
100+
@pytest.mark.asyncio
101+
async def test_custom_exclude_does_not_affect_other_paths(self):
102+
def custom_exclude(path: str) -> bool:
103+
return path.startswith("/admin")
104+
105+
middleware = get_trailing_slash_middleware(exclude=custom_exclude)
106+
request = Request("GET", b"/home", [])
107+
108+
async def handler(req):
109+
return text("OK")
110+
111+
response = await middleware(request, handler)
112+
113+
assert response.status == 301
114+
assert response.headers.get_single(b"Location") == b"/home/"
115+
116+
@pytest.mark.asyncio
117+
async def test_none_exclude_disables_exclusion(self):
118+
middleware = get_trailing_slash_middleware(exclude=lambda x: False)
119+
request = Request("GET", b"/api/users", [])
120+
121+
async def handler(req):
122+
return text("OK")
123+
124+
response = await middleware(request, handler)
125+
126+
assert response.status == 301
127+
assert response.headers.get_single(b"Location") == b"/api/users/"
128+
129+
@pytest.mark.asyncio
130+
async def test_handles_root_path(self):
131+
middleware = get_trailing_slash_middleware()
132+
request = Request("GET", b"/", [])
133+
134+
async def handler(req):
135+
return text("OK")
136+
137+
response = await middleware(request, handler)
138+
139+
assert response.status == 200
140+
141+
@pytest.mark.asyncio
142+
async def test_normalizes_path_with_leading_slashes(self):
143+
middleware = get_trailing_slash_middleware()
144+
request = Request("GET", b"/home", [])
145+
146+
async def handler(req):
147+
return text("OK")
148+
149+
response = await middleware(request, handler)
150+
151+
assert response.status == 301
152+
assert response.headers.get_single(b"Location") == b"/home/"
153+
154+
@pytest.mark.asyncio
155+
async def test_nested_paths_without_trailing_slash(self):
156+
middleware = get_trailing_slash_middleware()
157+
request = Request("GET", b"/about/team", [])
158+
159+
async def handler(req):
160+
return text("OK")
161+
162+
response = await middleware(request, handler)
163+
164+
assert response.status == 301
165+
assert response.headers.get_single(b"Location") == b"/about/team/"

0 commit comments

Comments
 (0)