Skip to content

Commit

Permalink
Merge pull request #221 from dandi/smoke
Browse files Browse the repository at this point in the history
Add some smoke tests
  • Loading branch information
jwodder authored Jan 16, 2025
2 parents 707bcc1 + 8ac2d2b commit 68249b5
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 20 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ xml-rs = "0.8.25"

[dev-dependencies]
assert_matches = "1.5.0"
http-body-util = "0.1.2"
pretty_assertions = "1.4.1"
rstest = { version = "0.24.0", default-features = false }

Expand Down
212 changes: 192 additions & 20 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use axum::{
routing::get,
Router,
};
use clap::Parser;
use clap::{Args, Parser};
use http_body::Body as _;
use std::fmt;
use std::net::IpAddr;
Expand All @@ -55,9 +55,8 @@ static ROBOTS_TXT: &str = "User-agent: *\nDisallow: /\n";
#[derive(Clone, Debug, Eq, Parser, PartialEq)]
#[command(version = env!("VERSION_WITH_GIT"))]
struct Arguments {
/// API URL of the DANDI Archive instance to serve
#[arg(long, default_value = DEFAULT_API_URL, value_name = "URL")]
api_url: HttpUrl,
#[command(flatten)]
config: Config,

/// IP address to listen on
#[arg(long, default_value = "127.0.0.1")]
Expand All @@ -66,6 +65,13 @@ struct Arguments {
/// Port to listen on
#[arg(short, long, default_value_t = 8080)]
port: u16,
}

#[derive(Args, Clone, Debug, Eq, PartialEq)]
struct Config {
/// API URL of the DANDI Archive instance to serve
#[arg(long, default_value = DEFAULT_API_URL, value_name = "URL")]
api_url: HttpUrl,

/// Redirect requests for blob assets directly to S3 instead of to Archive
/// URLs that redirect to signed S3 URLs
Expand All @@ -82,6 +88,19 @@ struct Arguments {
zarrman_cache_mb: u64,
}

impl Default for Config {
fn default() -> Config {
Config {
api_url: DEFAULT_API_URL
.parse::<HttpUrl>()
.expect("DEFAULT_API_URL should be a valid HttpUrl"),
prefer_s3_redirects: false,
title: env!("CARGO_PKG_NAME").into(),
zarrman_cache_mb: 100,
}
}
}

// See
// <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/time/struct.OffsetTime.html#method.local_rfc_3339>
// for an explanation of the main + #[tokio::main]run thing
Expand Down Expand Up @@ -111,18 +130,32 @@ fn main() -> anyhow::Result<()> {
#[tokio::main]
async fn run() -> anyhow::Result<()> {
let args = Arguments::parse();
let dandi = DandiClient::new(args.api_url)?;
let zarrfetcher = ManifestFetcher::new(args.zarrman_cache_mb * 1_000_000)?;
let app = get_app(args.config)?;
let listener = tokio::net::TcpListener::bind((args.ip_addr, args.port))
.await
.context("failed to bind listener")?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.await
.context("failed to serve application")?;
Ok(())
}

fn get_app(cfg: Config) -> anyhow::Result<Router> {
let dandi = DandiClient::new(cfg.api_url)?;
let zarrfetcher = ManifestFetcher::new(cfg.zarrman_cache_mb * 1_000_000)?;
zarrfetcher.install_periodic_dump(ZARR_MANIFEST_CACHE_DUMP_PERIOD);
let zarrman = ZarrManClient::new(zarrfetcher);
let templater = Templater::new(args.title)?;
let templater = Templater::new(cfg.title)?;
let dav = Arc::new(DandiDav {
dandi,
zarrman,
templater,
prefer_s3_redirects: args.prefer_s3_redirects,
prefer_s3_redirects: cfg.prefer_s3_redirects,
});
let app = Router::new()
Ok(Router::new()
.route(
"/.static/styles.css",
get(|| async {
Expand Down Expand Up @@ -175,17 +208,7 @@ async fn run() -> anyhow::Result<()> {
"starting processing request",
);
}),
);
let listener = tokio::net::TcpListener::bind((args.ip_addr, args.port))
.await
.context("failed to bind listener")?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.await
.context("failed to serve application")?;
Ok(())
))
}

/// Handle `HEAD` requests by converting them to `GET` requests and discarding
Expand Down Expand Up @@ -257,3 +280,152 @@ impl fmt::Display for UsizeDiff {
)
}
}

#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
use http_body_util::BodyExt;
use tower::ServiceExt; // for `collect`

fn fill_html_footer(html: &str) -> String {
let commit_str = match option_env!("GIT_COMMIT") {
Some(s) => std::borrow::Cow::from(format!(", commit {s}")),
None => std::borrow::Cow::from(""),
};
html.replacen(
"{package_url}",
&env!("CARGO_PKG_REPOSITORY").replace('/', "&#x2F;"),
1,
)
.replacen("{version}", env!("CARGO_PKG_VERSION"), 1)
.replacen("{commit}", &commit_str, 1)
}

#[tokio::test]
async fn test_get_styles() {
let app = get_app(Config::default()).unwrap();
let response = app
.oneshot(
Request::builder()
.uri("/.static/styles.css")
.header("X-Forwarded-For", "127.0.0.1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok()),
Some(CSS_CONTENT_TYPE)
);
assert!(!response.headers().contains_key("DAV"));
let body = response.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8_lossy(&body);
assert_eq!(body, STYLESHEET);
}

#[tokio::test]
async fn test_get_root() {
let app = get_app(Config::default()).unwrap();
let response = app
.oneshot(
Request::builder()
.uri("/")
.header("X-Forwarded-For", "127.0.0.1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok()),
Some(HTML_CONTENT_TYPE)
);
assert!(response.headers().contains_key("DAV"));
let body = response.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8_lossy(&body);
let expected = fill_html_footer(include_str!("testdata/index.html"));
assert_eq!(body, expected);
}

#[tokio::test]
async fn test_head_styles() {
let app = get_app(Config::default()).unwrap();
let response = app
.oneshot(
Request::builder()
.method(Method::HEAD)
.uri("/.static/styles.css")
.header("X-Forwarded-For", "127.0.0.1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok()),
Some(CSS_CONTENT_TYPE)
);
assert_eq!(
response
.headers()
.get(CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<usize>().ok()),
Some(STYLESHEET.len())
);

assert!(!response.headers().contains_key("DAV"));
let body = response.into_body().collect().await.unwrap().to_bytes();
assert!(body.is_empty());
}

#[tokio::test]
async fn test_head_root() {
let app = get_app(Config::default()).unwrap();
let response = app
.oneshot(
Request::builder()
.method(Method::HEAD)
.uri("/")
.header("X-Forwarded-For", "127.0.0.1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok()),
Some(HTML_CONTENT_TYPE)
);
let expected = fill_html_footer(include_str!("testdata/index.html"));
assert_eq!(
response
.headers()
.get(CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<usize>().ok()),
Some(expected.len())
);
assert!(response.headers().contains_key("DAV"));
let body = response.into_body().collect().await.unwrap().to_bytes();
assert!(body.is_empty());
}
}
50 changes: 50 additions & 0 deletions src/testdata/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>dandidav — &#x2F;</title>
<link rel="stylesheet" type="text/css" href="/.static/styles.css"/>
</head>
<body>
<div class="breadcrumbs">
<a href="&#x2F;">dandidav</a>
</div>
<table class="collection">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Created</th>
<th>Modified</th>
</tr>
</thead>
<tbody>
<tr>
<td class="name dir">
<div class="link-with-metadata">
<span class="item-link"><a href="&#x2F;dandisets&#x2F;">dandisets/</a></span>
</div>
</td>
<td class="type">Dandisets</td>
<td class="null">&#x2014;</td>
<td class="null">&#x2014;</td>
<td class="null">&#x2014;</td>
</tr>
<tr>
<td class="name dir">
<div class="link-with-metadata">
<span class="item-link"><a href="&#x2F;zarrs&#x2F;">zarrs/</a></span>
</div>
</td>
<td class="type">Zarrs</td>
<td class="null">&#x2014;</td>
<td class="null">&#x2014;</td>
<td class="null">&#x2014;</td>
</tr>
</tbody>
</table>
<footer>
<a href="{package_url}">dandidav</a>, v{version}{commit}
</footer>
</body>
</html>

0 comments on commit 68249b5

Please sign in to comment.