Skip to content

test(linux): add credential-free remote-stream integration tests (#424) - #658

Merged
TheZupZup merged 2 commits into
mainfrom
thezupzup/trusting-fermi-qfsoam
Sep 20, 2026
Merged

TheZupZup merged 2 commits into
mainfrom
thezupzup/trusting-fermi-qfsoam

Conversation

@TheZupZup

Copy link
Copy Markdown
Owner

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.dart runs the whole Linux remote chain for real:

jellyfin: / subsonic: / plex: track
  → RoutingPlayableUriResolver
  → the provider resolver and its *MusicSource
  → the real Http*Client, over a real socket
  → a loopback fake server that checks the credential
  → the minted URL handed to LinuxPlaybackController
  → an engine that fetches those bytes back off the same socket

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 repo created, answer only the endpoints playback actually calls, and verify the credential the way each provider carries it: Jellyfin's ApiKey query and Authorization header, Subsonic's u/t/s query, Plex's X-Plex-Token header 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:

  • opaque, credential-free track reference, before and after everything
  • fresh authenticated URL minting, per play (two plays, two resolutions)
  • handoff into LinuxPlaybackController and the play/pause/seek/stop lifecycle
  • queue transitions (completion and skip) re-resolving the next track against its own provider
  • bounded mid-stream retry and its re-resolution, then a clean error when the budget is spent
  • listener-driven Retry against a recovered server, and an unreachable server
  • provider session expiry for all three (a rejected credential asks for a sign-in, not a pointless retry)
  • a stale library row whose item the server no longer has, and a Plex item with no playable part
  • cross-provider fallback to a sibling copy, both at resolve time and after the retry budget is spent
  • the Linux backend preflight: refuses a machine with no libmpv before a credential is minted, and plays after Retry once the package is installed

Security assertions

Four invariants ride on every case:

  • the queued track stays a logical reference (jellyfin:101), never a URL
  • the crash-safe session document carries that identity and no authenticated URL, host or token
  • nothing the listener can see carries a credential, including when the engine's own error quotes the tokenized URL
  • the SafeEventLog breadcrumbs a bug report would include carry no URL at all

What still needs a real box

libmpv itself. flutter test runs 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 by tool/linux_audio_backend_smoke.dart, and docs/linux-desktop.md gains an explicit manual matrix for the rest.

One thing worth knowing

The fake servers deliberately skip ephemeral ports containing 401 or 403. classifyEngineError reads 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 . clean
  • flutter analyze clean
  • flutter test green (6136 tests)
  • the new file run 90+ times in a row with no flake
  • ./scripts/check_secrets.sh clean

check_pr_security_surface.py flags 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

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
TheZupZup marked this pull request as ready for review September 20, 2026 04:02
@github-actions

Copy link
Copy Markdown
Contributor

Repository integrity review

CLEAN

Previously reported repository-integrity findings are resolved.

@TheZupZup
TheZupZup merged commit 44c19b4 into main Sep 20, 2026
16 checks passed
@TheZupZup
TheZupZup deleted the thezupzup/trusting-fermi-qfsoam branch September 20, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Player] Add Linux remote-stream integration tests

1 participant