From 1e5204a230a6739cff0a41f654e47589a17e5d96 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:54:06 -0700 Subject: [PATCH] Show Tidal callback port (not Spotify's) in auth instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discord-reported: clicking the Tidal "Authenticate" button on a Docker setup landed users on a remote-access instructions page that told them their callback URL would look like http://127.0.0.1:8888/tidal/callback?code=... — Spotify's port, hardcoded into the Tidal instructions. Users who followed those instructions literally saved 8888 into their tidal.redirect_uri setting; that mismatched their Tidal Developer App's registered :8889 redirect URI and Tidal returned error 1002 (invalid redirect URI) on every auth attempt. Pull the port from the actual TidalClient.redirect_uri the OAuth URL was just built with (urlparse), with the SOULSYNC_TIDAL_CALLBACK_PORT env var as fallback when the URI can't be parsed. Both the Step 2 example and the Step 3 highlighted URL now reflect whatever Tidal port the user is actually configured to use. Adds 3 regression tests covering the reported scenario, custom callback ports via SOULSYNC_TIDAL_CALLBACK_PORT, and a defensive fallback when redirect_uri is unparseable. Tests hit the real /auth/tidal route through Flask's test client and assert the rendered HTML, so future hardcoded ports get caught immediately. --- tests/test_tidal_auth_instructions.py | 147 ++++++++++++++++++++++++++ web_server.py | 20 +++- 2 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 tests/test_tidal_auth_instructions.py diff --git a/tests/test_tidal_auth_instructions.py b/tests/test_tidal_auth_instructions.py new file mode 100644 index 00000000..f82c59da --- /dev/null +++ b/tests/test_tidal_auth_instructions.py @@ -0,0 +1,147 @@ +"""Regression tests for Tidal auth instruction page port rendering. + +Discord-reported bug: the auth-instructions page shown after clicking +the Tidal "Authenticate" button rendered example callback URLs with +port ``8888`` (Spotify's port) instead of ``8889`` (Tidal's port). +Users who followed the instructions literally saved Spotify's port +into their ``tidal.redirect_uri`` setting; that mismatched their +Tidal Developer App's registered ``:8889`` redirect URI and Tidal +returned error 1002 (invalid redirect URI) on every auth attempt. + +These tests make sure the rendered instructions show whatever port +the OAuth URL itself was built with, so the displayed example always +matches what the user must register in their Tidal app. +""" + +from typing import Callable +from unittest.mock import MagicMock, patch + +import pytest + + +# Run the route through Flask's test client so we get the real HTML +# the user would see. We patch out: +# - TidalClient (the real client tries to connect to Tidal), +# - the activity-feed call (writes to runtime state), +# - request.host detection (so the Docker code path is exercised +# and the instructions page is the one with the example URL). +@pytest.fixture +def auth_route_client(monkeypatch: pytest.MonkeyPatch): + """Return a Flask test client wired up enough to render the + Tidal auth-instructions page.""" + # Force the "remote/docker" branch by faking a remote-host request. + # Easier than mocking is_docker; the route only needs ONE of the + # two flags to render the instructions page. + monkeypatch.setattr( + "os.path.exists", + lambda p: p == "/.dockerenv" or False, + ) + + fake_client = MagicMock() + fake_client.client_id = "fake-id" + fake_client.code_verifier = "v" * 40 + fake_client.code_challenge = "c" * 40 + fake_client.auth_url = "https://login.tidal.com/authorize" + + def _set_redirect_uri(value): + fake_client.redirect_uri = value + fake_client._generate_pkce_challenge = MagicMock() + + with patch("core.tidal_client.TidalClient", return_value=fake_client): + with patch("web_server.add_activity_item"): + from web_server import app as flask_app + flask_app.config['TESTING'] = True + yield flask_app.test_client(), fake_client + + +def _extract_html(response) -> str: + return response.get_data(as_text=True) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_instructions_show_tidal_port_not_spotify_port_when_config_uses_8889( + auth_route_client, monkeypatch: pytest.MonkeyPatch, +) -> None: + """The reported scenario: tidal.redirect_uri config carries port + 8889, the rendered instructions must show 8889 (not Spotify's + 8888) in both the Step 2 example and the Step 3 highlighted URL.""" + client, fake_client = auth_route_client + + fake_client.redirect_uri = "http://127.0.0.1:8889/tidal/callback" + + from config.settings import config_manager + monkeypatch.setattr( + config_manager, "get", + lambda key, default=None: ( + "http://127.0.0.1:8889/tidal/callback" + if key == "tidal.redirect_uri" else default + ), + ) + + response = client.get("/auth/tidal", base_url="http://192.168.1.50:8008") + html = _extract_html(response) + + # Both example URLs in the instructions must use Tidal's port. + assert ":8889/tidal/callback" in html, ( + "Step 2/3 example URLs must reflect the configured Tidal port" + ) + assert ":8888/tidal/callback" not in html, ( + "Spotify's port must not appear in Tidal auth instructions" + ) + + +def test_instructions_respect_custom_callback_port_from_env( + auth_route_client, monkeypatch: pytest.MonkeyPatch, +) -> None: + """SOULSYNC_TIDAL_CALLBACK_PORT env var changes which port the + Tidal callback server binds to; the instructions must reflect + that custom port too, not assume the 8889 default.""" + client, fake_client = auth_route_client + + fake_client.redirect_uri = "http://127.0.0.1:9999/tidal/callback" + + from config.settings import config_manager + monkeypatch.setattr( + config_manager, "get", + lambda key, default=None: ( + "http://127.0.0.1:9999/tidal/callback" + if key == "tidal.redirect_uri" else default + ), + ) + monkeypatch.setenv("SOULSYNC_TIDAL_CALLBACK_PORT", "9999") + + response = client.get("/auth/tidal", base_url="http://192.168.1.50:8008") + html = _extract_html(response) + + assert ":9999/tidal/callback" in html + + +def test_instructions_fall_back_to_default_port_when_redirect_uri_is_unparseable( + auth_route_client, monkeypatch: pytest.MonkeyPatch, +) -> None: + """Defensive: if redirect_uri somehow has no port (corrupted + config, schemeless string, etc.), the instructions fall back to + the default Tidal port from the env var instead of crashing or + showing the Spotify port.""" + client, fake_client = auth_route_client + + fake_client.redirect_uri = "not-a-valid-url" + + from config.settings import config_manager + monkeypatch.setattr( + config_manager, "get", + lambda key, default=None: ( + "not-a-valid-url" if key == "tidal.redirect_uri" else default + ), + ) + + response = client.get("/auth/tidal", base_url="http://192.168.1.50:8008") + html = _extract_html(response) + + # Falls back to Tidal default 8889, never to Spotify's 8888. + assert ":8889/tidal/callback" in html + assert ":8888/tidal/callback" not in html diff --git a/web_server.py b/web_server.py index 3f4b55b7..ace196fc 100644 --- a/web_server.py +++ b/web_server.py @@ -5652,7 +5652,21 @@ def auth_tidal(): # Show instructions for remote/docker access page_title = "Tidal Authentication (Remote/Docker)" step_1_text = "Click the link below to authenticate with Tidal" - + + # Pull the actual Tidal callback port from the same place the + # OAuth URL was built. Using a hardcoded 8888 here used to + # mislead users into saving Spotify's port into their Tidal + # redirect URI, which then gave Tidal error 1002 (invalid + # redirect URI) on every auth attempt. + try: + from urllib.parse import urlparse as _urlparse + _parsed = _urlparse(temp_tidal_client.redirect_uri) + tidal_port = _parsed.port or int( + os.environ.get('SOULSYNC_TIDAL_CALLBACK_PORT', 8889) + ) + except Exception: + tidal_port = int(os.environ.get('SOULSYNC_TIDAL_CALLBACK_PORT', 8889)) + return f'''
@@ -5680,11 +5694,11 @@ def auth_tidal():Step 2: After authorizing, you'll see a blank page or an error. The URL will look like:
-http://127.0.0.1:8888/tidal/callback?code=...
+ http://127.0.0.1:{tidal_port}/tidal/callback?code=...
Step 3: Change 127.0.0.1 to {host} and press Enter:
http://{host}:8888/tidal/callback?code=...
+ http://{host}:{tidal_port}/tidal/callback?code=...
Authentication will then complete!