tuftool 0.16.0

Utility for creating and signing The Update Framework (TUF) repositories
mod test_utils;

use assert_cmd::assert::Assert;
use assert_cmd::cargo_bin_cmd;
use httptest::{matchers::*, responders::*, Expectation, Server};
use std::fs::read_to_string;
use std::path::Path;
use std::str::FromStr;
use tempfile::TempDir;
use url::Url;

/// Set an expectation in a test HTTP server which serves a file from `tuf-reference-impl`.
fn create_successful_get(relative_path: &str) -> httptest::Expectation {
    let repo_dir = test_utils::test_data().join("tuf-reference-impl");
    let file_bytes = std::fs::read(repo_dir.join(relative_path)).unwrap();
    Expectation::matching(request::method_path("GET", format!("/{}", relative_path)))
        .times(1)
        .respond_with(
            status_code(200)
                .append_header("content-type", "application/octet-stream")
                .body(file_bytes),
        )
}

/// Set an expectation in a test HTTP server to return a `403 Forbidden` status code.
/// This is necessary for objects like `x.root.json` as tough will continue to increment
/// `x.root.json` until it receives either `403 Forbidden` or `404 NotFound`.
/// S3 returns `403 Forbidden` when requesting a file that does not exist.
fn create_unsuccessful_get(relative_path: &str) -> httptest::Expectation {
    Expectation::matching(request::method_path("GET", format!("/{}", relative_path)))
        .times(1)
        .respond_with(status_code(403))
}

/// Asserts that the named file in `outdir` exactly matches the file in `tuf-reference-impl/targets`
fn assert_file_match(outdir: &Path, filename: &str) {
    let got = read_to_string(outdir.join(filename)).unwrap();
    let want = read_to_string(
        test_utils::test_data()
            .join("tuf-reference-impl")
            .join("targets")
            .join(filename),
    )
    .unwrap();
    assert_eq!(got, want, "{} contents do not match.", filename);
}

fn download_command(metadata_base_url: Url, targets_base_url: Url) {
    let tempdir = TempDir::new().unwrap();
    let outdir = tempdir.path().join("outdir");
    let root_json = test_utils::test_data()
        .join("tuf-reference-impl")
        .join("metadata")
        .join("root.json");

    // Download a test repo.
    cargo_bin_cmd!("tuftool")
        .args([
            "download",
            "-r",
            root_json.to_str().unwrap(),
            "--metadata-url",
            metadata_base_url.as_str(),
            "--targets-url",
            targets_base_url.as_str(),
            outdir.to_str().unwrap(),
        ])
        .assert()
        .success();

    // Assert the files are exactly correct
    assert_file_match(&outdir, "file1.txt");
    assert_file_match(&outdir, "file2.txt");

    // Download again into the same outdir, this will fail because the directory exists.
    cargo_bin_cmd!("tuftool")
        .args([
            "download",
            "-r",
            root_json.to_str().unwrap(),
            "--metadata-url",
            metadata_base_url.as_str(),
            "--targets-url",
            targets_base_url.as_str(),
            outdir.to_str().unwrap(),
        ])
        .assert()
        .failure();
}

#[test]
// Ensure that the download command works with http transport and that we require outdir to
// not-exist.
fn download_http_transport() {
    let server = Server::run();
    server.expect(create_successful_get("metadata/role1.json"));
    server.expect(create_successful_get("metadata/role2.json"));
    server.expect(create_successful_get("metadata/snapshot.json"));
    server.expect(create_successful_get("metadata/targets.json"));
    server.expect(create_successful_get("metadata/timestamp.json"));
    server.expect(create_successful_get("targets/file1.txt"));
    server.expect(create_successful_get("targets/file2.txt"));
    server.expect(create_unsuccessful_get("metadata/2.root.json"));
    let metadata_base_url = Url::from_str(server.url_str("/metadata").as_str()).unwrap();
    let targets_base_url = Url::from_str(server.url_str("/targets").as_str()).unwrap();
    download_command(metadata_base_url, targets_base_url);
}

#[test]
// Ensure that the download command works with file transport, and that we require outdir to
// not-exist.
fn download_file_transport() {
    let repo_dir = test_utils::test_data().join("tuf-reference-impl");
    let metadata_base_url = test_utils::dir_url(repo_dir.join("metadata").to_str().unwrap());
    let targets_base_url = test_utils::dir_url(repo_dir.join("targets").to_str().unwrap());
    download_command(metadata_base_url, targets_base_url);
}

fn download_expired_repo(outdir: &Path, repo_dir: &TempDir, allow_expired_repo: bool) -> Assert {
    let root_json = test_utils::test_data().join("simple-rsa").join("root.json");
    let metadata_base_url = &test_utils::dir_url(repo_dir.path().join("metadata"));
    let targets_base_url = &test_utils::dir_url(repo_dir.path().join("targets"));
    let mut cmd = cargo_bin_cmd!("tuftool");
    cmd.args([
        "download",
        "-r",
        root_json.to_str().unwrap(),
        "--metadata-url",
        metadata_base_url.as_str(),
        "--targets-url",
        targets_base_url.as_str(),
        outdir.to_str().unwrap(),
    ]);
    if allow_expired_repo {
        cmd.arg("--allow-expired-repo").assert()
    } else {
        cmd.assert()
    }
}

#[test]
// Ensure download command fails when metadata has expired
fn download_command_expired_repo_fail() {
    let outdir = TempDir::new().unwrap();
    let repo_dir = TempDir::new().unwrap();
    // Create a expired repo using tuftool
    test_utils::create_expired_repo(repo_dir.path());
    // assert failure for download command
    download_expired_repo(outdir.path(), &repo_dir, false).failure();
}

#[test]
// Ensure download command is successful when metadata has expired but --allow-expired-repo flag is passed
fn download_command_expired_repo_allow() {
    let tempdir = TempDir::new().unwrap();
    let outdir = tempdir.path().join("outdir");
    let repo_dir = TempDir::new().unwrap();
    // Create a expired repo using tuftool
    test_utils::create_expired_repo(repo_dir.path());
    // assert success for download command
    download_expired_repo(&outdir, &repo_dir, true).success();
    // Assert the files are exactly correct
    assert_file_match(&outdir, "file1.txt");
    assert_file_match(&outdir, "file2.txt");
}

#[test]
// Ensure that we handle path-like target names correctly.
fn download_safe_target_paths() {
    let repo_dir = test_utils::test_data().join("safe-target-paths");
    let root = repo_dir.join("metadata").join("1.root.json");
    let metadata_base_url = &test_utils::dir_url(repo_dir.join("metadata"));
    let targets_base_url = &test_utils::dir_url(repo_dir.join("targets"));
    let tempdir = TempDir::new().unwrap();
    let outdir = tempdir.path().join("outdir");
    let mut cmd = cargo_bin_cmd!("tuftool");
    cmd.args([
        "download",
        "-r",
        root.to_str().unwrap(),
        "--metadata-url",
        metadata_base_url.as_str(),
        "--targets-url",
        targets_base_url.as_str(),
        outdir.to_str().unwrap(),
    ]);
    cmd.assert().success();
    assert!(outdir.join("data1.txt").is_file());
    assert!(outdir.join("foo/bar/data2.txt").is_file())
}