serves3 1.2.0-beta.2

A very simple proxy to browse files from private S3 buckets
// SPDX-FileCopyrightText: © Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: EUPL-1.2

mod common;

use {
    object_store::{PutPayload, path::Path as ObjectStorePath},
    scraper::{Html, Selector},
};

async fn create_sample_files(test: &common::Test) -> anyhow::Result<()> {
    test.bucket
        .put(
            &ObjectStorePath::from("file.txt"),
            PutPayload::from_static("I am a file".as_bytes()),
        )
        .await?;

    test.bucket
        .put(
            &ObjectStorePath::from("folder/file.txt"),
            PutPayload::from_static("I am a file in a folder".as_bytes()),
        )
        .await?;

    Ok(())
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn serves_files() -> anyhow::Result<()> {
    let test = common::Test::new().await?;
    create_sample_files(&test).await?;

    let resp = reqwest::get(test.base_url.join("file.txt")?).await?;
    assert_eq!(resp.bytes().await?, "I am a file");

    let resp = reqwest::get(test.base_url.join("folder/file.txt")?).await?;
    assert_eq!(resp.bytes().await?, "I am a file in a folder");

    Ok(())
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn serves_top_level_folder() -> anyhow::Result<()> {
    let test = common::Test::new().await?;
    create_sample_files(&test).await?;

    // Check that a file in the toplevel is listed:
    let resp = reqwest::get(test.base_url.clone()).await?;
    assert!(
        resp.status().is_success(),
        "Request failed with {}",
        resp.status()
    );

    let text = resp.text().await?;
    println!("{}", &text);
    let document = Html::parse_document(&text);

    let selector = Selector::parse(r#"h1"#).unwrap();
    for title in document.select(&selector) {
        assert_eq!(title.inner_html(), "/", "title doesn't match");
    }

    let selector =
        Selector::parse(r#"table > tbody > tr:nth-child(1) > td:first-child > a"#).unwrap();
    for item in document.select(&selector) {
        // Folders should be listed ending with a slash,
        // or HTTP gets confused. This is also due to the
        // normalization we do on the path in the main program.
        assert_eq!(item.attr("href"), Some("folder/"));
        assert_eq!(item.text().next(), Some("folder/"));
    }

    let selector =
        Selector::parse(r#"table > tbody > tr:nth-child(2) > td:first-child > a"#).unwrap();
    for item in document.select(&selector) {
        assert_eq!(item.attr("href"), Some("file.txt"));
        assert_eq!(item.text().next(), Some("file.txt"));
    }

    Ok(())
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn serves_second_level_folder() -> anyhow::Result<()> {
    let test = common::Test::new().await?;
    create_sample_files(&test).await?;

    // Check that a file in the second level is listed:
    let resp = reqwest::get(test.base_url.join("folder/")?).await?;
    assert!(
        resp.status().is_success(),
        "Request failed with {}",
        resp.status()
    );
    let text = resp.text().await?;
    println!("{}", &text);
    let document = Html::parse_document(&text);

    let selector = Selector::parse(r#"h1"#).unwrap();
    for title in document.select(&selector) {
        assert_eq!(title.inner_html(), "folder/", "title doesn't match");
    }

    let selector =
        Selector::parse(r#"table > tbody > tr:nth-child(1) > td:first-child > a"#).unwrap();
    for item in document.select(&selector) {
        assert_eq!(item.attr("href"), Some("../"));
        assert_eq!(item.inner_html(), "..");
    }

    let selector =
        Selector::parse(r#"table > tbody > tr:nth-child(2) > td:first-child > a"#).unwrap();
    for item in document.select(&selector) {
        assert_eq!(item.attr("href"), Some("file.txt"));
        assert_eq!(item.inner_html(), "file.txt");
    }

    Ok(())
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn serves_second_level_folder_without_ending_slash() -> anyhow::Result<()> {
    let test = common::Test::new().await?;
    create_sample_files(&test).await?;

    // Check that a file in the second level is listed even without an ending slash:
    let resp = reqwest::get(test.base_url.join("folder")?).await?;
    assert!(
        resp.status().is_success(),
        "Request failed with {}",
        resp.status()
    );

    // Ensure we were redirected to a URL ending with a slash
    assert!(resp.url().path().ends_with("/"));

    let text = resp.text().await?;
    println!("{}", &text);
    let document = Html::parse_document(&text);

    let selector = Selector::parse(r#"h1"#).unwrap();
    for title in document.select(&selector) {
        assert_eq!(title.inner_html(), "folder/", "title doesn't match");
    }

    let selector =
        Selector::parse(r#"table > tbody > tr:nth-child(1) > td:first-child > a"#).unwrap();
    for item in document.select(&selector) {
        assert_eq!(item.attr("href"), Some("../"));
        assert_eq!(item.inner_html(), "..");
    }

    let selector =
        Selector::parse(r#"table > tbody > tr:nth-child(2) > td:first-child > a"#).unwrap();
    for item in document.select(&selector) {
        assert_eq!(item.attr("href"), Some("file.txt"));
        assert_eq!(item.inner_html(), "file.txt");
    }

    Ok(())
}