tame-index 0.3.1

Provides access to local and remote cargo registry indices
Documentation
mod utils;

use http::header;
use tame_index::{IndexLocation, IndexUrl, SparseIndex};

#[inline]
fn crates_io(path: impl AsRef<tame_index::Path>) -> SparseIndex {
    SparseIndex::new(
        IndexLocation::new(IndexUrl::CratesIoSparse).with_root(Some(path.as_ref().to_owned())),
    )
    .unwrap()
}

/// Validates the we get a valid root and krate url
#[test]
fn opens_crates_io() {
    let index = crates_io(env!("CARGO_MANIFEST_DIR"));

    assert_eq!(index.url(), "https://index.crates.io/");
    assert_eq!(
        index.crate_url("autocfg".try_into().unwrap()),
        "https://index.crates.io/au/to/autocfg"
    );
}

/// Validates a request can be made for a crate that doesn't have a local cache entry
#[test]
fn make_request_without_cache() {
    let index = crates_io(env!("CARGO_MANIFEST_DIR"));

    let req = index
        .make_remote_request("serde".try_into().unwrap())
        .unwrap();

    let hdrs = req.headers();
    // Ensure neither of the possible cache headers are set
    assert!(
        hdrs.get(header::IF_MODIFIED_SINCE).is_none() && hdrs.get(header::IF_NONE_MATCH).is_none()
    );

    assert_eq!(hdrs.get("cargo-protocol").unwrap(), "version=1");
    assert_eq!(hdrs.get(header::ACCEPT).unwrap(), "text/plain");
    assert_eq!(hdrs.get(header::ACCEPT_ENCODING).unwrap(), "gzip");
}

const ETAG: &str = "W/\"fa62f662c9aae1f21cab393950d4ae23\"";
const DATE: &str = "Thu, 22 Oct 2023 09:40:03 GMT";

/// Validates appropriate headers are set when a cache entry does exist
#[test]
fn make_request_with_cache() {
    let td = utils::tempdir();

    let index = crates_io(&td);

    {
        let etag_krate = utils::fake_krate("etag-krate", 2);
        index
            .cache()
            .write_to_cache(&etag_krate, &format!("{}: {ETAG}", header::ETAG))
            .unwrap();

        let req = index
            .make_remote_request("etag-krate".try_into().unwrap())
            .unwrap();

        assert_eq!(req.headers().get(header::IF_NONE_MATCH).unwrap(), ETAG);
    }

    {
        let modified_krate = utils::fake_krate("modified-krate", 2);
        index
            .cache()
            .write_to_cache(
                &modified_krate,
                &format!("{}: {DATE}", header::LAST_MODIFIED),
            )
            .unwrap();

        let req = index
            .make_remote_request("modified-krate".try_into().unwrap())
            .unwrap();

        assert_eq!(req.headers().get(header::IF_MODIFIED_SINCE).unwrap(), DATE);
    }
}

/// Validates we can parse a response where the local cache version is up to date
#[test]
fn parse_unmodified_response() {
    let td = utils::tempdir();
    let index = crates_io(&td);

    let etag_krate = utils::fake_krate("etag-krate", 2);
    index
        .cache()
        .write_to_cache(&etag_krate, &format!("{}: {ETAG}", header::ETAG))
        .unwrap();

    let response = http::Response::builder()
        .status(http::StatusCode::NOT_MODIFIED)
        .header(header::ETAG, ETAG)
        .body(Vec::new())
        .unwrap();

    let cached_krate = index
        .parse_remote_response("etag-krate".try_into().unwrap(), response, true)
        .unwrap()
        .expect("cached krate");

    assert_eq!(etag_krate, cached_krate);
}

/// Validates we can parse a modified response
#[test]
fn parse_modified_response() {
    let td = utils::tempdir();
    let index = crates_io(&td);

    {
        let etag_krate = utils::fake_krate("etag-krate", 3);
        let mut serialized = Vec::new();
        etag_krate.write_json_lines(&mut serialized).unwrap();

        let response = http::Response::builder()
            .status(http::StatusCode::OK)
            .header(header::ETAG, ETAG)
            .body(serialized)
            .unwrap();

        let new_krate = index
            .parse_remote_response("etag-krate".try_into().unwrap(), response, true)
            .unwrap()
            .expect("new response");

        assert_eq!(etag_krate, new_krate);

        let cached_krate = index
            .cache()
            .cached_krate(
                "etag-krate".try_into().unwrap(),
                Some(&format!("{}: {ETAG}", header::ETAG)),
            )
            .unwrap()
            .expect("cached krate");

        assert_eq!(etag_krate, cached_krate);
    }

    {
        let modified_krate = utils::fake_krate("modified-krate", 3);
        let mut serialized = Vec::new();
        modified_krate.write_json_lines(&mut serialized).unwrap();

        let response = http::Response::builder()
            .status(http::StatusCode::OK)
            .header(header::LAST_MODIFIED, DATE)
            .body(serialized)
            .unwrap();

        let new_krate = index
            .parse_remote_response("modified-krate".try_into().unwrap(), response, true)
            .unwrap()
            .expect("new response");

        assert_eq!(modified_krate, new_krate);

        let cached_krate = index
            .cache()
            .cached_krate(
                "modified-krate".try_into().unwrap(),
                Some(&format!("{}: {DATE}", header::LAST_MODIFIED)),
            )
            .unwrap()
            .expect("cached krate");

        assert_eq!(modified_krate, cached_krate);
    }
}

/// Ensure we can actually send a request to crates.io and parse the response
#[test]
#[cfg(feature = "sparse")]
fn end_to_end() {
    let td = utils::tempdir();
    let index = crates_io(&td);

    let client = reqwest::blocking::Client::builder()
        .http2_prior_knowledge()
        .build()
        .unwrap();

    let rsi = tame_index::index::RemoteSparseIndex::new(index, client);

    let spdx_krate = rsi
        .krate("spdx".try_into().unwrap(), true)
        .expect("failed to retrieve spdx")
        .expect("failed to find spdx");

    spdx_krate
        .versions
        .iter()
        .find(|iv| iv.version == "0.10.1")
        .expect("failed to find expected version");
}