test(linux): add credential-free remote-stream integration tests (#424) - #658
Merged
Merged
Conversation
The resolver suites and the Linux controller suites each stub the other half out, so nothing checked that a `jellyfin:` / `subsonic:` / `plex:` row actually becomes a URL a server accepts, or that the credential minted for it stays where it belongs. test/core/services/linux_remote_stream_integration_test.dart runs the whole Linux remote chain for real: the routing resolver, the provider resolver and its *MusicSource, the real Http*Client over a real socket, a loopback fake server that verifies the credential the way the provider carries it, the minted URL handed to LinuxPlaybackController, and an engine stand-in that fetches those bytes back off the same socket. A URL the server would reject fails the load here too, so the interesting assertions are checked rather than assumed. No home server, no CI secret, no DNS, no public internet: the fake servers (test/support/fake_remote_music_servers.dart) bind 127.0.0.1 on an ephemeral port with synthetic tokens this repository created, and the whole file runs in well under a second. Covers the opaque track reference, per-play URL minting, the handoff to Linux playback, the play lifecycle, queue transitions, the bounded mid-stream retry and its re-resolution, provider session expiry for all three, vanished items, cross-provider fallback (at resolve time and after the retry budget is spent), and the Linux backend preflight, which refuses a machine with no libmpv before a credential is minted and plays after Retry once it is installed. Four security invariants ride on every case: the queued track stays a logical reference, the crash-safe session document carries no authenticated URL or host, nothing the listener can see carries a credential (including when the engine's own error quotes the tokenized URL, which the stand-in does on purpose), and the SafeEventLog breadcrumbs carry no URL at all. docs/linux-desktop.md records what this proves and what still needs a real box: libmpv itself, which flutter test cannot load, stays covered by tool/linux_audio_backend_smoke.dart and a manual matrix. Closes #424 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01256tjvuSxQBQcscVstg6UC
TheZupZup
marked this pull request as ready for review
September 20, 2026 04:02
Contributor
Repository integrity reviewCLEAN Previously reported repository-integrity findings are resolved. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #424
What this adds
The resolver suites and the Linux controller suites each stub the other half out: the resolver tests hand the controller a canned URI, the controller tests hand the resolver a canned server. So nothing checked whether a
jellyfin:/subsonic:/plex:row actually becomes a URL a server accepts, and nothing caught a credential leaking out of the one place it belongs.test/core/services/linux_remote_stream_integration_test.dartruns the whole Linux remote chain for real:No home server, no CI secret, no DNS, no public internet. The fake servers (
test/support/fake_remote_music_servers.dart) bind127.0.0.1on an ephemeral port with synthetic tokens this repo created, answer only the endpoints playback actually calls, and verify the credential the way each provider carries it: Jellyfin'sApiKeyquery andAuthorizationheader, Subsonic'su/t/squery, Plex'sX-Plex-Tokenheader for the API calls and query for the Part fetch. A request signed with the wrong token gets a 401.Why a socket instead of a MockClient
A substituted response function can only replay what the test already decided. A server that rejects a bad credential, plus an engine stand-in that really fetches what it was handed, make the interesting assertions real: a stream URL a server would refuse fails the load here too. That turns "the URL that reached the engine was authenticated" from an assumption into a check.
The engine stand-in's failures deliberately quote the URL, because the real backend's do (
Failed to open <uri>, passed straight through by the vendored just_audio_media_kit). That is the leak path the controller has to contain, so the tests get to prove it does.Coverage
25 tests, all under a second:
LinuxPlaybackControllerand the play/pause/seek/stop lifecycleSecurity assertions
Four invariants ride on every case:
jellyfin:101), never a URLSafeEventLogbreadcrumbs a bug report would include carry no URL at allWhat still needs a real box
libmpv itself.
flutter testruns on the Dart VM without the Linux plugin bundle, so the last hop (media_kit decoding and putting bytes out through PipeWire/PulseAudio) is not exercised here; the engine seam is driven as far as the handoff and no further. That hop stays covered bytool/linux_audio_backend_smoke.dart, anddocs/linux-desktop.mdgains an explicit manual matrix for the rest.One thing worth knowing
The fake servers deliberately skip ephemeral ports containing
401or403.classifyEngineErrorreads an error's text for those digits to tell an expired session from a dropped connection, and the mid-stream errors here quote the URL, so landing on port 40123 turned a transient drop into "your session expired" about once in a few hundred runs. Ruled out in the harness rather than worked around in the tests. It is a (much less likely) sharp edge in the shared classifier too, if a real server ever sits on such a port; not touched here, since this PR is tests only.Checks
dart format --set-exit-if-changed .cleanflutter analyzecleanflutter testgreen (6136 tests)./scripts/check_secrets.shcleancheck_pr_security_surface.pyflags this diff as network-capable and credential-sensitive, which is correct: it is a PR about network and credentials, in tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01256tjvuSxQBQcscVstg6UC
Generated by Claude Code