search_hub

at 2ab9aa7 Raw

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 { timeout_secs: None, api_url: "https://crates.io/api/v1/crates?q={}&per_page=10".into() };
    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 { timeout_secs: None, api_url: "https://crates.io/api/v1/crates?q={}&per_page=10".into() };
    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 { timeout_secs: None, api_url: "https://crates.io/api/v1/crates?q={}&per_page=10".into() };
    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('/')),
        timeout_secs: None,
    };
    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}");
        }
    }
}