tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

capabilities.rs (31969B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 use crate::common::MAX_SAFE_INTEGER;
      6 use crate::error::{ErrorStatus, WebDriverError, WebDriverResult};
      7 use serde_json::{Map, Value};
      8 use url::Url;
      9 
     10 pub type Capabilities = Map<String, Value>;
     11 
     12 /// Trait for objects that can be used to inspect browser capabilities
     13 ///
     14 /// The main methods in this trait are called with a Capabilites object
     15 /// resulting from a full set of potential capabilites for the session.  Given
     16 /// those Capabilities they return a property of the browser instance that
     17 /// would be initiated. In many cases this will be independent of the input,
     18 /// but in the case of e.g. browser version, it might depend on a path to the
     19 /// binary provided as a capability.
     20 pub trait BrowserCapabilities {
     21    /// Set up the Capabilites object
     22    ///
     23    /// Typically used to create any internal caches
     24    fn init(&mut self, _: &Capabilities);
     25 
     26    /// Name of the browser
     27    fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
     28 
     29    /// Version number of the browser
     30    fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
     31 
     32    /// Compare actual browser version to that provided in a version specifier
     33    ///
     34    /// Parameters are the actual browser version and the comparison string,
     35    /// respectively. The format of the comparison string is
     36    /// implementation-defined.
     37    fn compare_browser_version(&mut self, version: &str, comparison: &str)
     38        -> WebDriverResult<bool>;
     39 
     40    /// Name of the platform/OS
     41    fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
     42 
     43    /// Whether insecure certificates are supported
     44    fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     45 
     46    /// Indicates whether driver supports all of the window resizing and
     47    /// repositioning commands.
     48    fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     49 
     50    /// Indicates that interactability checks will be applied to `<input type=file>`.
     51    fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     52 
     53    /// Whether a WebSocket URL for the created session has to be returned
     54    fn web_socket_url(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     55 
     56    /// Indicates whether the endpoint node supports all Virtual Authenticators commands.
     57    fn webauthn_virtual_authenticators(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     58 
     59    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the User
     60    /// Verification Method extension.
     61    fn webauthn_extension_uvm(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     62 
     63    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the prf
     64    /// extension.
     65    fn webauthn_extension_prf(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     66 
     67    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the
     68    /// largeBlob extension.
     69    fn webauthn_extension_large_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     70 
     71    /// Indicates whether the endpoint node WebAuthn WebDriver implementation supports the credBlob
     72    /// extension.
     73    fn webauthn_extension_cred_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
     74 
     75    fn accept_proxy(
     76        &mut self,
     77        proxy_settings: &Map<String, Value>,
     78        _: &Capabilities,
     79    ) -> WebDriverResult<bool>;
     80 
     81    /// Type check custom properties
     82    ///
     83    /// Check that custom properties containing ":" have the correct data types.
     84    /// Properties that are unrecognised must be ignored i.e. return without
     85    /// error.
     86    fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()>;
     87 
     88    /// Check if custom properties are accepted capabilites
     89    ///
     90    /// Check that custom properties containing ":" are compatible with
     91    /// the implementation.
     92    fn accept_custom(
     93        &mut self,
     94        name: &str,
     95        value: &Value,
     96        merged: &Capabilities,
     97    ) -> WebDriverResult<bool>;
     98 }
     99 
    100 /// Trait to abstract over various version of the new session parameters
    101 ///
    102 /// This trait is expected to be implemented on objects holding the capabilities
    103 /// from a new session command.
    104 pub trait CapabilitiesMatching {
    105    /// Match the BrowserCapabilities against some candidate capabilites
    106    ///
    107    /// Takes a BrowserCapabilites object and returns a set of capabilites that
    108    /// are valid for that browser, if any, or None if there are no matching
    109    /// capabilities.
    110    fn match_browser<T: BrowserCapabilities>(
    111        &self,
    112        browser_capabilities: &mut T,
    113    ) -> WebDriverResult<Option<Capabilities>>;
    114 }
    115 
    116 #[derive(Debug, PartialEq, Serialize, Deserialize)]
    117 pub struct SpecNewSessionParameters {
    118    #[serde(default = "Capabilities::default")]
    119    pub alwaysMatch: Capabilities,
    120    #[serde(default = "firstMatch_default")]
    121    pub firstMatch: Vec<Capabilities>,
    122 }
    123 
    124 impl Default for SpecNewSessionParameters {
    125    fn default() -> Self {
    126        SpecNewSessionParameters {
    127            alwaysMatch: Capabilities::new(),
    128            firstMatch: vec![Capabilities::new()],
    129        }
    130    }
    131 }
    132 
    133 fn firstMatch_default() -> Vec<Capabilities> {
    134    vec![Capabilities::default()]
    135 }
    136 
    137 impl SpecNewSessionParameters {
    138    fn validate<T: BrowserCapabilities>(
    139        &self,
    140        mut capabilities: Capabilities,
    141        browser_capabilities: &mut T,
    142    ) -> WebDriverResult<Capabilities> {
    143        // Filter out entries with the value `null`
    144        let null_entries = capabilities
    145            .iter()
    146            .filter(|&(_, value)| *value == Value::Null)
    147            .map(|(k, _)| k.clone())
    148            .collect::<Vec<String>>();
    149        for key in null_entries {
    150            capabilities.remove(&key);
    151        }
    152 
    153        for (key, value) in &capabilities {
    154            match &**key {
    155                x @ "acceptInsecureCerts"
    156                | x @ "setWindowRect"
    157                | x @ "strictFileInteractability"
    158                | x @ "webSocketUrl"
    159                | x @ "webauthn:virtualAuthenticators"
    160                | x @ "webauthn:extension:uvm"
    161                | x @ "webauthn:extension:prf"
    162                | x @ "webauthn:extension:largeBlob"
    163                | x @ "webauthn:extension:credBlob" => {
    164                    if !value.is_boolean() {
    165                        return Err(WebDriverError::new(
    166                            ErrorStatus::InvalidArgument,
    167                            format!("{} is not boolean: {}", x, value),
    168                        ));
    169                    }
    170                }
    171                x @ "browserName" | x @ "browserVersion" | x @ "platformName" => {
    172                    if !value.is_string() {
    173                        return Err(WebDriverError::new(
    174                            ErrorStatus::InvalidArgument,
    175                            format!("{} is not a string: {}", x, value),
    176                        ));
    177                    }
    178                }
    179                "pageLoadStrategy" => SpecNewSessionParameters::validate_page_load_strategy(value)?,
    180                "proxy" => SpecNewSessionParameters::validate_proxy(value)?,
    181                "timeouts" => SpecNewSessionParameters::validate_timeouts(value)?,
    182                "unhandledPromptBehavior" => {
    183                    SpecNewSessionParameters::validate_unhandled_prompt_behavior(value)?
    184                }
    185                x => {
    186                    if !x.contains(':') {
    187                        return Err(WebDriverError::new(
    188                            ErrorStatus::InvalidArgument,
    189                            format!(
    190                                "{} is not the name of a known capability or extension capability",
    191                                x
    192                            ),
    193                        ));
    194                    } else {
    195                        browser_capabilities.validate_custom(x, value)?
    196                    }
    197                }
    198            }
    199        }
    200 
    201        // With a value of `false` the capability needs to be removed.
    202        if let Some(Value::Bool(false)) = capabilities.get(&"webSocketUrl".to_string()) {
    203            capabilities.remove(&"webSocketUrl".to_string());
    204        }
    205 
    206        Ok(capabilities)
    207    }
    208 
    209    fn validate_page_load_strategy(value: &Value) -> WebDriverResult<()> {
    210        match value {
    211            Value::String(x) => match &**x {
    212                "normal" | "eager" | "none" => {}
    213                x => {
    214                    return Err(WebDriverError::new(
    215                        ErrorStatus::InvalidArgument,
    216                        format!("Invalid page load strategy: {}", x),
    217                    ))
    218                }
    219            },
    220            _ => {
    221                return Err(WebDriverError::new(
    222                    ErrorStatus::InvalidArgument,
    223                    "pageLoadStrategy is not a string",
    224                ))
    225            }
    226        }
    227        Ok(())
    228    }
    229 
    230    fn validate_proxy(proxy_value: &Value) -> WebDriverResult<()> {
    231        let obj = try_opt!(
    232            proxy_value.as_object(),
    233            ErrorStatus::InvalidArgument,
    234            "proxy is not an object"
    235        );
    236 
    237        for (key, value) in obj {
    238            match &**key {
    239                "proxyType" => match value.as_str() {
    240                    Some("pac") | Some("direct") | Some("autodetect") | Some("system")
    241                    | Some("manual") => {}
    242                    Some(x) => {
    243                        return Err(WebDriverError::new(
    244                            ErrorStatus::InvalidArgument,
    245                            format!("Invalid proxyType value: {}", x),
    246                        ))
    247                    }
    248                    None => {
    249                        return Err(WebDriverError::new(
    250                            ErrorStatus::InvalidArgument,
    251                            format!("proxyType is not a string: {}", value),
    252                        ))
    253                    }
    254                },
    255 
    256                "proxyAutoconfigUrl" => match value.as_str() {
    257                    Some(x) => {
    258                        Url::parse(x).map_err(|_| {
    259                            WebDriverError::new(
    260                                ErrorStatus::InvalidArgument,
    261                                format!("proxyAutoconfigUrl is not a valid URL: {}", x),
    262                            )
    263                        })?;
    264                    }
    265                    None => {
    266                        return Err(WebDriverError::new(
    267                            ErrorStatus::InvalidArgument,
    268                            "proxyAutoconfigUrl is not a string",
    269                        ))
    270                    }
    271                },
    272 
    273                "httpProxy" => SpecNewSessionParameters::validate_host(value, "httpProxy")?,
    274                "noProxy" => SpecNewSessionParameters::validate_no_proxy(value)?,
    275                "sslProxy" => SpecNewSessionParameters::validate_host(value, "sslProxy")?,
    276                "socksProxy" => SpecNewSessionParameters::validate_host(value, "socksProxy")?,
    277                "socksVersion" => {
    278                    if !value.is_number() {
    279                        return Err(WebDriverError::new(
    280                            ErrorStatus::InvalidArgument,
    281                            format!("socksVersion is not a number: {}", value),
    282                        ));
    283                    }
    284                }
    285 
    286                x => {
    287                    return Err(WebDriverError::new(
    288                        ErrorStatus::InvalidArgument,
    289                        format!("Invalid proxy configuration entry: {}", x),
    290                    ))
    291                }
    292            }
    293        }
    294 
    295        Ok(())
    296    }
    297 
    298    fn validate_no_proxy(value: &Value) -> WebDriverResult<()> {
    299        match value.as_array() {
    300            Some(hosts) => {
    301                for host in hosts {
    302                    match host.as_str() {
    303                        Some(_) => {}
    304                        None => {
    305                            return Err(WebDriverError::new(
    306                                ErrorStatus::InvalidArgument,
    307                                format!("noProxy item is not a string: {}", host),
    308                            ))
    309                        }
    310                    }
    311                }
    312            }
    313            None => {
    314                return Err(WebDriverError::new(
    315                    ErrorStatus::InvalidArgument,
    316                    format!("noProxy is not an array: {}", value),
    317                ))
    318            }
    319        }
    320 
    321        Ok(())
    322    }
    323 
    324    /// Validate whether a named capability is JSON value is a string
    325    /// containing a host and possible port
    326    fn validate_host(value: &Value, entry: &str) -> WebDriverResult<()> {
    327        match value.as_str() {
    328            Some(host) => {
    329                if host.contains("://") {
    330                    return Err(WebDriverError::new(
    331                        ErrorStatus::InvalidArgument,
    332                        format!("{} must not contain a scheme: {}", entry, host),
    333                    ));
    334                }
    335 
    336                // Temporarily add a scheme so the host can be parsed as URL
    337                let url = Url::parse(&format!("http://{}", host)).map_err(|_| {
    338                    WebDriverError::new(
    339                        ErrorStatus::InvalidArgument,
    340                        format!("{} is not a valid URL: {}", entry, host),
    341                    )
    342                })?;
    343 
    344                if url.username() != ""
    345                    || url.password().is_some()
    346                    || url.path() != "/"
    347                    || url.query().is_some()
    348                    || url.fragment().is_some()
    349                {
    350                    return Err(WebDriverError::new(
    351                        ErrorStatus::InvalidArgument,
    352                        format!("{} is not of the form host[:port]: {}", entry, host),
    353                    ));
    354                }
    355            }
    356 
    357            None => {
    358                return Err(WebDriverError::new(
    359                    ErrorStatus::InvalidArgument,
    360                    format!("{} is not a string: {}", entry, value),
    361                ))
    362            }
    363        }
    364 
    365        Ok(())
    366    }
    367 
    368    fn validate_timeouts(value: &Value) -> WebDriverResult<()> {
    369        let obj = try_opt!(
    370            value.as_object(),
    371            ErrorStatus::InvalidArgument,
    372            "timeouts capability is not an object"
    373        );
    374 
    375        for (key, value) in obj {
    376            match &**key {
    377                _x @ "script" if value.is_null() => {}
    378 
    379                x @ "script" | x @ "pageLoad" | x @ "implicit" => {
    380                    let timeout = try_opt!(
    381                        value.as_f64(),
    382                        ErrorStatus::InvalidArgument,
    383                        format!("{} timeouts value is not a number: {}", x, value)
    384                    );
    385                    if timeout < 0.0 || timeout.fract() != 0.0 {
    386                        return Err(WebDriverError::new(
    387                            ErrorStatus::InvalidArgument,
    388                            format!(
    389                                "'{}' timeouts value is not a positive Integer: {}",
    390                                x, timeout
    391                            ),
    392                        ));
    393                    }
    394                    if (timeout as u64) > MAX_SAFE_INTEGER {
    395                        return Err(WebDriverError::new(
    396                            ErrorStatus::InvalidArgument,
    397                            format!(
    398                                "'{}' timeouts value is greater than maximum safe integer: {}",
    399                                x, timeout
    400                            ),
    401                        ));
    402                    }
    403                }
    404 
    405                x => {
    406                    return Err(WebDriverError::new(
    407                        ErrorStatus::InvalidArgument,
    408                        format!("Invalid timeouts capability entry: {}", x),
    409                    ))
    410                }
    411            }
    412        }
    413 
    414        Ok(())
    415    }
    416 
    417    fn validate_unhandled_prompt_behavior(value: &Value) -> WebDriverResult<()> {
    418        match value {
    419            Value::Object(obj) => {
    420                // Unhandled Prompt Behavior type as used by WebDriver BiDi
    421                for (key, value) in obj {
    422                    match &**key {
    423                        x @ "alert"
    424                        | x @ "beforeUnload"
    425                        | x @ "confirm"
    426                        | x @ "default"
    427                        | x @ "file"
    428                        | x @ "prompt" => {
    429                            let behavior = try_opt!(
    430                                value.as_str(),
    431                                ErrorStatus::InvalidArgument,
    432                                format!(
    433                                    "'{}' unhandledPromptBehavior value is not a string: {}",
    434                                    x, value
    435                                )
    436                            );
    437 
    438                            match behavior {
    439                                "accept" | "accept and notify" | "dismiss"
    440                                | "dismiss and notify" | "ignore" => {}
    441                                x => {
    442                                    return Err(WebDriverError::new(
    443                                        ErrorStatus::InvalidArgument,
    444                                        format!(
    445                                            "'{}' unhandledPromptBehavior value is invalid: {}",
    446                                            x, behavior
    447                                        ),
    448                                    ))
    449                                }
    450                            }
    451                        }
    452                        x => {
    453                            return Err(WebDriverError::new(
    454                                ErrorStatus::InvalidArgument,
    455                                format!("Invalid unhandledPromptBehavior entry: {}", x),
    456                            ))
    457                        }
    458                    }
    459                }
    460            }
    461            Value::String(behavior) => match behavior.as_str() {
    462                "accept" | "accept and notify" | "dismiss" | "dismiss and notify" | "ignore" => {}
    463                x => {
    464                    return Err(WebDriverError::new(
    465                        ErrorStatus::InvalidArgument,
    466                        format!("Invalid unhandledPromptBehavior value: {}", x),
    467                    ))
    468                }
    469            },
    470            _ => {
    471                return Err(WebDriverError::new(
    472                    ErrorStatus::InvalidArgument,
    473                    format!(
    474                        "unhandledPromptBehavior is neither an object nor a string: {}",
    475                        value
    476                    ),
    477                ))
    478            }
    479        }
    480 
    481        Ok(())
    482    }
    483 }
    484 
    485 impl CapabilitiesMatching for SpecNewSessionParameters {
    486    fn match_browser<T: BrowserCapabilities>(
    487        &self,
    488        browser_capabilities: &mut T,
    489    ) -> WebDriverResult<Option<Capabilities>> {
    490        let default = vec![Map::new()];
    491        let capabilities_list = if self.firstMatch.is_empty() {
    492            &default
    493        } else {
    494            &self.firstMatch
    495        };
    496 
    497        let merged_capabilities = capabilities_list
    498            .iter()
    499            .map(|first_match_entry| {
    500                if first_match_entry
    501                    .keys()
    502                    .any(|k| self.alwaysMatch.contains_key(k))
    503                {
    504                    return Err(WebDriverError::new(
    505                        ErrorStatus::InvalidArgument,
    506                        "firstMatch key shadowed a value in alwaysMatch",
    507                    ));
    508                }
    509                let mut merged = self.alwaysMatch.clone();
    510                for (key, value) in first_match_entry.clone() {
    511                    merged.insert(key, value);
    512                }
    513                Ok(merged)
    514            })
    515            .map(|merged| merged.and_then(|x| self.validate(x, browser_capabilities)))
    516            .collect::<WebDriverResult<Vec<Capabilities>>>()?;
    517 
    518        let selected = merged_capabilities
    519            .iter()
    520            .find(|merged| {
    521                browser_capabilities.init(merged);
    522 
    523                for (key, value) in merged.iter() {
    524                    match &**key {
    525                        "browserName" => {
    526                            let browserValue = browser_capabilities
    527                                .browser_name(merged)
    528                                .ok()
    529                                .and_then(|x| x);
    530 
    531                            if value.as_str() != browserValue.as_deref() {
    532                                return false;
    533                            }
    534                        }
    535                        "browserVersion" => {
    536                            let browserValue = browser_capabilities
    537                                .browser_version(merged)
    538                                .ok()
    539                                .and_then(|x| x);
    540                            // We already validated this was a string
    541                            let version_cond = value.as_str().unwrap_or("");
    542                            if let Some(version) = browserValue {
    543                                if !browser_capabilities
    544                                    .compare_browser_version(&version, version_cond)
    545                                    .unwrap_or(false)
    546                                {
    547                                    return false;
    548                                }
    549                            } else {
    550                                return false;
    551                            }
    552                        }
    553                        "platformName" => {
    554                            let browserValue = browser_capabilities
    555                                .platform_name(merged)
    556                                .ok()
    557                                .and_then(|x| x);
    558                            if value.as_str() != browserValue.as_deref() {
    559                                return false;
    560                            }
    561                        }
    562                        "acceptInsecureCerts" => {
    563                            if value.as_bool().unwrap_or(false)
    564                                && !browser_capabilities
    565                                    .accept_insecure_certs(merged)
    566                                    .unwrap_or(false)
    567                            {
    568                                return false;
    569                            }
    570                        }
    571                        "setWindowRect" => {
    572                            if value.as_bool().unwrap_or(false)
    573                                && !browser_capabilities
    574                                    .set_window_rect(merged)
    575                                    .unwrap_or(false)
    576                            {
    577                                return false;
    578                            }
    579                        }
    580                        "strictFileInteractability" => {
    581                            if value.as_bool().unwrap_or(false)
    582                                && !browser_capabilities
    583                                    .strict_file_interactability(merged)
    584                                    .unwrap_or(false)
    585                            {
    586                                return false;
    587                            }
    588                        }
    589                        "proxy" => {
    590                            let default = Map::new();
    591                            let proxy = value.as_object().unwrap_or(&default);
    592                            if !browser_capabilities
    593                                .accept_proxy(proxy, merged)
    594                                .unwrap_or(false)
    595                            {
    596                                return false;
    597                            }
    598                        }
    599                        "webSocketUrl" => {
    600                            if value.as_bool().unwrap_or(false)
    601                                && !browser_capabilities.web_socket_url(merged).unwrap_or(false)
    602                            {
    603                                return false;
    604                            }
    605                        }
    606                        "webauthn:virtualAuthenticators" => {
    607                            if value.as_bool().unwrap_or(false)
    608                                && !browser_capabilities
    609                                    .webauthn_virtual_authenticators(merged)
    610                                    .unwrap_or(false)
    611                            {
    612                                return false;
    613                            }
    614                        }
    615                        "webauthn:extension:uvm" => {
    616                            if value.as_bool().unwrap_or(false)
    617                                && !browser_capabilities
    618                                    .webauthn_extension_uvm(merged)
    619                                    .unwrap_or(false)
    620                            {
    621                                return false;
    622                            }
    623                        }
    624                        "webauthn:extension:prf" => {
    625                            if value.as_bool().unwrap_or(false)
    626                                && !browser_capabilities
    627                                    .webauthn_extension_prf(merged)
    628                                    .unwrap_or(false)
    629                            {
    630                                return false;
    631                            }
    632                        }
    633                        "webauthn:extension:largeBlob" => {
    634                            if value.as_bool().unwrap_or(false)
    635                                && !browser_capabilities
    636                                    .webauthn_extension_large_blob(merged)
    637                                    .unwrap_or(false)
    638                            {
    639                                return false;
    640                            }
    641                        }
    642                        "webauthn:extension:credBlob" => {
    643                            if value.as_bool().unwrap_or(false)
    644                                && !browser_capabilities
    645                                    .webauthn_extension_cred_blob(merged)
    646                                    .unwrap_or(false)
    647                            {
    648                                return false;
    649                            }
    650                        }
    651                        name => {
    652                            if name.contains(':') {
    653                                if !browser_capabilities
    654                                    .accept_custom(name, value, merged)
    655                                    .unwrap_or(false)
    656                                {
    657                                    return false;
    658                                }
    659                            } else {
    660                                // Accept the capability
    661                            }
    662                        }
    663                    }
    664                }
    665 
    666                true
    667            })
    668            .cloned();
    669        Ok(selected)
    670    }
    671 }
    672 
    673 #[cfg(test)]
    674 mod tests {
    675    use super::*;
    676    use crate::test::assert_de;
    677    use serde_json::{self, json};
    678 
    679    #[test]
    680    fn test_json_spec_new_session_parameters_alwaysMatch_only() {
    681        let caps = SpecNewSessionParameters {
    682            alwaysMatch: Capabilities::new(),
    683            firstMatch: vec![Capabilities::new()],
    684        };
    685        assert_de(&caps, json!({"alwaysMatch": {}}));
    686    }
    687 
    688    #[test]
    689    fn test_json_spec_new_session_parameters_firstMatch_only() {
    690        let caps = SpecNewSessionParameters {
    691            alwaysMatch: Capabilities::new(),
    692            firstMatch: vec![Capabilities::new()],
    693        };
    694        assert_de(&caps, json!({"firstMatch": [{}]}));
    695    }
    696 
    697    #[test]
    698    fn test_json_spec_new_session_parameters_alwaysMatch_null() {
    699        let json = json!({
    700            "alwaysMatch": null,
    701            "firstMatch": [{}],
    702        });
    703        assert!(serde_json::from_value::<SpecNewSessionParameters>(json).is_err());
    704    }
    705 
    706    #[test]
    707    fn test_json_spec_new_session_parameters_firstMatch_null() {
    708        let json = json!({
    709            "alwaysMatch": {},
    710            "firstMatch": null,
    711        });
    712        assert!(serde_json::from_value::<SpecNewSessionParameters>(json).is_err());
    713    }
    714 
    715    #[test]
    716    fn test_json_spec_new_session_parameters_both_empty() {
    717        let json = json!({
    718            "alwaysMatch": {},
    719            "firstMatch": [{}],
    720        });
    721        let caps = SpecNewSessionParameters {
    722            alwaysMatch: Capabilities::new(),
    723            firstMatch: vec![Capabilities::new()],
    724        };
    725 
    726        assert_de(&caps, json);
    727    }
    728 
    729    #[test]
    730    fn test_json_spec_new_session_parameters_both_with_capability() {
    731        let json = json!({
    732            "alwaysMatch": {"foo": "bar"},
    733            "firstMatch": [{"foo2": "bar2"}],
    734        });
    735        let mut caps = SpecNewSessionParameters {
    736            alwaysMatch: Capabilities::new(),
    737            firstMatch: vec![Capabilities::new()],
    738        };
    739        caps.alwaysMatch.insert("foo".into(), "bar".into());
    740        caps.firstMatch[0].insert("foo2".into(), "bar2".into());
    741 
    742        assert_de(&caps, json);
    743    }
    744 
    745    #[test]
    746    fn test_validate_unhandled_prompt_behavior() {
    747        fn validate_prompt_behavior(v: Value) -> WebDriverResult<()> {
    748            SpecNewSessionParameters::validate_unhandled_prompt_behavior(&v)
    749        }
    750 
    751        // capability as string
    752        validate_prompt_behavior(json!("accept")).unwrap();
    753        validate_prompt_behavior(json!("accept and notify")).unwrap();
    754        validate_prompt_behavior(json!("dismiss")).unwrap();
    755        validate_prompt_behavior(json!("dismiss and notify")).unwrap();
    756        validate_prompt_behavior(json!("ignore")).unwrap();
    757        assert!(validate_prompt_behavior(json!("foo")).is_err());
    758 
    759        // capability as object
    760        let types = ["alert", "beforeUnload", "confirm", "default", "file", "prompt"];
    761        let handlers = [
    762            "accept",
    763            "accept and notify",
    764            "dismiss",
    765            "dismiss and notify",
    766            "ignore",
    767        ];
    768        for promptType in types {
    769            assert!(validate_prompt_behavior(json!({promptType: "foo"})).is_err());
    770            for handler in handlers {
    771                validate_prompt_behavior(json!({promptType: handler})).unwrap();
    772            }
    773        }
    774 
    775        for handler in handlers {
    776            assert!(validate_prompt_behavior(json!({"foo": handler})).is_err());
    777        }
    778    }
    779 
    780    #[test]
    781    fn test_validate_proxy() {
    782        fn validate_proxy(v: Value) -> WebDriverResult<()> {
    783            SpecNewSessionParameters::validate_proxy(&v)
    784        }
    785 
    786        // proxy hosts
    787        validate_proxy(json!({"httpProxy":  "127.0.0.1"})).unwrap();
    788        validate_proxy(json!({"httpProxy": "127.0.0.1:"})).unwrap();
    789        validate_proxy(json!({"httpProxy": "127.0.0.1:3128"})).unwrap();
    790        validate_proxy(json!({"httpProxy": "localhost"})).unwrap();
    791        validate_proxy(json!({"httpProxy": "localhost:3128"})).unwrap();
    792        validate_proxy(json!({"httpProxy": "[2001:db8::1]"})).unwrap();
    793        validate_proxy(json!({"httpProxy": "[2001:db8::1]:3128"})).unwrap();
    794        validate_proxy(json!({"httpProxy": "example.org"})).unwrap();
    795        validate_proxy(json!({"httpProxy": "example.org:3128"})).unwrap();
    796 
    797        assert!(validate_proxy(json!({"httpProxy": "http://example.org"})).is_err());
    798        assert!(validate_proxy(json!({"httpProxy": "example.org:-1"})).is_err());
    799        assert!(validate_proxy(json!({"httpProxy": "2001:db8::1"})).is_err());
    800 
    801        // no proxy for manual proxy type
    802        validate_proxy(json!({"noProxy": ["foo"]})).unwrap();
    803 
    804        assert!(validate_proxy(json!({"noProxy": "foo"})).is_err());
    805        assert!(validate_proxy(json!({"noProxy": [42]})).is_err());
    806    }
    807 }