tests: sulkta smoke — iOS / TV / default-order player_from_client + HEAD probe
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).
This commit is contained in:
parent
bda0fea193
commit
b50f04d565
1 changed files with 114 additions and 0 deletions
114
tests/sulkta_smoke.rs
Normal file
114
tests/sulkta_smoke.rs
Normal file
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
Reference in a new issue