use std::sync::OnceLock; use search_hub::search_engines::SearchEngine; static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new(); fn rt() -> &'static tokio::runtime::Runtime { RT.get_or_init(|| tokio::runtime::Runtime::new().unwrap()) } fn client() -> reqwest::Client { reqwest::Client::builder() .user_agent("search_hub_test") .build() .unwrap() } #[test] fn crates_io_returns_results_for_generic_query() { let engine = search_hub::search_engines::crates_io::CratesIo; let client = client(); let results = rt().block_on(engine.fetch_results("tokio", &client)); assert!(results.is_ok(), "crates.io search should succeed: {:?}", results.err()); let entries = results.unwrap(); assert!(!entries.is_empty(), "should return at least one crate for 'tokio'"); assert!(entries.len() <= 10, "max 10 results"); for entry in &entries { assert!(!entry.title.is_empty(), "title should not be empty"); assert!(!entry.url.is_empty(), "url should not be empty"); assert!(entry.url.starts_with("http"), "url should start with http"); assert_eq!(entry.engine, "crates.io", "engine should be crates.io"); } println!("crates.io returned {} results for 'tokio':", entries.len()); for e in &entries { println!(" - {} ({})", e.title, e.url); } } #[test] fn crates_io_search_uses_https_urls() { let engine = search_hub::search_engines::crates_io::CratesIo; let client = client(); let results = rt().block_on(engine.fetch_results("serde", &client)); assert!(results.is_ok(), "crates.io search should succeed: {:?}", results.err()); let entries = results.unwrap(); assert!(!entries.is_empty(), "should return at least one crate for 'serde'"); for entry in &entries { assert!( entry.url.starts_with("https://"), "url should be https: {}", entry.url ); } } #[test] fn crates_io_empty_query_returns_error() { let engine = search_hub::search_engines::crates_io::CratesIo; let client = client(); let results = rt().block_on(engine.fetch_results("zzzzzzzzzz_nonexistent_crate_xxxxxxxxx", &client)); assert!(results.is_err(), "should return error for nonsense query"); } #[test] fn searxng_returns_results_if_configured() { let instance = match std::env::var("SEARCH_HUB_SEARXNG_INSTANCE") { Ok(v) => v, Err(_) => { eprintln!("skipping searxng test: SEARCH_HUB_SEARXNG_INSTANCE not set"); return; } }; let engine = search_hub::search_engines::searxng::SearXng { instance: instance.clone(), url_tpl: format!("{}/search?format=json&q={{}}", instance.trim_end_matches('/')), }; let client = client(); let results = rt().block_on(engine.fetch_results("rust", &client)); match results { Ok(entries) => { assert!(!entries.is_empty(), "searxng should return results for 'rust'"); for entry in &entries { assert!(!entry.title.is_empty(), "title should not be empty"); assert!(!entry.url.is_empty(), "url should not be empty"); } println!("searxng returned {} results for 'rust':", entries.len()); for e in &entries { println!(" [{}] {} ({})", e.engine, e.title, e.url); } } Err(e) => { panic!("searxng search failed: {e}"); } } }