diff --git a/src/lib.rs b/src/lib.rs index 8a4e06b..d8bedba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,28 @@ struct Session { authorization: Option, } +impl Session { + fn from_config(config: &[(String, Setting)]) -> Result { + let lookup = |wanted: &str| { + config + .iter() + .find(|(key, _)| key == wanted) + .and_then(|(_, value)| text(Some(value.clone()))) + }; + let root = lookup("url").ok_or(ErrorCode::InvalidInput)?; + if !root.starts_with("https://") && !root.starts_with("http://") { + return Err(ErrorCode::InvalidInput); + } + let authorization = lookup("username").map(|username| { + dav::basic(&username, &lookup("password").unwrap_or_default()) + }); + Ok(Self { + root, + authorization, + }) + } +} + thread_local! { static SESSION: RefCell> = const { RefCell::new(None) }; } @@ -227,26 +249,7 @@ impl Guest for Webdav { config: Vec<(String, Setting)>, ) -> Result { SESSION.set(None); - let lookup = |wanted: &str| { - config - .iter() - .find(|(key, _)| key == wanted) - .and_then(|(_, value)| text(Some(value.clone()))) - }; - let root = lookup("url").ok_or(ErrorCode::InvalidInput)?; - if !root.starts_with("https://") && !root.starts_with("http://") { - return Err(ErrorCode::InvalidInput); - } - let authorization = match (lookup("username"), lookup("password")) { - (Some(username), Some(password)) => { - Some(dav::basic(&username, &password)) - } - _ => None, - }; - SESSION.set(Some(Session { - root, - authorization, - })); + SESSION.set(Some(Session::from_config(&config)?)); let probe = Self::stat(Locator::Path("/".to_owned())).and_then(|info| { if matches!(info.kind, EntryKind::Directory) { Ok(()) @@ -429,23 +432,59 @@ sdk::export!(Webdav with_types_in sdk); #[cfg(test)] mod tests { - use super::{SESSION, Session, Webdav, text}; + use super::{SESSION, Session, Webdav}; use crate::sdk::{ErrorCode, Guest, Setting}; #[tokio::test] - async fn passwords_keep_leading_and_trailing_whitespace() { + async fn passwords_keep_leading_and_trailing_whitespace() + -> Result<(), ErrorCode> { + let config = vec![ + ( + "url".to_owned(), + Setting::Text("https://dav.example/".to_owned()), + ), + ("username".to_owned(), Setting::Text("user".to_owned())), + ( + "password".to_owned(), + Setting::Secret(" pass word \t".to_owned()), + ), + ]; assert_eq!( - text(Some(Setting::Secret(" pass word \t".to_owned()))), - Some(" pass word \t".to_owned()) + Session::from_config(&config)?.authorization.as_deref(), + Some("Basic dXNlcjogcGFzcyB3b3JkIAk=") ); + Ok(()) } #[tokio::test] - async fn an_empty_password_still_supplies_basic_auth_credentials() { + async fn a_username_without_a_password_setting_sends_basic_auth() + -> Result<(), ErrorCode> { + let config = vec![ + ( + "url".to_owned(), + Setting::Text("https://dav.example/".to_owned()), + ), + ("username".to_owned(), Setting::Text("user".to_owned())), + ]; assert_eq!( - text(Some(Setting::Secret(String::new()))), - Some(String::new()) + Session::from_config(&config)?.authorization.as_deref(), + Some("Basic dXNlcjo=") ); + Ok(()) + } + + #[tokio::test] + async fn no_username_keeps_the_connection_anonymous() -> Result<(), ErrorCode> + { + let config = vec![ + ( + "url".to_owned(), + Setting::Text("https://dav.example/".to_owned()), + ), + ("password".to_owned(), Setting::Secret("unused".to_owned())), + ]; + assert!(Session::from_config(&config)?.authorization.is_none()); + Ok(()) } #[tokio::test]