test: add integration tests (#36)

This commit is contained in:
sigoden 2022-06-12 08:43:50 +08:00 committed by GitHub
parent 6b01c143d9
commit 471bca86c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 2590 additions and 47 deletions

1328
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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"]
@ -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

View file

@ -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.
@ -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.

View file

@ -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,
@ -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 => {
@ -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?;
}
}
"MOVE" if allow_upload && allow_delete && !is_miss => {
self.handle_move(path, headers, &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 {
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
@ -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()
@ -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,
@ -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(_) => {
@ -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(());
}
@ -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,
@ -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,
@ -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>,
@ -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>
@ -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)]
@ -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);
}
}

61
tests/allow.rs Normal file
View file

@ -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
tests/auth.rs Normal file
View file

@ -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(())
}

80
tests/bind.rs Normal file
View file

@ -0,0 +1,80 @@
mod fixtures;
use fixtures::{port, server, tmpdir, Error, TestServer};
use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir;
use regex::Regex;
use rstest::rstest;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
#[rstest]
#[case(&["-b", "20.205.243.166"])]
fn bind_fails(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> {
Command::cargo_bin("duf")?
.env("RUST_LOG", "false")
.arg(tmpdir.path())
.arg("-p")
.arg(port.to_string())
.args(args)
.assert()
.stderr(predicates::str::contains("creating server listener"))
.failure();
Ok(())
}
#[rstest]
fn bind_ipv4(server: TestServer) -> Result<(), Error> {
assert!(reqwest::blocking::get(format!("http://127.0.0.1:{}", server.port()).as_str()).is_ok());
Ok(())
}
#[rstest]
fn bind_ipv6(#[with(&["-b", "::"])] server: TestServer) -> Result<(), Error> {
assert_eq!(
reqwest::blocking::get(format!("http://127.0.0.1:{}", server.port()).as_str()).is_ok(),
!cfg!(windows)
);
assert!(reqwest::blocking::get(format!("http://[::1]:{}", server.port()).as_str()).is_ok());
Ok(())
}
#[rstest]
#[case(&[] as &[&str])]
#[case(&["--path-prefix", "/prefix"])]
fn validate_printed_urls(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> {
let mut child = Command::cargo_bin("duf")?
.env("RUST_LOG", "false")
.arg(tmpdir.path())
.arg("-p")
.arg(port.to_string())
.args(args)
.stdout(Stdio::piped())
.spawn()?;
// WARN assumes urls list is terminated by an empty line
let url_lines = BufReader::new(child.stdout.take().unwrap())
.lines()
.map(|line| line.expect("Error reading stdout"))
.take_while(|line| !line.is_empty()) /* non-empty lines */
.collect::<Vec<_>>();
let url_lines = url_lines.join("\n");
let urls = Regex::new(r"http://[a-zA-Z0-9\.\[\]:/]+")
.unwrap()
.captures_iter(url_lines.as_str())
.map(|caps| caps.get(0).unwrap().as_str())
.collect::<Vec<_>>();
assert!(!urls.is_empty());
for url in urls {
reqwest::blocking::get(url)?.error_for_status()?;
}
child.kill()?;
Ok(())
}

37
tests/cors.rs Normal file
View file

@ -0,0 +1,37 @@
mod fixtures;
mod utils;
use fixtures::{server, Error, TestServer};
use rstest::rstest;
#[rstest]
fn cors(#[with(&["--cors"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"*"
);
assert_eq!(
resp.headers().get("access-control-allow-headers").unwrap(),
"range, content-type, accept, origin, www-authenticate"
);
Ok(())
}
#[rstest]
fn cors_options(#[with(&["--cors"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"OPTIONS", server.url()).send()?;
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"*"
);
assert_eq!(
resp.headers().get("access-control-allow-headers").unwrap(),
"range, content-type, accept, origin, www-authenticate"
);
Ok(())
}

29
tests/data/cert.pem Normal file
View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFCTCCAvGgAwIBAgIUcegjikATvwNSIbN43QybKWIcKSMwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDYxMTA4NTQyMloXDTMyMDYw
ODA4NTQyMlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAo2wdMbFPkX7CAF/Y+hVj5bwm4dlxhwW2Z9Ic2RZFC5w2
oK2XwyasDBEqDlgv/bN4xObAVlDZ/4/SuTVSDrNB8dtQl7GTWptpbFKJUdNocU88
wqd4k/cLZg2aiQqnZKD88w/AxXnYw+F8yU0pFGj9GX0S5at3/V1hrBVxVO8Y99bb
gnJA8NMm0Pw2xYZS++ULuzoECk0xbNdtbtPrIuweI5mMvsJvtiw67EIdl3N9Lj5p
L4a7X1C0Xk5H4mOcwM0qq3m31HsCW91PMCjU6suo764rx5Jqv0n9HCNxdiSEadCw
f+GrmKtFOw3DcGPETg5AJR8H3rG1agKKjI+vRtL/tZ7coFOhZKXdjGvvUFcWcqO+
GppHh16pzJDXi2qeD9Cu5b2ayM2uBnfV7Q3FjOeDqD+BCJ0ClaqNmAD9TF2htzdu
Inl+G3OJb4cqaYjaF5YmiZISfrimK5eR2I3et5cqnbuDHMKvDfUd9Jgj/2IqPOHJ
EguuXSO7WNKfQmlTv7EN/xrD6jiB/M8ADaSxjCqTbtKNyCbJlu2Wy9WlDXwPkNW8
g70T4Br4U4Iy3N/0w2lAAhiizdC2jkehSKmWE2nmixGSXxkSOMgXQXDJ9RBtDQfd
8ym/ADfyVndUSnHvf9jCH1NPHlFbB7RVSvUHX22Qq63NUvhV32ct+/IyD/qPpl0C
AwEAAaNTMFEwHQYDVR0OBBYEFKwSSbPXBIkmzja3/cNJyqhWy96WMB8GA1UdIwQY
MBaAFKwSSbPXBIkmzja3/cNJyqhWy96WMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggIBAHcrdu1nGDN5YvcHXzbBx73AC921fmn5xxzeFRO7af157g5h
4zornLMk4Obp+UGkMbWK4K0NAQXKKm5WjcmoOHNRg7TgTE7b1gcVuS4phdwlIqA6
eZGg+NWZyeaIJNjdHgWgGoe+S+5Ne1I7sDKiEXrOzITJrDcQgBKFF08kqT6UNY2W
q90m+olPtrewAMgWllpxJ90u4qifPcwP+neDZJim9MhVYtHHeFsmyzlS185iasj8
sxvp5HDTopmz0tDuiLHvOMKmyf7vapsnbqEGngQi2qV9rBmldyRLnWSe8u/FN31f
zhSk1ikSm1cQ/iyL898XexSmTafyaF8ELswdIMHkGZkVQurWeKn3/CEDXokXkpMI
4dlCSgM7SU+XtcjtXbR8/pHpcW2ZnBR0la/qIv81aNKkJeUkTcPC8BUv4jI/oT6z
LRrvRjMnHJjnADACuutlNRU4/e7h1XuvlXgFHsp63k7GJXouoIwdHjfkErZXsoEX
WeS+pPatkT7wbhfgYVwglMRIpgCu++htSRCV/lbSuYzCG6mKtxJyy4eslSjpHNPG
wELDKgzsgLtuTyNfP458O9i8x6wf9J6eVaHe3nqgqkOnnmQxEYnsPaFUMWG1/DYi
U+mA/VdQrPe3J4Z082sCe4MVmTzWlWCDpNFFQpv51NbWzc/kuIZuJCAwoZD0
-----END CERTIFICATE-----

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -keyout key_pkcs8.pem -out cert.pem -nodes -days 3650
openssl rsa -in key_pkcs8.pem -out key_pkcs1.pem

51
tests/data/key_pkcs1.pem Normal file
View file

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

52
tests/data/key_pkcs8.pem Normal file
View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCjbB0xsU+RfsIA
X9j6FWPlvCbh2XGHBbZn0hzZFkULnDagrZfDJqwMESoOWC/9s3jE5sBWUNn/j9K5
NVIOs0Hx21CXsZNam2lsUolR02hxTzzCp3iT9wtmDZqJCqdkoPzzD8DFedjD4XzJ
TSkUaP0ZfRLlq3f9XWGsFXFU7xj31tuCckDw0ybQ/DbFhlL75Qu7OgQKTTFs121u
0+si7B4jmYy+wm+2LDrsQh2Xc30uPmkvhrtfULReTkfiY5zAzSqrebfUewJb3U8w
KNTqy6jvrivHkmq/Sf0cI3F2JIRp0LB/4auYq0U7DcNwY8RODkAlHwfesbVqAoqM
j69G0v+1ntygU6Fkpd2Ma+9QVxZyo74amkeHXqnMkNeLap4P0K7lvZrIza4Gd9Xt
DcWM54OoP4EInQKVqo2YAP1MXaG3N24ieX4bc4lvhyppiNoXliaJkhJ+uKYrl5HY
jd63lyqdu4Mcwq8N9R30mCP/Yio84ckSC65dI7tY0p9CaVO/sQ3/GsPqOIH8zwAN
pLGMKpNu0o3IJsmW7ZbL1aUNfA+Q1byDvRPgGvhTgjLc3/TDaUACGKLN0LaOR6FI
qZYTaeaLEZJfGRI4yBdBcMn1EG0NB93zKb8AN/JWd1RKce9/2MIfU08eUVsHtFVK
9QdfbZCrrc1S+FXfZy378jIP+o+mXQIDAQABAoICAA8zb0PACnauI72FLNmWQ8SK
y9MWNskobt8ZXDWVLLb3wN0RSJyObFJzBkZvTYd7ujAWhq2At8ZdymwP2aIkam3k
yIpTqjEzY4r/4mYKFBz2EeDJUx3wTM8hVM6KfyeNEfpJ7NRxkQgnYUQYOUn8ug40
5B0Y5gBbP0qSdAhCdUAYbDKmDgRYn7Owwid9ZxqnI4A21UlgBMjrAPLeI3EMJYGK
Nt1HmLyJQLHZNIdx5qL2izIwhh1HlH9xKJ6WRW5ppx5sI/1Ps9h3rXX9VlDAwOYK
fWgC5osEhtanNVaX7vkowEIk66E3zSjBDvzVSt031h+d002TmhxSQDDy7IrZ9Y75
Nx0V10evHS2cQiyeTPL4m2n+PkSr8kZWVJjTd1yTgUiMNIz8z7oc/lHaN8YKyK2i
ejTdcaPM2wmi7RSeit1j5UviqiUi2ii9aR83mG52Zuy17iBLf91K+2gHK2jlICpQ
ZRLRU2OGxTnD4jIwUr6QiaUm2Fvf4Y0X/uE0+fgzC7EgVcTLVxU+77DCgWgvn7uy
2nryaVievGYQgU4k1yMXW6t+oIwBdZfFp37vVO0GJGK3m0BlUeNPP00GTK8Jxc4S
rdEwSMDMK1MaV25TA1CmaZ0LIeTZKetaJ5UurJO3nmvUWHMRnQCoeJaYcI1GerP8
8kUEgp9f1gqeQrbFUCABAoIBAQDSNy9nRx8YYijxYb+vmaK2mdV5DL1dn4hzsmz1
Pvdktwb6qj/ZcOERdxHg4pmhIwgkii0wgnVNkp0031CAWnsi5RadimenaH4fJ+/y
HsMdnCiEfDhiz0Alj2PhZUY/80pVw+tNj8WQndUcqJV3XQVeg2fSoTDnLehrjNG1
FgWunuC86hEzChYAMs3+HC5DZBGsE+3TO2Gp9t5O1ATiD3nS2SozLpXq5qGRkS9C
ZJ3VSkPfloYozDE6kr17D0IAp25+0NU673He8oA6HS5ez0PExR8kUp/whIjYkHe8
WasAnaXqpIYR2hV2JeTAd1GxoQElsOtHe6pHmi8zSpWTazbdAoIBAQDHA+qq/lK4
88ySwEIFJ2zr3ZwSxcDXFu/DUiw66CobbZ8I8NIivTD1znFTdBK/YEWZoIslARUA
+gj+Oo8yoh05YL2VJcS4rDb3uLSv8YAxw1x7U35fPVqHJUTcCTrknCL++CvQPfy2
G1AmUX/8EHSE8W++3gZDYTg7kQvBNcg+vjrBAUZ/Qh+q+5+kegqKzMnkXmWLJO0H
nQngizYIpQxS5erpeVYysI0vJoZbjZy21yKBs0A9qCdAA0cVQg70zTp0BT1ZWdQk
b140N3+0PDRcMuNGpNFeU2ipZHBYyXfxleIcj2gek6oeN4Bd6927meSqya1JDnQu
Ic6Ur39w7nWBAoIBAEeWP3BeSfqVwvegoBR+r2ibrpTQf9RYNbGgu3YxATKtGZVX
UPMNe7fk4pPA1yyGr4kx/P2eD7BhE2QkAFgVeAWEGlWAApiB/yFBCndbfnqg6TNk
0hbkGSYxVIQ3Ztr0UQYv9/ur0E3VUfO2fV/ur5tmCH80IWE4wwONKwUOANVciZAL
7duBvchHhNraYe7VLyd4ie77M+Ebjs0tapRJTOU+yx4u6SLbc34NARkNBqrR5ZdI
vVs7XjBiNGCp64IpgnlxSIwPaqnPz1b7BQWIc72Ym2hVMt5HZArL99qOdqt5BD2J
1ZZSvChF80aP26Qv/iIFw7nOJUjI+vXumk1fn4kCggEAKWSgJ4aUMBHNR+yjDq1f
k0NZlfH5pQMJRWfO+UWWlHPdcx+bpUwSZd3VZlUrHRDqCQUCS+UIuFZJDo+hfxeG
2otLgz0CyKJRIH8TXdDuJ+uJTcFU9HwvWxKeWuG0lR4AjWWSv1JVQI83wXCMHSJT
Cd+prVpClTsgVgtaFYqZJqusTr50Kj3XllmVn8EedjYX+97CYgmBH2dDSafKykuB
37AghwUMr6hXBSqPIXW31bOzYqSkxO43k7Ha1AoUUEhrUvxQAUiUvitHvbaZYiRv
g4m2Cdh4D5VrnGfekij9QIs7J1OQhWOTK4vBsEdBrCZbEEa4Owy9j2bKr9Q8voFq
gQKCAQEAl5jk6BVskcHphqB4G1QE97S0shuetg9hcg+wvJxn49ejvcBJulseHpq/
sJ0/bzw6677MTQ/qIB2sitTjWRz7ZPHsrxtBvSkOtfSlBlAAjOgFYqi7CtryRJ5L
V2SIs4uI6GdgOf45gTi03l497sCUGTtvX/HJs24LJNh1ofW49ojnpFo8HxoclefG
08dS9ibSgx+bcQ+OAX8WMZ2kTus9x1haopQSTlQ+ZwZVWz7IdFKwQOdrnNcGpCgN
ouf7sp+Bw7xTGGEvZx1xDa+wLULtAg+7TjeUAheB7l37WE3CtmMfD/1KRXdQi2cQ
afHJpnY0yZEqN1PeDaKVOCQAHbD8qQ==
-----END PRIVATE KEY-----

25
tests/favicon.rs Normal file
View file

@ -0,0 +1,25 @@
mod fixtures;
mod utils;
use fixtures::{server, Error, TestServer};
use rstest::rstest;
#[rstest]
fn default_favicon(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}favicon.ico", server.url()))?;
assert_eq!(resp.status(), 200);
assert_eq!(resp.headers().get("content-type").unwrap(), "image/x-icon");
Ok(())
}
#[rstest]
fn exist_favicon(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}favicon.ico", server.url());
let data = b"abc";
let resp = fetch!(b"PUT", &url).body(data.to_vec()).send()?;
assert_eq!(resp.status(), 201);
let resp = reqwest::blocking::get(url)?;
assert_eq!(resp.status(), 200);
assert_eq!(resp.bytes()?, data.to_vec());
Ok(())
}

191
tests/fixtures.rs Normal file
View file

@ -0,0 +1,191 @@
use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir;
use assert_fs::prelude::*;
use port_check::free_local_port;
use reqwest::Url;
use rstest::fixture;
use std::process::{Child, Command, Stdio};
use std::thread::sleep;
use std::time::{Duration, Instant};
#[allow(dead_code)]
pub type Error = Box<dyn std::error::Error>;
/// File names for testing purpose
#[allow(dead_code)]
pub static FILES: &[&str] = &[
"test.txt",
"test.html",
"index.html",
"test.mkv",
#[cfg(not(windows))]
"test \" \' & < >.csv",
"😀.data",
"⎙.mp4",
"#[]{}()@!$&'`+,;= %20.test",
#[cfg(unix)]
":?#[]{}<>()@!$&'`|*+,;= %20.test",
#[cfg(not(windows))]
"foo\\bar.test",
];
/// Directory names for testing purpose
#[allow(dead_code)]
pub static DIR_NO_INDEX: &str = "dir-no-index/";
/// Directory names for testing purpose
#[allow(dead_code)]
pub static DIRECTORIES: &[&str] = &["dira/", "dirb/", "dirc/", DIR_NO_INDEX];
/// Name of a deeply nested file
#[allow(dead_code)]
pub static DEEPLY_NESTED_FILE: &str = "very/deeply/nested/test.rs";
/// Test fixture which creates a temporary directory with a few files and directories inside.
/// The directories also contain files.
#[fixture]
#[allow(dead_code)]
pub fn tmpdir() -> TempDir {
let tmpdir = assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests");
for file in FILES {
tmpdir
.child(file)
.write_str(&format!("This is {}", file))
.expect("Couldn't write to file");
}
for directory in DIRECTORIES {
for file in FILES {
if *directory == DIR_NO_INDEX {
continue;
}
tmpdir
.child(format!("{}{}", directory, file))
.write_str(&format!("This is {}{}", directory, file))
.expect("Couldn't write to file");
}
}
tmpdir
.child(&DEEPLY_NESTED_FILE)
.write_str("File in a deeply nested directory.")
.expect("Couldn't write to file");
tmpdir
}
/// Get a free port.
#[fixture]
#[allow(dead_code)]
pub fn port() -> u16 {
free_local_port().expect("Couldn't find a free local port")
}
/// Run miniserve as a server; Start with a temporary directory, a free port and some
/// optional arguments then wait for a while for the server setup to complete.
#[fixture]
#[allow(dead_code)]
pub fn server<I>(#[default(&[] as &[&str])] args: I) -> TestServer
where
I: IntoIterator + Clone,
I::Item: AsRef<std::ffi::OsStr>,
{
let port = port();
let tmpdir = tmpdir();
let child = Command::cargo_bin("duf")
.expect("Couldn't find test binary")
.env("RUST_LOG", "false")
.arg(tmpdir.path())
.arg("-p")
.arg(port.to_string())
.args(args.clone())
.stdout(Stdio::null())
.spawn()
.expect("Couldn't run test binary");
let is_tls = args
.into_iter()
.any(|x| x.as_ref().to_str().unwrap().contains("tls"));
wait_for_port(port);
TestServer::new(port, tmpdir, child, is_tls)
}
/// Same as `server()` but ignore stderr
#[fixture]
#[allow(dead_code)]
pub fn server_no_stderr<I>(#[default(&[] as &[&str])] args: I) -> TestServer
where
I: IntoIterator + Clone,
I::Item: AsRef<std::ffi::OsStr>,
{
let port = port();
let tmpdir = tmpdir();
let child = Command::cargo_bin("duf")
.expect("Couldn't find test binary")
.env("RUST_LOG", "false")
.arg(tmpdir.path())
.arg("-p")
.arg(port.to_string())
.args(args.clone())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Couldn't run test binary");
let is_tls = args
.into_iter()
.any(|x| x.as_ref().to_str().unwrap().contains("tls"));
wait_for_port(port);
TestServer::new(port, tmpdir, child, is_tls)
}
/// Wait a max of 1s for the port to become available.
fn wait_for_port(port: u16) {
let start_wait = Instant::now();
while !port_check::is_port_reachable(format!("localhost:{}", port)) {
sleep(Duration::from_millis(100));
if start_wait.elapsed().as_secs() > 1 {
panic!("timeout waiting for port {}", port);
}
}
}
#[allow(dead_code)]
pub struct TestServer {
port: u16,
tmpdir: TempDir,
child: Child,
is_tls: bool,
}
#[allow(dead_code)]
impl TestServer {
pub fn new(port: u16, tmpdir: TempDir, child: Child, is_tls: bool) -> Self {
Self {
port,
tmpdir,
child,
is_tls,
}
}
pub fn url(&self) -> Url {
let protocol = if self.is_tls { "https" } else { "http" };
Url::parse(&format!("{}://localhost:{}", protocol, self.port)).unwrap()
}
pub fn path(&self) -> &std::path::Path {
self.tmpdir.path()
}
pub fn port(&self) -> u16 {
self.port
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.child.kill().expect("Couldn't kill test server");
self.child.wait().unwrap();
}
}

184
tests/http.rs Normal file
View file

@ -0,0 +1,184 @@
mod fixtures;
mod utils;
use fixtures::{server, Error, TestServer};
use rstest::rstest;
#[rstest]
fn get_dir(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
assert_index_resp!(resp);
Ok(())
}
#[rstest]
fn head_dir(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", server.url()).send()?;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"text/html; charset=utf-8"
);
assert_eq!(resp.text()?, "");
Ok(())
}
#[rstest]
fn get_dir_404(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}404/", server.url()))?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn head_dir_404(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}404/", server.url())).send()?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn get_dir_zip(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}?zip", server.url()))?;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/zip"
);
assert!(resp.headers().contains_key("content-disposition"));
Ok(())
}
#[rstest]
fn head_dir_zip(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}?zip", server.url())).send()?;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/zip"
);
assert!(resp.headers().contains_key("content-disposition"));
assert_eq!(resp.text()?, "");
Ok(())
}
#[rstest]
fn get_dir_search(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}?q={}", server.url(), "test.html"))?;
assert_eq!(resp.status(), 200);
let paths = utils::retrive_index_paths(&resp.text()?);
assert!(!paths.is_empty());
for p in paths {
assert!(p.contains(&"test.html"));
}
Ok(())
}
#[rstest]
fn head_dir_search(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}?q={}", server.url(), "test.html")).send()?;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"text/html; charset=utf-8"
);
assert_eq!(resp.text()?, "");
Ok(())
}
#[rstest]
fn get_file(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}index.html", server.url()))?;
assert_eq!(resp.status(), 200);
assert_eq!(resp.headers().get("content-type").unwrap(), "text/html");
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
assert!(resp.headers().contains_key("etag"));
assert!(resp.headers().contains_key("last-modified"));
assert!(resp.headers().contains_key("content-length"));
assert_eq!(resp.text()?, "This is index.html");
Ok(())
}
#[rstest]
fn head_file(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}index.html", server.url())).send()?;
assert_eq!(resp.status(), 200);
assert_eq!(resp.headers().get("content-type").unwrap(), "text/html");
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
assert!(resp.headers().contains_key("etag"));
assert!(resp.headers().contains_key("last-modified"));
assert!(resp.headers().contains_key("content-length"));
assert_eq!(resp.text()?, "");
Ok(())
}
#[rstest]
fn get_file_404(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}404", server.url()))?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn head_file_404(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}404", server.url())).send()?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn options_dir(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"OPTIONS", format!("{}index.html", server.url())).send()?;
assert_eq!(resp.status(), 204);
assert_eq!(
resp.headers().get("allow").unwrap(),
"GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"
);
assert_eq!(resp.headers().get("dav").unwrap(), "1");
Ok(())
}
#[rstest]
fn put_file(#[with(&["-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(), 201);
let resp = reqwest::blocking::get(url)?;
assert_eq!(resp.status(), 200);
Ok(())
}
#[rstest]
fn put_file_create_dir(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}xyz/file1", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 201);
let resp = reqwest::blocking::get(url)?;
assert_eq!(resp.status(), 200);
Ok(())
}
#[rstest]
fn put_file_conflict_dir(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}dira", server.url());
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
assert_eq!(resp.status(), 403);
Ok(())
}
#[rstest]
fn delete_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let url = format!("{}test.html", server.url());
let resp = fetch!(b"DELETE", &url).send()?;
assert_eq!(resp.status(), 204);
let resp = reqwest::blocking::get(url)?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn delete_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"DELETE", format!("{}file1", server.url())).send()?;
assert_eq!(resp.status(), 404);
Ok(())
}

30
tests/path_prefix.rs Normal file
View file

@ -0,0 +1,30 @@
mod fixtures;
mod utils;
use fixtures::{server, Error, TestServer};
use rstest::rstest;
#[rstest]
fn path_prefix_index(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}{}", server.url(), "xyz"))?;
assert_index_resp!(resp);
Ok(())
}
#[rstest]
fn path_prefix_file(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}{}/index.html", server.url(), "xyz"))?;
assert_eq!(resp.status(), 200);
assert_eq!(resp.text()?, "This is index.html");
Ok(())
}
#[rstest]
fn path_prefix_propfind(
#[with(&["--path-prefix", "xyz"])] server: TestServer,
) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", format!("{}{}", server.url(), "xyz")).send()?;
let text = resp.text()?;
assert!(text.contains("<D:href>/xyz/</D:href>"));
Ok(())
}

35
tests/render.rs Normal file
View file

@ -0,0 +1,35 @@
mod fixtures;
use fixtures::{server, Error, TestServer, DIR_NO_INDEX};
use rstest::rstest;
#[rstest]
fn render_index(#[with(&["--render-index"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
let text = resp.text()?;
assert_eq!(text, "This is index.html");
Ok(())
}
#[rstest]
fn render_index_404(#[with(&["--render-index"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}/{}", server.url(), DIR_NO_INDEX))?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn render_spa(#[with(&["--render-spa"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
let text = resp.text()?;
assert_eq!(text, "This is index.html");
Ok(())
}
#[rstest]
fn render_spa_no_404(#[with(&["--render-spa"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}/{}", server.url(), DIR_NO_INDEX))?;
let text = resp.text()?;
assert_eq!(text, "This is index.html");
Ok(())
}

46
tests/symlink.rs Normal file
View file

@ -0,0 +1,46 @@
mod fixtures;
mod utils;
use assert_fs::fixture::TempDir;
use fixtures::{server, tmpdir, Error, TestServer};
use rstest::rstest;
#[cfg(unix)]
use std::os::unix::fs::symlink as symlink_dir;
#[cfg(windows)]
use std::os::windows::fs::symlink_dir;
#[rstest]
fn default_not_allow_symlink(server: TestServer, tmpdir: TempDir) -> Result<(), Error> {
// Create symlink directory "foo" to point outside the root
let dir = "foo";
symlink_dir(tmpdir.path(), server.path().join(dir)).expect("Couldn't create symlink");
let resp = reqwest::blocking::get(format!("{}{}", server.url(), dir))?;
assert_eq!(resp.status(), 404);
let resp = reqwest::blocking::get(format!("{}{}/index.html", server.url(), dir))?;
assert_eq!(resp.status(), 404);
let resp = reqwest::blocking::get(server.url())?;
let paths = utils::retrive_index_paths(&resp.text()?);
assert!(!paths.is_empty());
assert!(!paths.contains(&format!("{}/", dir)));
Ok(())
}
#[rstest]
fn allow_symlink(
#[with(&["--allow-symlink"])] server: TestServer,
tmpdir: TempDir,
) -> Result<(), Error> {
// Create symlink directory "foo" to point outside the root
let dir = "foo";
symlink_dir(tmpdir.path(), server.path().join(dir)).expect("Couldn't create symlink");
let resp = reqwest::blocking::get(format!("{}{}", server.url(), dir))?;
assert_eq!(resp.status(), 200);
let resp = reqwest::blocking::get(format!("{}{}/index.html", server.url(), dir))?;
assert_eq!(resp.status(), 200);
let resp = reqwest::blocking::get(server.url())?;
let paths = utils::retrive_index_paths(&resp.text()?);
assert!(!paths.is_empty());
assert!(paths.contains(&format!("{}/", dir)));
Ok(())
}

51
tests/tls.rs Normal file
View file

@ -0,0 +1,51 @@
mod fixtures;
mod utils;
use assert_cmd::Command;
use fixtures::{server, Error, TestServer};
use predicates::str::contains;
use reqwest::blocking::ClientBuilder;
use rstest::rstest;
/// Can start the server with TLS and receive encrypted responses.
#[rstest]
#[case(server(&[
"--tls-cert", "tests/data/cert.pem",
"--tls-key", "tests/data/key_pkcs8.pem",
]))]
#[case(server(&[
"--tls-cert", "tests/data/cert.pem",
"--tls-key", "tests/data/key_pkcs1.pem",
]))]
fn tls_works(#[case] server: TestServer) -> Result<(), Error> {
let client = ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()?;
let resp = client.get(server.url()).send()?.error_for_status()?;
assert_index_resp!(resp);
Ok(())
}
/// Wrong path for cert throws error.
#[rstest]
fn wrong_path_cert() -> Result<(), Error> {
Command::cargo_bin("duf")?
.args(&["--tls-cert", "wrong", "--tls-key", "tests/data/key.pem"])
.assert()
.failure()
.stderr(contains("error: Failed to access `wrong`"));
Ok(())
}
/// Wrong paths for key throws errors.
#[rstest]
fn wrong_path_key() -> Result<(), Error> {
Command::cargo_bin("duf")?
.args(&["--tls-cert", "tests/data/cert.pem", "--tls-key", "wrong"])
.assert()
.failure()
.stderr(contains("error: Failed to access `wrong`"));
Ok(())
}

61
tests/utils.rs Normal file
View file

@ -0,0 +1,61 @@
use serde_json::Value;
use std::collections::HashSet;
#[macro_export]
macro_rules! assert_index_resp {
($resp:ident) => {
assert_index_resp!($resp, self::fixtures::FILES)
};
($resp:ident, $files:expr) => {
assert_eq!($resp.status(), 200);
let body = $resp.text()?;
let paths = self::utils::retrive_index_paths(&body);
assert!(!paths.is_empty());
for file in $files {
assert!(paths.contains(&file.to_string()));
}
};
}
#[macro_export]
macro_rules! fetch {
($method:literal, $url:expr) => {
reqwest::blocking::Client::new().request(hyper::Method::from_bytes($method)?, $url)
};
}
#[allow(dead_code)]
pub fn retrive_index_paths(index: &str) -> HashSet<String> {
retrive_index_paths_impl(index).unwrap_or_default()
}
#[allow(dead_code)]
pub fn encode_uri(v: &str) -> String {
let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
parts.join("/")
}
fn retrive_index_paths_impl(index: &str) -> Option<HashSet<String>> {
let lines: Vec<&str> = index.lines().collect();
let (i, _) = lines
.iter()
.enumerate()
.find(|(_, v)| v.contains("const DATA"))?;
let line = lines.get(i + 1)?;
let value: Value = line.parse().ok()?;
let paths = value
.get("paths")?
.as_array()?
.iter()
.flat_map(|v| {
let name = v.get("name")?.as_str()?;
let path_type = v.get("path_type")?.as_str()?;
if path_type.ends_with("Dir") {
Some(format!("{}/", name))
} else {
Some(name.to_owned())
}
})
.collect();
Some(paths)
}

203
tests/webdav.rs Normal file
View file

@ -0,0 +1,203 @@
mod fixtures;
mod utils;
use fixtures::{server, Error, TestServer, FILES};
use rstest::rstest;
use xml::escape::escape_str_pcdata;
#[rstest]
fn propfind_dir(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", format!("{}dira", server.url())).send()?;
assert_eq!(resp.status(), 207);
let body = resp.text()?;
assert!(body.contains("<D:href>/dira</D:href>"));
assert!(body.contains("<D:displayname>dira</D:displayname>"));
for f in FILES {
assert!(body.contains(&format!("<D:href>/dira/{}</D:href>", utils::encode_uri(f))));
assert!(body.contains(&format!(
"<D:displayname>{}</D:displayname>",
escape_str_pcdata(f)
)));
}
Ok(())
}
#[rstest]
fn propfind_dir_depth0(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", format!("{}dira", server.url()))
.header("depth", "0")
.send()?;
assert_eq!(resp.status(), 207);
let body = resp.text()?;
assert!(body.contains("<D:href>/dira</D:href>"));
assert!(body.contains("<D:displayname>dira</D:displayname>"));
assert_eq!(
body.lines()
.filter(|v| *v == "<D:status>HTTP/1.1 200 OK</D:status>")
.count(),
1
);
Ok(())
}
#[rstest]
fn propfind_404(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", format!("{}404", server.url())).send()?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn propfind_file(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPFIND", format!("{}test.html", server.url())).send()?;
assert_eq!(resp.status(), 207);
let body = resp.text()?;
assert!(body.contains("<D:href>/test.html</D:href>"));
assert!(body.contains("<D:displayname>test.html</D:displayname>"));
assert_eq!(
body.lines()
.filter(|v| *v == "<D:status>HTTP/1.1 200 OK</D:status>")
.count(),
1
);
Ok(())
}
#[rstest]
fn proppatch_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPPATCH", format!("{}test.html", server.url())).send()?;
assert_eq!(resp.status(), 207);
let body = resp.text()?;
assert!(body.contains("<D:href>/test.html</D:href>"));
Ok(())
}
#[rstest]
fn proppatch_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"PROPPATCH", format!("{}404", server.url())).send()?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn mkcol_dir(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"MKCOL", format!("{}newdir", server.url())).send()?;
assert_eq!(resp.status(), 201);
Ok(())
}
#[rstest]
fn mkcol_not_allow_upload(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"MKCOL", format!("{}newdir", server.url())).send()?;
assert_eq!(resp.status(), 403);
Ok(())
}
#[rstest]
fn copy_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"COPY", format!("{}test.html", server.url()))
.header("Destination", &new_url)
.send()?;
assert_eq!(resp.status(), 204);
let resp = reqwest::blocking::get(new_url)?;
assert_eq!(resp.status(), 200);
Ok(())
}
#[rstest]
fn copy_not_allow_upload(server: TestServer) -> Result<(), Error> {
let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"COPY", format!("{}test.html", server.url()))
.header("Destination", &new_url)
.send()?;
assert_eq!(resp.status(), 403);
Ok(())
}
#[rstest]
fn copy_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"COPY", format!("{}404", server.url()))
.header("Destination", &new_url)
.send()?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn move_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let origin_url = format!("{}test.html", server.url());
let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"MOVE", &origin_url)
.header("Destination", &new_url)
.send()?;
assert_eq!(resp.status(), 204);
let resp = reqwest::blocking::get(new_url)?;
assert_eq!(resp.status(), 200);
let resp = reqwest::blocking::get(origin_url)?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn move_not_allow_upload(#[with(&["--allow-delete"])] server: TestServer) -> Result<(), Error> {
let origin_url = format!("{}test.html", server.url());
let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"MOVE", &origin_url)
.header("Destination", &new_url)
.send()?;
assert_eq!(resp.status(), 403);
Ok(())
}
#[rstest]
fn move_not_allow_delete(#[with(&["--allow-upload"])] server: TestServer) -> Result<(), Error> {
let origin_url = format!("{}test.html", server.url());
let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"MOVE", &origin_url)
.header("Destination", &new_url)
.send()?;
assert_eq!(resp.status(), 403);
Ok(())
}
#[rstest]
fn move_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let new_url = format!("{}test2.html", server.url());
let resp = fetch!(b"MOVE", format!("{}404", server.url()))
.header("Destination", &new_url)
.send()?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn lock_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"LOCK", format!("{}test.html", server.url())).send()?;
assert_eq!(resp.status(), 200);
let body = resp.text()?;
assert!(body.contains("<D:href>/test.html</D:href>"));
Ok(())
}
#[rstest]
fn lock_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"LOCK", format!("{}404", server.url())).send()?;
assert_eq!(resp.status(), 404);
Ok(())
}
#[rstest]
fn unlock_file(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"LOCK", format!("{}test.html", server.url())).send()?;
assert_eq!(resp.status(), 200);
Ok(())
}
#[rstest]
fn unlock_file_404(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"LOCK", format!("{}404", server.url())).send()?;
assert_eq!(resp.status(), 404);
Ok(())
}