mod fixtures; use assert_cmd::prelude::*; use assert_fs::fixture::TempDir; use fixtures::{ port, server, server_no_stderr, tmpdir, Error, TestServer, DIRECTORIES, FILES, HIDDEN_DIRECTORIES, HIDDEN_FILES, }; use http::StatusCode; use regex::Regex; use rstest::rstest; use select::document::Document; use select::node::Node; use std::process::{Command, Stdio}; use std::thread::sleep; use std::time::Duration; #[cfg(unix)] use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file}; #[cfg(windows)] use std::os::windows::fs::{symlink_dir, symlink_file}; #[rstest] fn serves_requests_with_no_options(tmpdir: TempDir) -> Result<(), Error> { let mut child = Command::cargo_bin("miniserve")? .arg(tmpdir.path()) .stdout(Stdio::null()) .spawn()?; sleep(Duration::from_secs(1)); let body = reqwest::blocking::get("http://localhost:8080")?.error_for_status()?; let parsed = Document::from_read(body)?; for &file in FILES { assert!(parsed.find(|x: &Node| x.text() == file).next().is_some()); } child.kill()?; Ok(()) } #[rstest] fn serves_requests_with_non_default_port(server: TestServer) -> Result<(), Error> { let body = reqwest::blocking::get(server.url())?.error_for_status()?; let parsed = Document::from_read(body)?; for &file in FILES { let f = parsed.find(|x: &Node| x.text() == file).next().unwrap(); reqwest::blocking::get(server.url().join(f.attr("href").unwrap())?)?.error_for_status()?; assert_eq!( format!("/{}", file), percent_encoding::percent_decode_str(f.attr("href").unwrap()).decode_utf8_lossy(), ); } for &directory in DIRECTORIES { assert!(parsed .find(|x: &Node| x.text() == directory) .next() .is_some()); let dir_body = reqwest::blocking::get(server.url().join(&directory)?)?.error_for_status()?; let dir_body_parsed = Document::from_read(dir_body)?; for &file in FILES { assert!(dir_body_parsed .find(|x: &Node| x.text() == file) .next() .is_some()); } } Ok(()) } #[rstest] fn serves_requests_hidden_files(#[with(&["--hidden"])] server: TestServer) -> Result<(), Error> { let body = reqwest::blocking::get(server.url())?.error_for_status()?; let parsed = Document::from_read(body)?; for &file in FILES.into_iter().chain(HIDDEN_FILES) { let f = parsed.find(|x: &Node| x.text() == file).next().unwrap(); assert_eq!( format!("/{}", file), percent_encoding::percent_decode_str(f.attr("href").unwrap()).decode_utf8_lossy(), ); } for &directory in DIRECTORIES.into_iter().chain(HIDDEN_DIRECTORIES) { assert!(parsed .find(|x: &Node| x.text() == directory) .next() .is_some()); let dir_body = reqwest::blocking::get(server.url().join(&directory)?)?.error_for_status()?; let dir_body_parsed = Document::from_read(dir_body)?; for &file in FILES.into_iter().chain(HIDDEN_FILES) { assert!(dir_body_parsed .find(|x: &Node| x.text() == file) .next() .is_some()); } } Ok(()) } #[rstest] fn serves_requests_no_hidden_files_without_flag(server: TestServer) -> Result<(), Error> { let body = reqwest::blocking::get(server.url())?.error_for_status()?; let parsed = Document::from_read(body)?; for &hidden_item in HIDDEN_FILES.into_iter().chain(HIDDEN_DIRECTORIES) { assert!(parsed .find(|x: &Node| x.text() == hidden_item) .next() .is_none()); let resp = reqwest::blocking::get(server.url().join(&hidden_item)?)?; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } Ok(()) } #[rstest] #[case(true, false, server(&["--no-symlinks"]))] #[case(true, true, server(&["--no-symlinks", "--show-symlink-info"]))] #[case(false, false, server(None::<&str>))] fn serves_requests_symlinks( #[case] no_symlinks: bool, #[case] show_symlink_info: bool, #[case] server: TestServer, ) -> Result<(), Error> { let files = &["symlink-file.html"]; let dirs = &["symlink-dir/"]; let broken = &["symlink broken"]; for &directory in dirs { let orig = DIRECTORIES[0].strip_suffix("/").unwrap(); let link = server.path().join(directory.strip_suffix("/").unwrap()); symlink_dir(orig, link).expect("Couldn't create symlink"); } for &file in files { symlink_file(FILES[0], server.path().join(file)).expect("Couldn't create symlink"); } for &file in broken { symlink_file("should-not-exist.xxx", server.path().join(file)) .expect("Couldn't create symlink"); } let body = reqwest::blocking::get(server.url())?.error_for_status()?; let parsed = Document::from_read(body)?; for &entry in files.into_iter().chain(dirs) { let node = parsed .find(|x: &Node| x.name().unwrap_or_default() == "a" && x.text() == entry) .next(); assert_eq!(node.is_none(), no_symlinks); if node.is_some() && show_symlink_info { assert_eq!(node.unwrap().attr("class").unwrap(), "symlink"); } if no_symlinks { continue; } let node = node.unwrap(); assert_eq!(node.attr("href").unwrap().strip_prefix("/").unwrap(), entry); reqwest::blocking::get(server.url().join(&entry)?)?.error_for_status()?; if entry.ends_with("/") { let node = parsed .find(|x: &Node| x.name().unwrap_or_default() == "a" && x.text() == DIRECTORIES[0]) .next(); assert_eq!(node.unwrap().attr("class").unwrap(), "directory"); } else { let node = parsed .find(|x: &Node| x.name().unwrap_or_default() == "a" && x.text() == FILES[0]) .next(); assert_eq!(node.unwrap().attr("class").unwrap(), "file"); } } for &entry in broken { assert!(parsed.find(|x: &Node| x.text() == entry).next().is_none()); } Ok(()) } #[rstest] fn serves_requests_with_randomly_assigned_port(tmpdir: TempDir) -> Result<(), Error> { let mut child = Command::cargo_bin("miniserve")? .arg(tmpdir.path()) .arg("-p") .arg("0".to_string()) .stdout(Stdio::piped()) .spawn()?; sleep(Duration::from_secs(1)); child.kill()?; let output = child.wait_with_output().expect("Failed to read stdout"); let all_text = String::from_utf8(output.stdout)?; let re = Regex::new(r"http://127.0.0.1:(\d+)").unwrap(); let caps = re.captures(all_text.as_str()).unwrap(); let port_num = caps.get(1).unwrap().as_str().parse::().unwrap(); assert!(port_num > 0); Ok(()) } #[rstest] fn serves_requests_custom_index_notice(tmpdir: TempDir, port: u16) -> Result<(), Error> { let mut child = Command::cargo_bin("miniserve")? .arg("--index=not.html") .arg("-p") .arg(port.to_string()) .arg(tmpdir.path()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; sleep(Duration::from_secs(1)); child.kill()?; let output = child.wait_with_output().expect("Failed to read stdout"); let all_text = String::from_utf8(output.stderr); assert!(all_text .unwrap() .contains("The file 'not.html' provided for option --index could not be found.")); Ok(()) } #[rstest] #[case(server_no_stderr(&["--index", FILES[0]]))] #[case(server_no_stderr(&["--index", "does-not-exist.html"]))] fn index_fallback_to_listing(#[case] server: TestServer) -> Result<(), Error> { // If index file is not found, show directory listing instead. // both cases should return `Ok` reqwest::blocking::get(server.url())?.error_for_status()?; Ok(()) }