Reimplement config parsing, add integration tests

This commit is contained in:
Matteo Settenvini 2024-06-02 04:58:01 +02:00
parent ed3a1fbfe9
commit 4defbcec1f
13 changed files with 2122 additions and 619 deletions

38
tests/common/minio.rs Normal file
View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: © Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: EUPL-1.2
use {testcontainers::core::WaitFor, testcontainers::Image, testcontainers_modules::minio};
const MINIO_IMAGE_TAG: &'static str = "RELEASE.2024-05-28T17-19-04Z";
pub struct MinIO {
inner: minio::MinIO,
}
impl Image for MinIO {
type Args = minio::MinIOServerArgs;
fn name(&self) -> String {
self.inner.name()
}
fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::message_on_stderr("API:")]
}
fn tag(&self) -> String {
MINIO_IMAGE_TAG.into()
}
fn env_vars(&self) -> Box<dyn Iterator<Item = (&String, &String)> + '_> {
self.inner.env_vars()
}
}
impl Default for MinIO {
fn default() -> Self {
Self {
inner: Default::default(),
}
}
}

123
tests/common/mod.rs Normal file
View file

@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: © Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: EUPL-1.2
mod minio;
use {
anyhow::{anyhow, Result},
reqwest::Url,
std::{ptr::null_mut, str::FromStr},
testcontainers::{runners::AsyncRunner, ContainerAsync},
tokio::io::AsyncBufReadExt as _,
};
pub struct Test {
pub base_url: Url,
pub bucket: s3::Bucket,
pub serves3: tokio::process::Child,
pub minio: ContainerAsync<minio::MinIO>,
}
const MAXIMUM_SERVES3_INIT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const BUCKET_NAME: &'static str = "integration-test-bucket";
const REGION: &'static str = "test-region";
const ACCESS_KEY: &'static str = "minioadmin";
const SECRET_KEY: &'static str = "minioadmin";
impl Test {
pub async fn new() -> Result<Self> {
// NOTE: right now there is a bug in bollard
// that makes testcontainers work in Docker only and
// not podman (it is not able to fetch exposed ports).
// If this test fails make sure you are using docker.
std::env::remove_var("DOCKER_HOST");
let image = minio::MinIO::default();
let container = image.start().await?;
let endpoint = format!(
"http://{host}:{port}",
host = container.get_host().await?,
port = container.get_host_port_ipv4(9000).await?
);
let credentials = s3::creds::Credentials::new(
Some(&ACCESS_KEY),
Some(&SECRET_KEY),
None,
None,
Some("test"),
)?;
let bucket = s3::Bucket::create_with_path_style(
&BUCKET_NAME,
s3::Region::Custom {
region: REGION.into(),
endpoint: endpoint.clone(),
},
credentials,
s3::BucketConfiguration::private(),
)
.await?
.bucket;
let bin = std::env!("CARGO_BIN_EXE_serves3");
let mut child = tokio::process::Command::new(bin)
.env("SERVES3_ADDRESS", "127.0.0.1")
.env("SERVES3_PORT", "0")
.env("SERVES3_LOG_LEVEL", "debug")
.env(
"SERVES3_S3_BUCKET",
format!(
r#"{{
name = "{name}",
endpoint = "{endpoint}",
region = "{region}",
access_key_id = "{user}",
secret_access_key = "{secret}",
path_style = true
}}"#,
name = BUCKET_NAME,
endpoint = endpoint,
region = &REGION,
user = ACCESS_KEY,
secret = SECRET_KEY
),
)
.stdout(std::process::Stdio::piped())
.spawn()?;
let base_url = tokio::time::timeout(MAXIMUM_SERVES3_INIT_TIMEOUT, async {
let stdout = child.stdout.as_mut().unwrap();
let mut lines = tokio::io::BufReader::new(stdout).lines();
let re = regex::Regex::new("^Rocket has launched from (http://.+)$").unwrap();
while let Some(line) = lines.next_line().await? {
println!("{}", &line);
if let Some(captures) = re.captures(&line) {
let url = captures.get(1).unwrap().as_str();
return Ok(Url::from_str(url)?);
}
}
Err(anyhow!("Rocket did not print that it has started"))
})
.await??;
Ok(Self {
base_url,
bucket,
serves3: child,
minio: container,
})
}
}
impl Drop for Test {
fn drop(&mut self) {
unsafe {
let pid = self.serves3.id().unwrap() as i32;
libc::kill(pid, libc::SIGTERM);
libc::waitpid(pid, null_mut(), 0);
}
}
}

158
tests/integration.rs Normal file
View file

@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: © Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: EUPL-1.2
mod common;
use scraper::{Html, Selector};
#[test_log::test(tokio::test)]
async fn serves_files() -> anyhow::Result<()> {
let test = common::Test::new().await?;
test.bucket
.put_object("file.txt", "I am a file".as_bytes())
.await?;
test.bucket
.put_object("folder/file.txt", "I am a file in a folder".as_bytes())
.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)]
async fn serves_top_level_folder() -> anyhow::Result<()> {
let test = common::Test::new().await?;
test.bucket
.put_object("file.txt", "I am a file".as_bytes())
.await?;
test.bucket
.put_object("folder/file.txt", "I am a file in a folder".as_bytes())
.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) {
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)]
async fn serves_second_level_folder() -> anyhow::Result<()> {
let test = common::Test::new().await?;
test.bucket
.put_object("file.txt", "I am a file".as_bytes())
.await?;
test.bucket
.put_object("folder/file.txt", "I am a file in a folder".as_bytes())
.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)]
async fn serves_second_level_folder_without_ending_slash() -> anyhow::Result<()> {
let test = common::Test::new().await?;
test.bucket
.put_object("file.txt", "I am a file".as_bytes())
.await?;
test.bucket
.put_object("folder/file.txt", "I am a file in a folder".as_bytes())
.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()
);
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(())
}