diff options
Diffstat (limited to 'testing/geckodriver/src/capabilities.rs')
-rw-r--r-- | testing/geckodriver/src/capabilities.rs | 1088 |
1 files changed, 1088 insertions, 0 deletions
diff --git a/testing/geckodriver/src/capabilities.rs b/testing/geckodriver/src/capabilities.rs new file mode 100644 index 0000000000..e21651ea61 --- /dev/null +++ b/testing/geckodriver/src/capabilities.rs @@ -0,0 +1,1088 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::command::LogOptions; +use crate::logging::Level; +use base64; +use mozdevice::AndroidStorageInput; +use mozprofile::preferences::Pref; +use mozprofile::profile::Profile; +use mozrunner::runner::platform::firefox_default_path; +use mozversion::{self, firefox_binary_version, firefox_version, Version}; +use regex::bytes::Regex; +use serde_json::{Map, Value}; +use std::collections::BTreeMap; +use std::default::Default; +use std::fmt::{self, Display}; +use std::fs; +use std::io; +use std::io::BufWriter; +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::str::{self, FromStr}; +use webdriver::capabilities::{BrowserCapabilities, Capabilities}; +use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; +use zip; + +#[derive(Clone, Debug)] +enum VersionError { + VersionError(mozversion::Error), + MissingBinary, +} + +impl From<mozversion::Error> for VersionError { + fn from(err: mozversion::Error) -> VersionError { + VersionError::VersionError(err) + } +} + +impl Display for VersionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + VersionError::VersionError(ref x) => x.fmt(f), + VersionError::MissingBinary => "No binary provided".fmt(f), + } + } +} + +impl From<VersionError> for WebDriverError { + fn from(err: VersionError) -> WebDriverError { + WebDriverError::new(ErrorStatus::SessionNotCreated, err.to_string()) + } +} + +/// Provides matching of `moz:firefoxOptions` and resolutionnized of which Firefox +/// binary to use. +/// +/// `FirefoxCapabilities` is constructed with the fallback binary, should +/// `moz:firefoxOptions` not contain a binary entry. This may either be the +/// system Firefox installation or an override, for example given to the +/// `--binary` flag of geckodriver. +pub struct FirefoxCapabilities<'a> { + pub chosen_binary: Option<PathBuf>, + fallback_binary: Option<&'a PathBuf>, + version_cache: BTreeMap<PathBuf, Result<Version, VersionError>>, +} + +impl<'a> FirefoxCapabilities<'a> { + pub fn new(fallback_binary: Option<&'a PathBuf>) -> FirefoxCapabilities<'a> { + FirefoxCapabilities { + chosen_binary: None, + fallback_binary, + version_cache: BTreeMap::new(), + } + } + + fn set_binary(&mut self, capabilities: &Map<String, Value>) { + self.chosen_binary = capabilities + .get("moz:firefoxOptions") + .and_then(|x| x.get("binary")) + .and_then(|x| x.as_str()) + .map(PathBuf::from) + .or_else(|| self.fallback_binary.cloned()) + .or_else(firefox_default_path); + } + + fn version(&mut self, binary: Option<&Path>) -> Result<Version, VersionError> { + if let Some(binary) = binary { + if let Some(cache_value) = self.version_cache.get(binary) { + return cache_value.clone(); + } + let rv = self + .version_from_ini(binary) + .or_else(|_| self.version_from_binary(binary)); + if let Ok(ref version) = rv { + debug!("Found version {}", version); + } else { + debug!("Failed to get binary version"); + } + self.version_cache.insert(binary.to_path_buf(), rv.clone()); + rv + } else { + Err(VersionError::MissingBinary) + } + } + + fn version_from_ini(&self, binary: &Path) -> Result<Version, VersionError> { + debug!("Trying to read firefox version from ini files"); + let version = firefox_version(binary)?; + if let Some(version_string) = version.version_string { + Version::from_str(&version_string).map_err(|err| err.into()) + } else { + Err(VersionError::VersionError( + mozversion::Error::MetadataError("Missing version string".into()), + )) + } + } + + fn version_from_binary(&self, binary: &Path) -> Result<Version, VersionError> { + debug!("Trying to read firefox version from binary"); + Ok(firefox_binary_version(binary)?) + } +} + +impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> { + fn init(&mut self, capabilities: &Capabilities) { + self.set_binary(capabilities); + } + + fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> { + Ok(Some("firefox".into())) + } + + fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> { + let binary = self.chosen_binary.clone(); + self.version(binary.as_ref().map(|x| x.as_ref())) + .map_err(|err| err.into()) + .map(|x| Some(x.to_string())) + } + + fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> { + Ok(if cfg!(target_os = "windows") { + Some("windows".into()) + } else if cfg!(target_os = "macos") { + Some("mac".into()) + } else if cfg!(target_os = "linux") { + Some("linux".into()) + } else { + None + }) + } + + fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } + + fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } + + fn compare_browser_version( + &mut self, + version: &str, + comparison: &str, + ) -> WebDriverResult<bool> { + Version::from_str(version) + .map_err(|err| VersionError::from(err))? + .matches(comparison) + .map_err(|err| VersionError::from(err).into()) + } + + fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } + + fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } + + fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> { + if !name.starts_with("moz:") { + return Ok(()); + } + match name { + "moz:firefoxOptions" => { + let data = try_opt!( + value.as_object(), + ErrorStatus::InvalidArgument, + "moz:firefoxOptions is not an object" + ); + for (key, value) in data.iter() { + match &**key { + "androidActivity" + | "androidDeviceSerial" + | "androidPackage" + | "profile" => { + if !value.is_string() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("{} is not a string", &**key), + )); + } + } + "androidIntentArguments" | "args" => { + if !try_opt!( + value.as_array(), + ErrorStatus::InvalidArgument, + format!("{} is not an array", &**key) + ) + .iter() + .all(|value| value.is_string()) + { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("{} entry is not a string", &**key), + )); + } + } + "binary" => { + if let Some(binary) = value.as_str() { + if !data.contains_key("androidPackage") + && self.version(Some(Path::new(binary))).is_err() + { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("{} is not a Firefox executable", &**key), + )); + } + } else { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("{} is not a string", &**key), + )); + } + } + "env" => { + let env_data = try_opt!( + value.as_object(), + ErrorStatus::InvalidArgument, + "env value is not an object" + ); + if !env_data.values().all(Value::is_string) { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Environment values were not all strings", + )); + } + } + "log" => { + let log_data = try_opt!( + value.as_object(), + ErrorStatus::InvalidArgument, + "log value is not an object" + ); + for (log_key, log_value) in log_data.iter() { + match &**log_key { + "level" => { + let level = try_opt!( + log_value.as_str(), + ErrorStatus::InvalidArgument, + "log level is not a string" + ); + if Level::from_str(level).is_err() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("Not a valid log level: {}", level), + )); + } + } + x => { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("Invalid log field {}", x), + )) + } + } + } + } + "prefs" => { + let prefs_data = try_opt!( + value.as_object(), + ErrorStatus::InvalidArgument, + "prefs value is not an object" + ); + let is_pref_value_type = |x: &Value| { + x.is_string() || x.is_i64() || x.is_u64() || x.is_boolean() + }; + if !prefs_data.values().all(is_pref_value_type) { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Preference values not all string or integer or boolean", + )); + } + } + x => { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("Invalid moz:firefoxOptions field {}", x), + )) + } + } + } + } + "moz:useNonSpecCompliantPointerOrigin" => { + if !value.is_boolean() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "moz:useNonSpecCompliantPointerOrigin is not a boolean", + )); + } + } + "moz:webdriverClick" => { + if !value.is_boolean() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "moz:webdriverClick is not a boolean", + )); + } + } + "moz:debuggerAddress" => { + if !value.is_boolean() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "moz:debuggerAddress is not a boolean", + )); + } + } + _ => { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("Unrecognised option {}", name), + )) + } + } + Ok(()) + } + + fn accept_custom(&mut self, _: &str, _: &Value, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } +} + +/// Android-specific options in the `moz:firefoxOptions` struct. +/// These map to "androidCamelCase", following [chromedriver's Android-specific +/// Capabilities](http://chromedriver.chromium.org/getting-started/getting-started---android). +#[derive(Default, Clone, Debug, PartialEq)] +pub struct AndroidOptions { + pub activity: Option<String>, + pub device_serial: Option<String>, + pub intent_arguments: Option<Vec<String>>, + pub package: String, + pub storage: AndroidStorageInput, +} + +impl AndroidOptions { + pub fn new(package: String, storage: AndroidStorageInput) -> AndroidOptions { + AndroidOptions { + package, + storage, + ..Default::default() + } + } +} + +/// Rust representation of `moz:firefoxOptions`. +/// +/// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes +/// the encoded profile, the binary arguments, log settings, and additional +/// preferences to be checked and unmarshaled from the `moz:firefoxOptions` +/// JSON Object into a Rust representation. +#[derive(Default, Debug)] +pub struct FirefoxOptions { + pub binary: Option<PathBuf>, + pub profile: Option<Profile>, + pub args: Option<Vec<String>>, + pub env: Option<Vec<(String, String)>>, + pub log: LogOptions, + pub prefs: Vec<(String, Pref)>, + pub android: Option<AndroidOptions>, +} + +impl FirefoxOptions { + pub fn new() -> FirefoxOptions { + Default::default() + } + + pub fn from_capabilities( + binary_path: Option<PathBuf>, + android_storage: AndroidStorageInput, + matched: &mut Capabilities, + ) -> WebDriverResult<FirefoxOptions> { + let mut rv = FirefoxOptions::new(); + rv.binary = binary_path; + + if let Some(json) = matched.remove("moz:firefoxOptions") { + let options = json.as_object().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "'moz:firefoxOptions' \ + capability is not an object", + ) + })?; + + rv.android = FirefoxOptions::load_android(android_storage, &options)?; + rv.args = FirefoxOptions::load_args(&options)?; + rv.env = FirefoxOptions::load_env(&options)?; + rv.log = FirefoxOptions::load_log(&options)?; + rv.prefs = FirefoxOptions::load_prefs(&options)?; + rv.profile = FirefoxOptions::load_profile(&options)?; + } + + if let Some(json) = matched.remove("moz:debuggerAddress") { + let use_web_socket = json.as_bool().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "moz:debuggerAddress is not a boolean", + ) + })?; + + if use_web_socket { + let mut remote_args = Vec::new(); + remote_args.push("--remote-debugging-port".to_owned()); + remote_args.push("0".to_owned()); + + if let Some(ref mut args) = rv.args { + args.append(&mut remote_args); + } else { + rv.args = Some(remote_args); + } + + // Force Fission disabled until Remote Agent is compatible, + // and preference hasn't been already set + let has_fission_pref = rv.prefs.iter().find(|&x| x.0 == "fission.autostart"); + if has_fission_pref.is_none() { + rv.prefs + .push(("fission.autostart".to_owned(), Pref::new(false))); + } + } + } + + Ok(rv) + } + + fn load_profile(options: &Capabilities) -> WebDriverResult<Option<Profile>> { + if let Some(profile_json) = options.get("profile") { + let profile_base64 = profile_json.as_str().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Profile is not a string") + })?; + let profile_zip = &*base64::decode(profile_base64)?; + + // Create an emtpy profile directory + let profile = Profile::new()?; + unzip_buffer( + profile_zip, + profile + .temp_dir + .as_ref() + .expect("Profile doesn't have a path") + .path(), + )?; + + Ok(Some(profile)) + } else { + Ok(None) + } + } + + fn load_args(options: &Capabilities) -> WebDriverResult<Option<Vec<String>>> { + if let Some(args_json) = options.get("args") { + let args_array = args_json.as_array().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Arguments were not an \ + array", + ) + })?; + let args = args_array + .iter() + .map(|x| x.as_str().map(|x| x.to_owned())) + .collect::<Option<Vec<String>>>() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Arguments entries were not all strings", + ) + })?; + Ok(Some(args)) + } else { + Ok(None) + } + } + + pub fn load_env(options: &Capabilities) -> WebDriverResult<Option<Vec<(String, String)>>> { + if let Some(env_data) = options.get("env") { + let env = env_data.as_object().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Env was not an object") + })?; + let mut rv = Vec::with_capacity(env.len()); + for (key, value) in env.iter() { + rv.push(( + key.clone(), + value + .as_str() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Env value is not a string", + ) + })? + .to_string(), + )); + } + Ok(Some(rv)) + } else { + Ok(None) + } + } + + fn load_log(options: &Capabilities) -> WebDriverResult<LogOptions> { + if let Some(json) = options.get("log") { + let log = json.as_object().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Log section is not an object") + })?; + + let level = match log.get("level") { + Some(json) => { + let s = json.as_str().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Log level is not a string", + ) + })?; + Some(Level::from_str(s).ok().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Log level is unknown") + })?) + } + None => None, + }; + + Ok(LogOptions { level }) + } else { + Ok(Default::default()) + } + } + + pub fn load_prefs(options: &Capabilities) -> WebDriverResult<Vec<(String, Pref)>> { + if let Some(prefs_data) = options.get("prefs") { + let prefs = prefs_data.as_object().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Prefs were not an object") + })?; + let mut rv = Vec::with_capacity(prefs.len()); + for (key, value) in prefs.iter() { + rv.push((key.clone(), pref_from_json(value)?)); + } + Ok(rv) + } else { + Ok(vec![]) + } + } + + pub fn load_android( + storage: AndroidStorageInput, + options: &Capabilities, + ) -> WebDriverResult<Option<AndroidOptions>> { + if let Some(package_json) = options.get("androidPackage") { + let package = package_json + .as_str() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidPackage is not a string", + ) + })? + .to_owned(); + + // https://developer.android.com/studio/build/application-id + let package_regexp = + Regex::new(r#"^([a-zA-Z][a-zA-Z0-9_]*\.){1,}([a-zA-Z][a-zA-Z0-9_]*)$"#).unwrap(); + if !package_regexp.is_match(package.as_bytes()) { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Not a valid androidPackage name", + )); + } + + let mut android = AndroidOptions::new(package, storage); + + android.activity = match options.get("androidActivity") { + Some(json) => { + let activity = json + .as_str() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidActivity is not a string", + ) + })? + .to_owned(); + + if activity.contains("/") { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidActivity should not contain '/", + )); + } + + Some(activity) + } + None => None, + }; + + android.device_serial = match options.get("androidDeviceSerial") { + Some(json) => Some( + json.as_str() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidDeviceSerial is not a string", + ) + })? + .to_owned(), + ), + None => None, + }; + + android.intent_arguments = match options.get("androidIntentArguments") { + Some(json) => { + let args_array = json.as_array().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidIntentArguments is not an array", + ) + })?; + let args = args_array + .iter() + .map(|x| x.as_str().map(|x| x.to_owned())) + .collect::<Option<Vec<String>>>() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidIntentArguments entries are not all strings", + ) + })?; + + Some(args) + } + None => None, + }; + + Ok(Some(android)) + } else { + Ok(None) + } + } +} + +fn pref_from_json(value: &Value) -> WebDriverResult<Pref> { + match *value { + Value::String(ref x) => Ok(Pref::new(x.clone())), + Value::Number(ref x) => Ok(Pref::new(x.as_i64().unwrap())), + Value::Bool(x) => Ok(Pref::new(x)), + _ => Err(WebDriverError::new( + ErrorStatus::UnknownError, + "Could not convert pref value to string, boolean, or integer", + )), + } +} + +fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> { + let reader = Cursor::new(buf); + let mut zip = zip::ZipArchive::new(reader) + .map_err(|_| WebDriverError::new(ErrorStatus::UnknownError, "Failed to unzip profile"))?; + + for i in 0..zip.len() { + let mut file = zip.by_index(i).map_err(|_| { + WebDriverError::new( + ErrorStatus::UnknownError, + "Processing profile zip file failed", + ) + })?; + let unzip_path = { + let name = file.name(); + let is_dir = name.ends_with('/'); + let rel_path = Path::new(name); + let dest_path = dest_dir.join(rel_path); + + { + let create_dir = if is_dir { + Some(dest_path.as_path()) + } else { + dest_path.parent() + }; + if let Some(dir) = create_dir { + if !dir.exists() { + debug!("Creating profile directory tree {}", dir.to_string_lossy()); + fs::create_dir_all(dir)?; + } + } + } + + if is_dir { + None + } else { + Some(dest_path) + } + }; + + if let Some(unzip_path) = unzip_path { + debug!("Extracting profile to {}", unzip_path.to_string_lossy()); + let dest = fs::File::create(unzip_path)?; + if file.size() > 0 { + let mut writer = BufWriter::new(dest); + io::copy(&mut file, &mut writer)?; + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + extern crate mozprofile; + + use self::mozprofile::preferences::Pref; + use super::*; + use crate::marionette::MarionetteHandler; + use mozdevice::AndroidStorageInput; + use serde_json::json; + use std::default::Default; + use std::fs::File; + use std::io::Read; + + use webdriver::capabilities::Capabilities; + + fn example_profile() -> Value { + let mut profile_data = Vec::with_capacity(1024); + let mut profile = File::open("src/tests/profile.zip").unwrap(); + profile.read_to_end(&mut profile_data).unwrap(); + Value::String(base64::encode(&profile_data)) + } + + fn make_options(firefox_opts: Capabilities) -> WebDriverResult<FirefoxOptions> { + let mut caps = Capabilities::new(); + caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts)); + + FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + } + + #[test] + fn fx_options_default() { + let opts = FirefoxOptions::new(); + assert_eq!(opts.android, None); + assert_eq!(opts.args, None); + assert_eq!(opts.binary, None); + assert_eq!(opts.log, LogOptions { level: None }); + assert_eq!(opts.prefs, vec![]); + // Profile doesn't support PartialEq + // assert_eq!(opts.profile, None); + } + + #[test] + fn fx_options_from_capabilities_no_binary_and_caps() { + let mut caps = Capabilities::new(); + + let opts = + FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps).unwrap(); + assert_eq!(opts.android, None); + assert_eq!(opts.args, None); + assert_eq!(opts.binary, None); + assert_eq!(opts.log, LogOptions { level: None }); + assert_eq!(opts.prefs, vec![]); + } + + #[test] + fn fx_options_from_capabilities_with_binary_and_caps() { + let mut caps = Capabilities::new(); + caps.insert( + "moz:firefoxOptions".into(), + Value::Object(Capabilities::new()), + ); + + let binary = PathBuf::from("foo"); + + let opts = FirefoxOptions::from_capabilities( + Some(binary.clone()), + AndroidStorageInput::Auto, + &mut caps, + ) + .unwrap(); + assert_eq!(opts.android, None); + assert_eq!(opts.args, None); + assert_eq!(opts.binary, Some(binary)); + assert_eq!(opts.log, LogOptions { level: None }); + assert_eq!(opts.prefs, vec![]); + } + + #[test] + fn fx_options_from_capabilities_with_debugger_address_not_set() { + let mut caps = Capabilities::new(); + + let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + .expect("Valid Firefox options"); + + assert!( + opts.args.is_none(), + "CLI arguments for Firefox unexpectedly found" + ); + } + + #[test] + fn fx_options_from_capabilities_with_debugger_address_false() { + let mut caps = Capabilities::new(); + caps.insert("moz:debuggerAddress".into(), json!(false)); + + let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + .expect("Valid Firefox options"); + + assert!( + opts.args.is_none(), + "CLI arguments for remote protocol unexpectedly found" + ); + } + + #[test] + fn fx_options_from_capabilities_with_debugger_address_true() { + let mut caps = Capabilities::new(); + caps.insert("moz:debuggerAddress".into(), json!(true)); + + let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + .expect("Valid Firefox options"); + + if let Some(args) = opts.args { + let mut iter = args.iter(); + assert!(iter + .find(|&arg| arg == &"--remote-debugging-port".to_owned()) + .is_some()); + assert_eq!(iter.next(), Some(&"0".to_owned())); + } else { + assert!(false, "CLI arguments for remote protocol not found"); + } + + assert!(opts + .prefs + .iter() + .any(|pref| pref == &("fission.autostart".to_owned(), Pref::new(false)))); + } + + #[test] + fn fx_options_from_capabilities_with_invalid_caps() { + let mut caps = Capabilities::new(); + caps.insert("moz:firefoxOptions".into(), json!(42)); + + FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + .expect_err("Firefox options need to be of type object"); + } + + #[test] + fn fx_options_android_no_package() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidAvtivity".into(), json!("foo")); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + assert_eq!(opts.android, None); + } + + #[test] + fn fx_options_android_package_valid_value() { + for value in ["foo.bar", "foo.bar.cheese.is.good", "Foo.Bar_9"].iter() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!(value)); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + assert_eq!( + opts.android, + Some(AndroidOptions::new( + value.to_string(), + AndroidStorageInput::Auto + )) + ); + } + } + + #[test] + fn fx_options_android_package_invalid_type() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!(42)); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_package_invalid_value() { + for value in ["../foo", "\\foo\n", "foo", "_foo", "0foo"].iter() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!(value)); + make_options(firefox_opts).expect_err("invalid firefox options"); + } + } + + #[test] + fn fx_options_android_activity_valid_value() { + for value in ["cheese", "Cheese_9"].iter() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidActivity".into(), json!(value)); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + let android_opts = AndroidOptions { + package: "foo.bar".to_owned(), + activity: Some(value.to_string()), + ..Default::default() + }; + assert_eq!(opts.android, Some(android_opts)); + } + } + + #[test] + fn fx_options_android_activity_invalid_type() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidActivity".into(), json!(42)); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_activity_invalid_value() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidActivity".into(), json!("foo.bar/cheese")); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_device_serial() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidDeviceSerial".into(), json!("cheese")); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + let android_opts = AndroidOptions { + package: "foo.bar".to_owned(), + device_serial: Some("cheese".to_owned()), + ..Default::default() + }; + assert_eq!(opts.android, Some(android_opts)); + } + + #[test] + fn fx_options_android_serial_invalid() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidDeviceSerial".into(), json!(42)); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_intent_arguments() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", "ipsum"])); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + let android_opts = AndroidOptions { + package: "foo.bar".to_owned(), + intent_arguments: Some(vec!["lorem".to_owned(), "ipsum".to_owned()]), + ..Default::default() + }; + assert_eq!(opts.android, Some(android_opts)); + } + + #[test] + fn fx_options_android_intent_arguments_no_array() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidIntentArguments".into(), json!(42)); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_intent_arguments_invalid_value() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", 42])); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_env() { + let mut env: Map<String, Value> = Map::new(); + env.insert("TEST_KEY_A".into(), Value::String("test_value_a".into())); + env.insert("TEST_KEY_B".into(), Value::String("test_value_b".into())); + + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("env".into(), env.into()); + + let mut opts = make_options(firefox_opts).expect("valid firefox options"); + for sorted in opts.env.iter_mut() { + sorted.sort() + } + assert_eq!( + opts.env, + Some(vec![ + ("TEST_KEY_A".into(), "test_value_a".into()), + ("TEST_KEY_B".into(), "test_value_b".into()), + ]) + ); + } + + #[test] + fn fx_options_env_invalid_container() { + let env = Value::Number(1.into()); + + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("env".into(), env.into()); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_env_invalid_value() { + let mut env: Map<String, Value> = Map::new(); + env.insert("TEST_KEY".into(), Value::Number(1.into())); + + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("env".into(), env.into()); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn test_profile() { + let encoded_profile = example_profile(); + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("profile".into(), encoded_profile); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + let mut profile = opts.profile.expect("valid firefox profile"); + let prefs = profile.user_prefs().expect("valid preferences"); + + println!("{:#?}", prefs.prefs); + + assert_eq!( + prefs.get("startup.homepage_welcome_url"), + Some(&Pref::new("data:text/html,PASS")) + ); + } + + #[test] + fn test_prefs() { + let encoded_profile = example_profile(); + let mut prefs: Map<String, Value> = Map::new(); + prefs.insert( + "browser.display.background_color".into(), + Value::String("#00ff00".into()), + ); + + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("profile".into(), encoded_profile); + firefox_opts.insert("prefs".into(), Value::Object(prefs)); + + let opts = make_options(firefox_opts).expect("valid profile and prefs"); + let mut profile = opts.profile.expect("valid firefox profile"); + + let handler = MarionetteHandler::new(Default::default()); + handler + .set_prefs(2828, &mut profile, true, opts.prefs) + .expect("set preferences"); + + let prefs_set = profile.user_prefs().expect("valid user preferences"); + println!("{:#?}", prefs_set.prefs); + + assert_eq!( + prefs_set.get("startup.homepage_welcome_url"), + Some(&Pref::new("data:text/html,PASS")) + ); + assert_eq!( + prefs_set.get("browser.display.background_color"), + Some(&Pref::new("#00ff00")) + ); + assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828))); + } +} |