Skip to content

Commit 92c92d5

Browse files
committed
fix(config): resolve incorrect DaisyUI version detection
- Fixed redirect handling in fetch_redirect_location to prevent automatic following - Added NoRedirectHandler to capture redirect responses without following them - Updated HTTP tests to mock build_opener instead of urlopen directly - Made DaisyUI-related tests version-agnostic for better maintainability - Tests now focus on behavior rather than specific version numbers
1 parent 65f45f5 commit 92c92d5

File tree

4 files changed

+127
-12
lines changed

4 files changed

+127
-12
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
### 🐛 Bug Fixes
6+
- **DaisyUI version detection**: Fixed incorrect version fetching when `TAILWIND_CLI_USE_DAISY_UI = True` enabled
7+
58
## 4.4.0 (2025-09-21)
69

710
### 🔧 Technical Improvements

src/django_tailwind_cli/utils/http.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from collections.abc import Callable
99
from urllib.error import HTTPError as UrllibHTTPError
1010
from urllib.error import URLError
11-
from urllib.request import Request, urlopen
11+
from urllib.request import Request, urlopen, HTTPRedirectHandler, build_opener
12+
from typing import IO
13+
from http.client import HTTPMessage
1214

1315
if TYPE_CHECKING:
1416
pass
@@ -30,6 +32,30 @@ class RequestTimeoutError(RequestError):
3032
"""Request timeout error."""
3133

3234

35+
class NoRedirectHandler(HTTPRedirectHandler):
36+
"""HTTP redirect handler that captures redirect information without following."""
37+
38+
def http_error_302(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> IO[bytes]: # noqa: ARG002
39+
"""Handle 302 Found redirects."""
40+
return fp
41+
42+
def http_error_301(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> IO[bytes]: # noqa: ARG002
43+
"""Handle 301 Moved Permanently redirects."""
44+
return fp
45+
46+
def http_error_303(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> IO[bytes]: # noqa: ARG002
47+
"""Handle 303 See Other redirects."""
48+
return fp
49+
50+
def http_error_307(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> IO[bytes]: # noqa: ARG002
51+
"""Handle 307 Temporary Redirect redirects."""
52+
return fp
53+
54+
def http_error_308(self, req: Request, fp: IO[bytes], code: int, msg: str, headers: HTTPMessage) -> IO[bytes]: # noqa: ARG002
55+
"""Handle 308 Permanent Redirect redirects."""
56+
return fp
57+
58+
3359
def fetch_redirect_location(url: str, timeout: int = 10) -> tuple[bool, str | None]:
3460
"""Fetch redirect location from a URL.
3561
@@ -44,11 +70,14 @@ def fetch_redirect_location(url: str, timeout: int = 10) -> tuple[bool, str | No
4470
RequestError: On network or HTTP errors
4571
"""
4672
try:
73+
# Create opener with no redirect handler to capture redirect responses
74+
opener = build_opener(NoRedirectHandler)
75+
4776
req = Request(url)
4877
# Set User-Agent to avoid blocking
4978
req.add_header("User-Agent", "django-tailwind-cli")
5079

51-
with urlopen(req, timeout=timeout) as response:
80+
with opener.open(req, timeout=timeout) as response:
5281
# Check if it's a redirect status
5382
if response.getcode() in (301, 302, 303, 307, 308):
5483
location = response.headers.get("Location")

tests/test_config.py

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,72 @@ def test_get_version_with_official_repo_and_version_3(settings: SettingsWrapper)
9797
get_version()
9898

9999

100+
def test_get_version_with_daisyui_enabled_latest(settings: SettingsWrapper, mocker: MockerFixture):
101+
"""Test that DaisyUI uses the correct repository and correctly parses version."""
102+
# Clear any existing cache
103+
from django_tailwind_cli.config import _get_cache_path
104+
from semver import Version
105+
106+
cache_path = _get_cache_path()
107+
if cache_path.exists():
108+
cache_path.unlink()
109+
110+
settings.TAILWIND_CLI_USE_DAISY_UI = True
111+
settings.TAILWIND_CLI_VERSION = "latest"
112+
113+
# Mock successful redirect to a generic valid DaisyUI version
114+
test_version = "9.8.7"
115+
request_get = mocker.patch("django_tailwind_cli.utils.http.fetch_redirect_location")
116+
request_get.return_value = (
117+
True,
118+
f"https://github.com/dobicinaitis/tailwind-cli-extra/releases/tag/v{test_version}",
119+
)
120+
121+
r_version_str, r_version = get_version()
122+
123+
# Test that version string is correctly extracted (without 'v' prefix)
124+
assert r_version_str == test_version
125+
126+
# Test that version is correctly parsed as semantic version
127+
assert isinstance(r_version, Version)
128+
assert str(r_version) == test_version
129+
130+
# Verify the correct DaisyUI repository URL was used (not standard Tailwind)
131+
request_get.assert_called_once_with(
132+
"https://github.com/dobicinaitis/tailwind-cli-extra/releases/latest/", timeout=10
133+
)
134+
135+
136+
def test_get_version_with_daisyui_fallback_when_network_fails(settings: SettingsWrapper, mocker: MockerFixture):
137+
"""Test fallback behavior when DaisyUI is enabled but network request fails."""
138+
# Clear any existing cache
139+
from django_tailwind_cli.config import _get_cache_path, FALLBACK_VERSION
140+
from semver import Version
141+
142+
cache_path = _get_cache_path()
143+
if cache_path.exists():
144+
cache_path.unlink()
145+
146+
settings.TAILWIND_CLI_USE_DAISY_UI = True
147+
settings.TAILWIND_CLI_VERSION = "latest"
148+
149+
# Mock failed network request
150+
request_get = mocker.patch("django_tailwind_cli.utils.http.fetch_redirect_location")
151+
request_get.return_value = (False, None)
152+
153+
r_version_str, r_version = get_version()
154+
155+
# Should fall back to the configured fallback version
156+
assert r_version_str == FALLBACK_VERSION
157+
assert isinstance(r_version, Version)
158+
assert str(r_version) == FALLBACK_VERSION
159+
160+
# Verify the correct DaisyUI repository URL was still used in the attempt
161+
request_get.assert_called_once_with(
162+
"https://github.com/dobicinaitis/tailwind-cli-extra/releases/latest/", timeout=10
163+
)
164+
165+
100166
def test_get_version_with_unofficial_repo_and_version_3(settings: SettingsWrapper):
101167
settings.TAILWIND_CLI_VERSION = "3.4.13"
102168
settings.TAILWIND_CLI_SRC_REPO = "oliverandrich/my-tailwindcss-cli"
@@ -296,18 +362,25 @@ def test_daisy_ui_support(
296362
settings: SettingsWrapper,
297363
mocker: MockerFixture,
298364
):
365+
from semver import Version
366+
299367
settings.TAILWIND_CLI_USE_DAISY_UI = True
368+
test_version = "7.6.5"
300369
request_get = mocker.patch("django_tailwind_cli.utils.http.fetch_redirect_location")
301-
request_get.return_value = (True, "https://github.com/dobicinaitis/tailwind-cli-extra/releases/tag/v2.1.4")
370+
request_get.return_value = (
371+
True,
372+
f"https://github.com/dobicinaitis/tailwind-cli-extra/releases/tag/v{test_version}",
373+
)
302374

303375
c = get_config()
304376

377+
# Test DaisyUI configuration is properly applied
305378
assert c.use_daisy_ui
306379
assert "tailwindcss-extra" in str(c.cli_path)
307380
assert "dobicinaitis/tailwind-cli-extra" in c.download_url
308381

382+
# Test version parsing works correctly
309383
r_version_str, r_version = get_version()
310-
assert r_version_str == "2.1.4"
311-
assert r_version.major == 2
312-
assert r_version.minor == 1
313-
assert r_version.patch == 4
384+
assert r_version_str == test_version
385+
assert isinstance(r_version, Version)
386+
assert str(r_version) == test_version

tests/test_http.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,45 @@ def test_fetch_redirect_location_timeout_error(self):
2121
"""Test timeout error handling."""
2222
mock_error = URLError(socket.timeout("timeout"))
2323

24-
with patch("django_tailwind_cli.utils.http.urlopen", side_effect=mock_error):
24+
with patch("django_tailwind_cli.utils.http.build_opener") as mock_build_opener:
25+
mock_opener = mock_build_opener.return_value
26+
mock_opener.open.side_effect = mock_error
2527
with pytest.raises(http.RequestTimeoutError, match="Request timeout"):
2628
http.fetch_redirect_location("https://example.com")
2729

2830
def test_fetch_redirect_location_connection_error(self):
2931
"""Test connection error handling."""
3032
mock_error = URLError(ConnectionRefusedError("connection refused"))
3133

32-
with patch("django_tailwind_cli.utils.http.urlopen", side_effect=mock_error):
34+
with patch("django_tailwind_cli.utils.http.build_opener") as mock_build_opener:
35+
mock_opener = mock_build_opener.return_value
36+
mock_opener.open.side_effect = mock_error
3337
with pytest.raises(http.NetworkConnectionError, match="Connection error"):
3438
http.fetch_redirect_location("https://example.com")
3539

3640
def test_fetch_redirect_location_generic_error(self):
3741
"""Test generic URL error handling."""
3842
mock_error = URLError("generic error")
3943

40-
with patch("django_tailwind_cli.utils.http.urlopen", side_effect=mock_error):
44+
with patch("django_tailwind_cli.utils.http.build_opener") as mock_build_opener:
45+
mock_opener = mock_build_opener.return_value
46+
mock_opener.open.side_effect = mock_error
4147
with pytest.raises(http.RequestError, match="URL error"):
4248
http.fetch_redirect_location("https://example.com")
4349

4450
def test_fetch_redirect_location_timeout_error_direct(self):
4551
"""Test direct timeout error handling."""
46-
with patch("django_tailwind_cli.utils.http.urlopen", side_effect=TimeoutError("timeout")):
52+
with patch("django_tailwind_cli.utils.http.build_opener") as mock_build_opener:
53+
mock_opener = mock_build_opener.return_value
54+
mock_opener.open.side_effect = TimeoutError("timeout")
4755
with pytest.raises(http.RequestTimeoutError, match="Socket timeout"):
4856
http.fetch_redirect_location("https://example.com")
4957

5058
def test_fetch_redirect_location_generic_exception(self):
5159
"""Test generic exception handling."""
52-
with patch("django_tailwind_cli.utils.http.urlopen", side_effect=ValueError("unexpected")):
60+
with patch("django_tailwind_cli.utils.http.build_opener") as mock_build_opener:
61+
mock_opener = mock_build_opener.return_value
62+
mock_opener.open.side_effect = ValueError("unexpected")
5363
with pytest.raises(http.RequestError, match="Unexpected error"):
5464
http.fetch_redirect_location("https://example.com")
5565

0 commit comments

Comments
 (0)