//! 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() ); }