Skip to content

Commit

Permalink
test: add integration tests (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigoden authored Jun 12, 2022
1 parent 6b01c14 commit 471bca8
Show file tree
Hide file tree
Showing 21 changed files with 2,637 additions and 94 deletions.
1,422 changes: 1,366 additions & 56 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ description = "Duf is a simple file server."
license = "MIT OR Apache-2.0"
homepage = "https://github.com/sigoden/duf"
repository = "https://github.com/sigoden/duf"
autotests = false
categories = ["command-line-utilities", "web-programming::http-server"]
keywords = ["static", "file", "server", "webdav", "cli"]

Expand Down Expand Up @@ -39,6 +38,19 @@ xml-rs = "0.8"
env_logger = { version = "0.9", default-features = false, features = ["humantime"] }
log = "0.4"

[dev-dependencies]
assert_cmd = "2"
reqwest = { version = "0.11", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
assert_fs = "1"
select = "0.5"
port_check = "0.1"
rstest = "0.13"
regex = "1"
pretty_assertions = "1.2"
url = "2"
diqwest = { version = "1", features = ["blocking"] }
predicates = "2"

[profile.release]
lto = true
strip = true
Expand Down
8 changes: 4 additions & 4 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ fn to_addr(ip: &str, port: u16) -> BoxResult<SocketAddr> {
// Load public certificate from file.
fn load_certs(filename: &str) -> BoxResult<Vec<Certificate>> {
// Open certificate file.
let certfile =
fs::File::open(&filename).map_err(|e| format!("Failed to open {}: {}", &filename, e))?;
let certfile = fs::File::open(&filename)
.map_err(|e| format!("Failed to access `{}`, {}", &filename, e))?;
let mut reader = io::BufReader::new(certfile);

// Load and return certificate.
Expand All @@ -221,8 +221,8 @@ fn load_certs(filename: &str) -> BoxResult<Vec<Certificate>> {
// Load private key from file.
fn load_private_key(filename: &str) -> BoxResult<PrivateKey> {
// Open keyfile.
let keyfile =
fs::File::open(&filename).map_err(|e| format!("Failed to open {}: {}", &filename, e))?;
let keyfile = fs::File::open(&filename)
.map_err(|e| format!("Failed to access `{}`, {}", &filename, e))?;
let mut reader = io::BufReader::new(keyfile);

// Load and return a single private key.
Expand Down
110 changes: 77 additions & 33 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ impl InnerService {
}

let req_path = req.uri().path();
let headers = req.headers();
let method = req.method().clone();

if req_path == "/favicon.ico" && method == Method::GET {
self.handle_send_favicon(req.headers(), &mut res).await?;
return Ok(res);
}

let path = match self.extract_path(req_path) {
Some(v) => v,
Expand All @@ -175,15 +182,6 @@ impl InnerService {
status!(res, StatusCode::NOT_FOUND);
return Ok(res);
}
if is_miss && path.ends_with("favicon.ico") {
*res.body_mut() = Body::from(FAVICON_ICO);
res.headers_mut()
.insert("content-type", "image/x-icon".parse().unwrap());
return Ok(res);
}

let headers = req.headers();
let method = req.method().clone();

match method {
Method::GET | Method::HEAD => {
Expand Down Expand Up @@ -247,12 +245,30 @@ impl InnerService {
status!(res, StatusCode::NOT_FOUND);
}
}
"MKCOL" if allow_upload && is_miss => self.handle_mkcol(path, &mut res).await?,
"COPY" if allow_upload && !is_miss => {
self.handle_copy(path, headers, &mut res).await?
"MKCOL" => {
if !allow_upload || !is_miss {
status!(res, StatusCode::FORBIDDEN);
} else {
self.handle_mkcol(path, &mut res).await?;
}
}
"COPY" => {
if !allow_upload {
status!(res, StatusCode::FORBIDDEN);
} else if is_miss {
status!(res, StatusCode::NOT_FOUND);
} else {
self.handle_copy(path, headers, &mut res).await?
}
}
"MOVE" if allow_upload && allow_delete && !is_miss => {
self.handle_move(path, headers, &mut res).await?
"MOVE" => {
if !allow_upload || !allow_delete {
status!(res, StatusCode::FORBIDDEN);
} else if is_miss {
status!(res, StatusCode::NOT_FOUND);
} else {
self.handle_move(path, headers, &mut res).await?
}
}
"LOCK" => {
// Fake lock
Expand Down Expand Up @@ -286,7 +302,13 @@ impl InnerService {
) -> BoxResult<()> {
ensure_path_parent(path).await?;

let mut file = fs::File::create(&path).await?;
let mut file = match fs::File::create(&path).await {
Ok(v) => v,
Err(_) => {
status!(res, StatusCode::FORBIDDEN);
return Ok(());
}
};

let body_with_io_error = req
.body_mut()
Expand Down Expand Up @@ -436,6 +458,25 @@ impl InnerService {
Ok(())
}

async fn handle_send_favicon(
&self,
headers: &HeaderMap<HeaderValue>,
res: &mut Response,
) -> BoxResult<()> {
let path = self.args.path.join("favicon.ico");
let meta = fs::metadata(&path).await.ok();
let is_file = meta.map(|v| v.is_file()).unwrap_or_default();
if is_file {
self.handle_send_file(path.as_path(), headers, false, res)
.await?;
} else {
*res.body_mut() = Body::from(FAVICON_ICO);
res.headers_mut()
.insert("content-type", "image/x-icon".parse().unwrap());
}
Ok(())
}

async fn handle_send_file(
&self,
path: &Path,
Expand Down Expand Up @@ -534,10 +575,10 @@ impl InnerService {
return Ok(());
}
},
None => 0,
None => 1,
};
let mut paths = vec![self.to_pathitem(path, &self.args.path).await?.unwrap()];
if depth > 0 {
if depth != 0 {
match self.list_dir(path, &self.args.path).await {
Ok(child) => paths.extend(child),
Err(_) => {
Expand Down Expand Up @@ -588,7 +629,7 @@ impl InnerService {

let meta = fs::symlink_metadata(path).await?;
if meta.is_dir() {
status!(res, StatusCode::BAD_REQUEST);
status!(res, StatusCode::FORBIDDEN);
return Ok(());
}

Expand Down Expand Up @@ -690,7 +731,10 @@ impl InnerService {
r#"
<title>Files in {}/ - Duf</title>
<style>{}</style>
<script>var DATA = {}; {}</script>
<script>
const DATA =
{}
{}</script>
"#,
rel_path.display(),
INDEX_CSS,
Expand Down Expand Up @@ -811,15 +855,9 @@ impl InnerService {
PathType::Dir | PathType::SymlinkDir => None,
PathType::File | PathType::SymlinkFile => Some(meta.len()),
};
let base_name = rel_path
.file_name()
.and_then(|v| v.to_str())
.unwrap_or("/")
.to_owned();
let name = normalize_path(rel_path);
Ok(Some(PathItem {
path_type,
base_name,
name,
mtime,
size,
Expand All @@ -839,7 +877,6 @@ struct IndexData {
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
struct PathItem {
path_type: PathType,
base_name: String,
name: String,
mtime: u64,
size: Option<u64>,
Expand All @@ -849,7 +886,7 @@ impl PathItem {
pub fn to_dav_xml(&self, prefix: &str) -> String {
let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822();
let href = encode_uri(&format!("{}{}", prefix, &self.name));
let displayname = escape_str_pcdata(&self.base_name);
let displayname = escape_str_pcdata(self.base_name());
match self.path_type {
PathType::Dir | PathType::SymlinkDir => format!(
r#"<D:response>
Expand Down Expand Up @@ -885,6 +922,12 @@ impl PathItem {
),
}
}
fn base_name(&self) -> &str {
Path::new(&self.name)
.file_name()
.and_then(|v| v.to_str())
.unwrap_or_default()
}
}

#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
Expand Down Expand Up @@ -1016,13 +1059,14 @@ fn print_listening(addr: &SocketAddr, prefix: &str, tls: bool) {
let addrs = retrieve_listening_addrs(addr);
let protocol = if tls { "https" } else { "http" };
if addrs.len() == 1 {
eprintln!("Listening on {}://{}{}", protocol, addr, prefix);
println!("Listening on {}://{}{}", protocol, addr, prefix);
} else {
eprintln!("Listening on:");
for addr in addrs {
eprintln!(" {}://{}{}", protocol, addr, prefix);
}
eprintln!();
let message = addrs
.iter()
.map(|addr| format!(" {}://{}{}", protocol, addr, prefix))
.collect::<Vec<String>>()
.join("\n");
println!("Listening on:\n{}\n", message);
}
}

Expand Down
61 changes: 61 additions & 0 deletions tests/allow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
mod fixtures;
mod utils;

use fixtures::{server, Error, TestServer};
use rstest::rstest;

#[rstest]
fn default_not_allow_upload(server: TestServer) -> Result<(), Error> {
let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 403);
Ok(())
}

#[rstest]
fn default_not_allow_delete(server: TestServer) -> Result<(), Error> {
let url = format!("{}test.html", server.url());
let resp = fetch!(b"DELETE", &url).send()?;
assert_eq!(resp.status(), 403);
Ok(())
}

#[rstest]
fn default_not_exist_dir(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}404/", server.url()))?;
assert_eq!(resp.status(), 404);
Ok(())
}

#[rstest]
fn allow_upload_not_exist_dir(
#[with(&["--allow-upload"])] server: TestServer,
) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}404/", server.url()))?;
assert_eq!(resp.status(), 200);
Ok(())
}

#[rstest]
fn allow_upload_no_override(#[with(&["--allow-upload"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}index.html", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 403);
Ok(())
}

#[rstest]
fn allow_delete_no_override(#[with(&["--allow-delete"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}index.html", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 403);
Ok(())
}

#[rstest]
fn allow_upload_delete_can_override(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}index.html", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 201);
Ok(())
}
38 changes: 38 additions & 0 deletions tests/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
mod fixtures;
mod utils;

use diqwest::blocking::WithDigestAuth;
use fixtures::{server, Error, TestServer};
use rstest::rstest;

#[rstest]
fn no_auth(#[with(&["--auth", "user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
assert_eq!(resp.status(), 401);
assert!(resp.headers().contains_key("www-authenticate"));
let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401);
Ok(())
}

#[rstest]
fn auth(#[with(&["--auth", "user:pass", "-A"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 401);
let resp = fetch!(b"PUT", &url)
.body(b"abc".to_vec())
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 201);
Ok(())
}

#[rstest]
fn auth_skip_access(
#[with(&["--auth", "user:pass", "--no-auth-access"])] server: TestServer,
) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
assert_eq!(resp.status(), 200);
Ok(())
}
Loading

0 comments on commit 471bca8

Please sign in to comment.