Show Tidal callback port (not Spotify's) in auth instructions

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.
This commit is contained in:
Broque Thomas 2026-04-30 20:54:06 -07:00
parent f3e312c2db
commit 1e5204a230
2 changed files with 164 additions and 3 deletions

View file

@ -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

View file

@ -5653,6 +5653,20 @@ def auth_tidal():
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'''
<html>
<head>
@ -5680,11 +5694,11 @@ def auth_tidal():
<p><a href="{auth_url}" target="_blank" style="font-size: 18px; color: #000000;">{auth_url}</a></p>
<hr>
<p><strong>Step 2:</strong> After authorizing, you'll see a blank page or an error. The URL will look like:</p>
<code>http://127.0.0.1:8888/tidal/callback?code=...</code>
<code>http://127.0.0.1:{tidal_port}/tidal/callback?code=...</code>
<p><strong>Step 3:</strong> Change <code style="display: inline; background: #ffe6e6; padding: 2px 6px;">127.0.0.1</code> to <code style="display: inline; background: #e8f5e9; padding: 2px 6px;">{host}</code> and press Enter:
<button class="copy-btn" onclick="copyIP()">Copy IP</button>
</p>
<code class="highlight">http://{host}:8888/tidal/callback?code=...</code>
<code class="highlight">http://{host}:{tidal_port}/tidal/callback?code=...</code>
<p>Authentication will then complete!</p>
<script>