From b50f04d56514692ebda8fd0bb115f90be0afdb92 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sun, 24 May 2026 11:54:18 -0700 Subject: [PATCH] =?UTF-8?q?tests:=20sulkta=20smoke=20=E2=80=94=20iOS=20/?= =?UTF-8?q?=20TV=20/=20default-order=20player=5Ffrom=5Fclient=20+=20HEAD?= =?UTF-8?q?=20probe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercises the patched default client_order + soft-fail DeobfData end-to-end against current YouTube. Verifies: 1. iOS player_from_client returns streams (no deobf path). 2. TV player_from_client returns streams (deobf path with soft-fail). 3. default-clients player() picks iOS primary and a returned audio URL HEADs to a 2xx/3xx (i.e. YouTube CDN accepts it). Lives alongside the upstream tests/youtube.rs so we don't fork their big snapshot-based test suite, but stays standalone so a single `cargo test --test sulkta_smoke` exercises just the load-bearing playback path for our consumers (straw, future torttube). --- tests/sulkta_smoke.rs | 114 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/sulkta_smoke.rs diff --git a/tests/sulkta_smoke.rs b/tests/sulkta_smoke.rs new file mode 100644 index 0000000..2b2f0f0 --- /dev/null +++ b/tests/sulkta_smoke.rs @@ -0,0 +1,114 @@ +//! Sulkta-fork smoke tests for the player pipeline. +//! +//! Verifies the patched default client order (`Ios, Tv` without botguard) plus +//! the soft-fail DeobfData::extract works against current YouTube player.js. +//! +//! Run with: `cargo test --test sulkta_smoke -- --nocapture` + +use rstest::{fixture, rstest}; +use rustypipe::client::{ClientType, RustyPipe}; + +/// A stable, long-running, public-domain music video. Used by upstream +/// tests too (`n4tK7LYFxI0` = Spektrem - Shine, NCS). +const TEST_VIDEO_ID: &str = "n4tK7LYFxI0"; + +#[fixture] +fn rp() -> RustyPipe { + RustyPipe::builder() + .storage_dir(env!("CARGO_MANIFEST_DIR")) + .build() + .unwrap() +} + +/// Sanity: iOS path returns stream URLs and never touches the deobf code. +#[rstest] +#[tokio::test] +async fn ios_player_returns_streams(rp: RustyPipe) { + let pd = rp + .query() + .player_from_client(TEST_VIDEO_ID, ClientType::Ios) + .await + .expect("iOS player_from_client should succeed"); + + assert_eq!(pd.details.id, TEST_VIDEO_ID); + assert!( + !pd.video_streams.is_empty() || !pd.video_only_streams.is_empty(), + "expected at least one video stream" + ); + assert!( + !pd.audio_streams.is_empty(), + "expected at least one audio stream" + ); +} + +/// Sanity: TV path (which sets `needs_deobf=true` for the sig_timestamp request +/// payload, but the soft-fail patch keeps the call alive even when sig_fn/nsig_fn +/// regex extraction fails on a rotated player.js). +#[rstest] +#[tokio::test] +async fn tv_player_returns_streams(rp: RustyPipe) { + let pd = rp + .query() + .player_from_client(TEST_VIDEO_ID, ClientType::Tv) + .await + .expect("TV player_from_client should succeed even when sig deobf regex misses"); + + assert_eq!(pd.details.id, TEST_VIDEO_ID); + assert!( + !pd.video_streams.is_empty() || !pd.video_only_streams.is_empty(), + "expected at least one TV video stream" + ); +} + +/// The patched default-client order should pick iOS as primary and return +/// playable streams in the absence of botguard signing. +#[rstest] +#[tokio::test] +async fn default_client_order_returns_streams(rp: RustyPipe) { + let order = rp.query().player_client_order(); + eprintln!("default client order (no botguard): {order:?}"); + assert_eq!( + order[0], + ClientType::Ios, + "iOS should be the no-botguard primary" + ); + + let pd = rp + .query() + .player(TEST_VIDEO_ID) + .await + .expect("default-clients player() should succeed"); + + assert_eq!(pd.details.id, TEST_VIDEO_ID); + assert!( + !pd.video_streams.is_empty() || !pd.video_only_streams.is_empty(), + "expected at least one video stream from the default-clients path" + ); + assert!( + !pd.audio_streams.is_empty(), + "expected at least one audio stream from the default-clients path" + ); + + // HEAD-probe one returned audio stream to confirm YT actually serves it. + let stream_url = pd + .audio_streams + .first() + .expect("at least one audio stream") + .url + .clone(); + eprintln!("probing first audio URL: {}", &stream_url[..stream_url.len().min(180)]); + let client = reqwest::Client::builder() + .user_agent("Mozilla/5.0 sulkta-rustypipe-smoke/1.0") + .build() + .unwrap(); + let resp = client + .head(&stream_url) + .send() + .await + .expect("HEAD request to YT CDN should not error"); + assert!( + resp.status().is_success() || resp.status().is_redirection(), + "audio URL HEAD returned non-OK status: {} (sig deobf likely needed but skipped)", + resp.status() + ); +}