diff --git a/Cargo.lock b/Cargo.lock index ee3961694..5844b7a6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,17 +128,6 @@ dependencies = [ "zstd-safe", ] -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -544,21 +533,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1057,20 +1031,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows 0.48.0", -] - -[[package]] -name = "generator" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" -dependencies = [ - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.58.0", + "windows", ] [[package]] @@ -1231,33 +1192,6 @@ dependencies = [ "url", ] -[[package]] -name = "hickory-proto" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d844af74f7b799e41c78221be863bade11c430d46042c3b49ca8ae0c6d27287" -dependencies = [ - "async-recursion", - "async-trait", - "cfg-if", - "critical-section", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.0", - "ring", - "thiserror 2.0.12", - "tinyvec", - "tokio", - "tracing", - "url", -] - [[package]] name = "hickory-resolver" version = "0.24.4" @@ -1266,7 +1200,7 @@ checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" dependencies = [ "cfg-if", "futures-util", - "hickory-proto 0.24.4", + "hickory-proto", "ipconfig", "lru-cache", "once_cell", @@ -1279,27 +1213,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hickory-resolver" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a128410b38d6f931fcc6ca5c107a3b02cabd6c05967841269a4ad65d23c44331" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto 0.25.1", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.0", - "resolv-conf", - "smallvec", - "thiserror 2.0.12", - "tokio", - "tracing", -] - [[package]] name = "home" version = "0.5.11" @@ -1508,7 +1421,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -1883,7 +1796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" dependencies = [ "cfg-if", - "generator 0.7.5", + "generator", "scoped-tls", "serde", "serde_json", @@ -1891,19 +1804,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator 0.8.4", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru-cache" version = "0.1.2" @@ -2001,25 +1901,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "moka" -version = "0.12.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "loom 0.7.2", - "parking_lot", - "portable-atomic", - "rustc_version", - "smallvec", - "tagptr", - "thiserror 1.0.69", - "uuid", -] - [[package]] name = "multer" version = "3.1.0" @@ -2121,10 +2002,6 @@ name = "once_cell" version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" -dependencies = [ - "critical-section", - "portable-atomic", -] [[package]] name = "openssl" @@ -2330,12 +2207,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" -[[package]] -name = "portable-atomic" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" - [[package]] name = "powerfmt" version = "0.2.0" @@ -2619,7 +2490,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.7", - "hickory-resolver 0.24.4", + "hickory-resolver", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -3283,7 +3154,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" dependencies = [ - "loom 0.5.6", + "loom", ] [[package]] @@ -3400,12 +3271,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "target-triple" version = "0.1.3" @@ -4303,16 +4168,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.52.0" @@ -4322,41 +4177,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.1.1" @@ -4369,20 +4189,11 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.3.2", - "windows-strings 0.3.1", + "windows-result", + "windows-strings", "windows-targets 0.53.0", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.2" @@ -4392,16 +4203,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.3.1" @@ -4660,7 +4461,7 @@ dependencies = [ "chrono", "futures", "h2 0.4.7", - "hickory-resolver 0.25.1", + "hickory-resolver", "http 1.3.1", "hyper 1.6.0", "hyper-util", diff --git a/Cargo.toml b/Cargo.toml index dc6cb6ca1..c81dd9342 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ fluent-langneg = "0.13" fluent-syntax = "0.11" fluent-templates = "0.13" h2 = "0.4" -hickory-resolver = "0.25" +hickory-resolver = "0.24" # 0.25 breaks the ability to detect DNS Errors, so we shouldn't update until that's fixed http = "1.3" hyper = "1.1" # Use the same version as `reqwest`: https://github.com/seanmonstar/reqwest/blob/v0.12.15/Cargo.toml#L121 hyper-util = "0.1.10" diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 6c0479b51..57233d375 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -1,7 +1,7 @@ pub use api_client::{WpApiClient, WpApiRequestBuilder}; pub use api_error::{ - MediaUploadRequestExecutionError, ParsedRequestError, RequestExecutionError, - RequestExecutionErrorReason, WpApiError, WpError, WpErrorCode, + InvalidSslErrorReason, MediaUploadRequestExecutionError, ParsedRequestError, + RequestExecutionError, RequestExecutionErrorReason, WpApiError, WpError, WpErrorCode, }; pub use parsed_url::{ParseUrlError, ParsedUrl}; use plugins::*; diff --git a/wp_api/src/login.rs b/wp_api/src/login.rs index 29cf5703f..f4bcf7bea 100644 --- a/wp_api/src/login.rs +++ b/wp_api/src/login.rs @@ -56,7 +56,7 @@ pub fn extract_login_details_from_parsed_url( }) } -#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Object)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, uniffi::Object)] pub struct WpApiDetails { pub name: String, pub description: String, @@ -137,7 +137,7 @@ impl WpApiDetails { } } -#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, uniffi::Record)] pub struct KnownApplicationPasswordBlockingPlugin { /// The name of the plugin. pub name: String, @@ -172,12 +172,12 @@ impl KnownApplicationPasswordBlockingPlugin { } } -#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, uniffi::Record)] pub struct WpRestApiAuthenticationScheme { pub endpoints: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, uniffi::Record)] pub struct WpRestApiAuthenticationEndpoint { pub authorization: String, } @@ -216,7 +216,7 @@ impl WpSupportsLocalization for OAuthResponseUrlError { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, PartialEq)] pub struct WpApiDetailsAuthenticationMap(HashMap); // If the response is `[]`, default to an empty `HashMap` diff --git a/wp_api/src/login/url_discovery.rs b/wp_api/src/login/url_discovery.rs index 9ea000299..7d70a7f56 100644 --- a/wp_api/src/login/url_discovery.rs +++ b/wp_api/src/login/url_discovery.rs @@ -362,7 +362,7 @@ pub struct AutoDiscoveryAttemptSuccess { pub api_details: Arc, } -#[derive(Debug, Clone, thiserror::Error, uniffi::Error, WpDeriveLocalizable)] +#[derive(Debug, Clone, PartialEq, thiserror::Error, uniffi::Error, WpDeriveLocalizable)] pub enum AutoDiscoveryAttemptFailure { ParseSiteUrl { error: ParseUrlError, @@ -427,7 +427,7 @@ impl WpSupportsLocalization for AutoDiscoveryAttemptFailure { } } -#[derive(Debug, Clone, uniffi::Enum)] +#[derive(Debug, Clone, PartialEq, uniffi::Enum)] pub enum FindApiRootFailure { FetchHomepage { error: RequestExecutionError }, // if no WP mentions @@ -461,7 +461,7 @@ impl FindApiRootFailure { } } -#[derive(Debug, Clone, uniffi::Enum)] +#[derive(Debug, Clone, PartialEq, uniffi::Enum)] pub enum FetchAndParseApiRootFailure { FetchApiRoot { error: RequestExecutionError, @@ -510,7 +510,7 @@ impl FetchAndParseApiRootFailure { } } -#[derive(Debug, Clone, uniffi::Enum)] +#[derive(Debug, Clone, PartialEq, uniffi::Enum)] pub enum ApplicationPasswordsNotSupportedReason { ApplicationPasswordBlockedByPlugin { plugin: KnownApplicationPasswordBlockingPlugin, diff --git a/wp_api/src/middleware.rs b/wp_api/src/middleware.rs index e67772c9a..1b03ab749 100644 --- a/wp_api/src/middleware.rs +++ b/wp_api/src/middleware.rs @@ -114,7 +114,7 @@ pub trait PerformsRequests { // MARK: - RetryAfterMiddleware #[derive(Debug, uniffi::Object)] -struct RetryAfterMiddleware { +pub struct RetryAfterMiddleware { max_retries: u8, max_retry_wait_seconds: u64, } @@ -122,7 +122,7 @@ struct RetryAfterMiddleware { #[uniffi::export] impl RetryAfterMiddleware { #[uniffi::constructor] - fn new(max_retries: u8, max_retry_wait_seconds: u64) -> Self { + pub fn new(max_retries: u8, max_retry_wait_seconds: u64) -> Self { println!("Creating retry middleware"); Self { max_retries, diff --git a/wp_api/src/reqwest_request_executor.rs b/wp_api/src/reqwest_request_executor.rs index 45b1ec0b9..eb789cb9a 100644 --- a/wp_api/src/reqwest_request_executor.rs +++ b/wp_api/src/reqwest_request_executor.rs @@ -8,7 +8,7 @@ use crate::{ }; use async_trait::async_trait; use h2::Error as Http2Error; -use hickory_resolver::ResolveError; +use hickory_resolver::error::ResolveError; use http::{HeaderMap, HeaderValue}; use hyper::Error as HyperError; use reqwest::multipart::Part; diff --git a/wp_api_integration_tests/src/lib.rs b/wp_api_integration_tests/src/lib.rs index 08bf919dc..838d50691 100644 --- a/wp_api_integration_tests/src/lib.rs +++ b/wp_api_integration_tests/src/lib.rs @@ -6,6 +6,8 @@ use wp_api::{ reqwest_request_executor::ReqwestRequestExecutor, tags::TagId, users::UserId, }; +pub mod mock; + // A `TestCredentials::instance()` function will be generated by this include!(concat!(env!("OUT_DIR"), "/generated_test_credentials.rs")); diff --git a/wp_api_integration_tests/src/mock.rs b/wp_api_integration_tests/src/mock.rs new file mode 100644 index 000000000..3e209745a --- /dev/null +++ b/wp_api_integration_tests/src/mock.rs @@ -0,0 +1,117 @@ +use async_trait::async_trait; +use std::sync::Arc; +use wp_api::{ + MediaUploadRequestExecutionError, RequestExecutionError, + request::{ + RequestExecutor, WpNetworkRequest, WpNetworkResponse, + endpoint::media_endpoint::MediaUploadRequest, + }, +}; + +#[derive(Debug)] +pub struct MockExecutor { + execute_fn: fn(Arc) -> Result, + upload_media_fn: + fn(Arc) -> Result, +} + +impl MockExecutor { + pub fn with_execute_fn( + execute_fn: fn(Arc) -> Result, + ) -> Self { + Self { + execute_fn, + upload_media_fn: |_: Arc| { + panic!("Upload media is not implemented for `MockExecutor`") + }, + } + } +} + +#[async_trait] +impl RequestExecutor for MockExecutor { + async fn execute( + &self, + request: Arc, + ) -> Result { + (self.execute_fn)(request) + } + + async fn upload_media( + &self, + media_upload_request: Arc, + ) -> Result { + (self.upload_media_fn)(media_upload_request) + } + + async fn sleep(&self, _: u64) {} +} + +pub mod response_helpers { + use http::{HeaderMap, header::HeaderValue}; + use std::{fs, path::Path, sync::Arc}; + use wp_api::request::{WpNetworkHeaderMap, WpNetworkResponse, endpoint::WpEndpointUrl}; + + pub fn with_api_root(url: &str) -> WpNetworkResponse { + let mut map = HeaderMap::new(); + let link_header_value = format!("<{url}>; rel=\"https://api.w.org/\""); + map.insert( + http::header::LINK, + HeaderValue::from_str(&link_header_value).expect("Failed to create Link header"), + ); + WpNetworkResponse { + body: vec![], + status_code: 200, + response_header_map: Arc::new(map.into()), + request_url: WpEndpointUrl("".to_string()), + request_header_map: WpNetworkHeaderMap::default().into(), + } + } + + pub fn json_response_from_path(json_file_path: &Path) -> WpNetworkResponse { + let json = fs::read_to_string(json_file_path).unwrap_or_else(|_| { + panic!( + "Should have been able to read the json file at: '{:#?}'", + json_file_path + ) + }); + let mut map = HeaderMap::new(); + map.insert( + http::header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ); + WpNetworkResponse { + body: json.as_bytes().to_vec(), + status_code: 200, + response_header_map: Arc::new(map.into()), + request_url: WpEndpointUrl("".to_string()), + request_header_map: WpNetworkHeaderMap::default().into(), + } + } + + pub fn retry_response(delay: usize) -> WpNetworkResponse { + let mut map = HeaderMap::new(); + map.insert( + http::header::RETRY_AFTER, + HeaderValue::from_str(format!("{delay}").as_str()) + .expect("Failed to create Retry-After header"), + ); + WpNetworkResponse { + body: vec![], + status_code: 429, + response_header_map: Arc::new(map.into()), + request_url: WpEndpointUrl("".to_string()), + request_header_map: WpNetworkHeaderMap::default().into(), + } + } + + pub fn empty_response(status_code: u16) -> WpNetworkResponse { + WpNetworkResponse { + body: vec![], + status_code, + response_header_map: WpNetworkHeaderMap::default().into(), + request_url: WpEndpointUrl("".to_string()), + request_header_map: WpNetworkHeaderMap::default().into(), + } + } +} diff --git a/wp_api_integration_tests/tests/test_login_err.rs b/wp_api_integration_tests/tests/test_login_err.rs deleted file mode 100644 index 518f30eb0..000000000 --- a/wp_api_integration_tests/tests/test_login_err.rs +++ /dev/null @@ -1,120 +0,0 @@ -use rstest::rstest; -use serial_test::parallel; -use std::sync::Arc; -use wp_api::{ - RequestExecutionError, RequestExecutionErrorReason, - login::{ - login_client::WpLoginClient, - url_discovery::{AutoDiscoveryAttemptFailure, FindApiRootFailure}, - }, - middleware::{ApiDiscoveryAuthenticationMiddleware, WpApiMiddleware, WpApiMiddlewarePipeline}, - reqwest_request_executor::ReqwestRequestExecutor, -}; - -#[rstest] -#[case("http://jalib923knblakis9ba92q3nbaslkes.nope")] -#[tokio::test] -#[parallel] -async fn test_login_flow_err_network_error(#[case] site_url: &str) { - let original_attempt_error = login_flow_err_helper(site_url, vec![]).await; - assert!( - original_attempt_error.is_network_error(), - "{:#?}", - original_attempt_error - ); -} - -#[rstest] -#[case("https://wordfence.wpmt.co")] -#[tokio::test] -#[parallel] -async fn test_login_flow_err_application_passwords_not_supported(#[case] site_url: &str) { - let original_attempt_error = login_flow_err_helper(site_url, vec![]).await; - assert_eq!( - original_attempt_error.is_application_passwords_disabled(), - Some(true), - "{:#?}", - original_attempt_error - ); -} - -#[rstest] -#[case("https://basic-auth.wpmt.co")] -#[tokio::test] -#[parallel] -async fn test_login_flow_err_http_authentication_required_error(#[case] site_url: &str) { - let original_attempt_error = login_flow_err_helper(site_url, vec![]).await; - assert!(matches!( - original_attempt_error, - AutoDiscoveryAttemptFailure::FindApiRoot { - find_api_root_failure: FindApiRootFailure::FetchHomepage { - error: RequestExecutionError::RequestExecutionFailed { - reason: RequestExecutionErrorReason::HttpAuthenticationRequiredError { .. }, - .. - }, - }, - .. - } - )); -} - -#[rstest] -#[case("https://basic-auth.wpmt.co")] -#[tokio::test] -#[parallel] -async fn test_login_flow_err_http_authentication_rejected_error(#[case] site_url: &str) { - let original_attempt_error = login_flow_err_helper( - site_url, - vec![Arc::new(ApiDiscoveryAuthenticationMiddleware::new( - "invalid".to_string(), - "invalid".to_string(), - ))], - ) - .await; - assert!(matches!( - original_attempt_error, - AutoDiscoveryAttemptFailure::FindApiRoot { - find_api_root_failure: FindApiRootFailure::FetchHomepage { - error: RequestExecutionError::RequestExecutionFailed { - reason: RequestExecutionErrorReason::HttpAuthenticationRejectedError { .. }, - .. - }, - }, - .. - } - )); -} - -#[rstest] -#[case("https://www.beeper.com/")] -#[tokio::test] -#[parallel] -async fn test_login_flow_err_not_a_wordpress_site(#[case] site_url: &str) { - let err = login_flow_err_helper(site_url, vec![]).await; - assert!( - matches!( - err, - AutoDiscoveryAttemptFailure::FindApiRoot { - find_api_root_failure: FindApiRootFailure::ProbablyNotAWordPressSite { .. }, - .. - } - ), - "{:#?}", - err - ); -} - -async fn login_flow_err_helper( - site_url: &str, - middlewares: Vec>, -) -> AutoDiscoveryAttemptFailure { - WpLoginClient::new( - Arc::new(ReqwestRequestExecutor::default()), - Arc::new(WpApiMiddlewarePipeline { middlewares }), - ) - .api_discovery(site_url.to_string()) - .await - .combined_result() - .unwrap_err() - .clone() -} diff --git a/wp_api_integration_tests/tests/test_login_immut.rs b/wp_api_integration_tests/tests/test_login_immut.rs deleted file mode 100644 index 1ea73bc38..000000000 --- a/wp_api_integration_tests/tests/test_login_immut.rs +++ /dev/null @@ -1,150 +0,0 @@ -use rstest::rstest; -use serial_test::parallel; -use std::sync::Arc; -use wp_api::{ - login::login_client::WpLoginClient, - middleware::{ApiDiscoveryAuthenticationMiddleware, WpApiMiddleware, WpApiMiddlewarePipeline}, - reqwest_request_executor::ReqwestRequestExecutor, -}; - -const LOCALHOST_AUTH_URL: &str = "http://localhost/wp-admin/authorize-application.php"; -const AUTOMATTIC_WIDGETS_AUTH_URL: &str = - "https://automatticwidgets.wpcomstaging.com/wp-admin/authorize-application.php"; -const OPTIONAL_HTTPS_AUTH_URL: &str = - "https://optional-https.wpmt.co/wp-admin/authorize-application.php"; -const VANILLA_WP_AUTH_URL: &str = "https://vanilla.wpmt.co/wp-admin/authorize-application.php"; - -#[rstest] -#[case("http://localhost", LOCALHOST_AUTH_URL)] -#[case("http://localhost/wp-json", LOCALHOST_AUTH_URL)] -#[case( - "https://automatticwidgets.wpcomstaging.com/", - AUTOMATTIC_WIDGETS_AUTH_URL -)] -#[case( - "https://automatticwidgets.wpcomstaging.com/wp-admin", - AUTOMATTIC_WIDGETS_AUTH_URL -)] -#[case( - "https://automatticwidgets.wpcomstaging.com/wp-admin.php", - AUTOMATTIC_WIDGETS_AUTH_URL -)] -#[case( - "https://automatticwidgets.wpcomstaging.com/wp-admin/", - AUTOMATTIC_WIDGETS_AUTH_URL -)] -#[case( - "https://automatticwidgets.wpcomstaging.com/wp-json", - AUTOMATTIC_WIDGETS_AUTH_URL -)] -#[case("automatticwidgets.wpcomstaging.com/ ", AUTOMATTIC_WIDGETS_AUTH_URL)] -#[case("vanilla.wpmt.co", VANILLA_WP_AUTH_URL)] -#[case("http://vanilla.wpmt.co", VANILLA_WP_AUTH_URL)] -#[case("http://optional-https.wpmt.co", OPTIONAL_HTTPS_AUTH_URL)] -#[case("https://optional-https.wpmt.co", OPTIONAL_HTTPS_AUTH_URL)] -#[case( - "https://わぷー.wpmt.co", - "https://xn--39j4bws.wpmt.co/wp-admin/authorize-application.php" -)] -#[case( - "https://jetpack.wpmt.co", - "https://jetpack.wpmt.co/wp-admin/authorize-application.php" -)] -#[case( - "https://aggressive-caching.wpmt.co", - "https://aggressive-caching.wpmt.co/wp-admin/authorize-application.php" -)] // Returns gzip responses, may not always include Link header -#[tokio::test] -#[parallel] -async fn test_login_flow(#[case] site_url: &str, #[case] expected_auth_url: &str) { - login_flow_helper(site_url, expected_auth_url, vec![]).await; -} - -#[rstest] -#[case( - "https://basic-auth.wpmt.co", - "https://basic-auth.wpmt.co/wp-admin/authorize-application.php" -)] -#[tokio::test] -#[parallel] -async fn test_login_flow_with_authentication_middleware( - #[case] site_url: &str, - #[case] expected_auth_url: &str, -) { - login_flow_helper( - site_url, - expected_auth_url, - vec![Arc::new(ApiDiscoveryAuthenticationMiddleware::new( - // These credentials are safe to check into the repo - "test@example.com".to_string(), - "str0ngp4ssw0rd!".to_string(), - ))], - ) - .await; -} - -#[rstest] -#[case( - "http://wordpress-1315525-4803651.cloudwaysapps.com", - VANILLA_WP_AUTH_URL -)] -#[tokio::test] -#[parallel] -async fn test_login_flow_accept_dangerous_certificates( - #[case] site_url: &str, - #[case] expected_auth_url: &str, -) { - login_flow_helper_with_executor( - ReqwestRequestExecutor::new_with_default_timeout(true), - site_url, - expected_auth_url, - vec![], - ) - .await; -} - -async fn login_flow_helper( - site_url: &str, - expected_auth_url: &str, - middlewares: Vec>, -) { - login_flow_helper_with_executor( - ReqwestRequestExecutor::default(), - site_url, - expected_auth_url, - middlewares, - ) - .await; -} - -async fn login_flow_helper_with_executor( - request_executor: ReqwestRequestExecutor, - site_url: &str, - expected_auth_url: &str, - middlewares: Vec>, -) { - let client = WpLoginClient::new( - Arc::new(request_executor), - Arc::new(WpApiMiddlewarePipeline { middlewares }), - ); - - let result = client.api_discovery(site_url.to_string()).await; - assert!( - result.is_successful(), - "Auto discovery failed: {:#?}", - result - ); - - let successful_attempt = result - .find_successful() - .expect("Already verified that auto discovery is successful"); - assert_eq!( - successful_attempt - .api_discovery_result - .clone() - .expect("Already verified that auto discovery is successful") - .api_details - .find_application_passwords_authentication_url(), - Some(expected_auth_url.to_string()) - ); -} diff --git a/wp_api_integration_tests/tests/test_login_remote.rs b/wp_api_integration_tests/tests/test_login_remote.rs new file mode 100644 index 000000000..581b69e8a --- /dev/null +++ b/wp_api_integration_tests/tests/test_login_remote.rs @@ -0,0 +1,451 @@ +use serial_test::parallel; +use std::{path::Path, sync::Arc}; +use wp_api::{ + InvalidSslErrorReason, RequestExecutionError, RequestExecutionErrorReason, + login::{ + login_client::WpLoginClient, + url_discovery::{ + ApplicationPasswordsNotSupportedReason, AutoDiscoveryAttemptFailure, + FetchAndParseApiRootFailure, FindApiRootFailure, + }, + }, + middleware::{ + ApiDiscoveryAuthenticationMiddleware, RetryAfterMiddleware, WpApiMiddleware, + WpApiMiddlewarePipeline, + }, + request::RequestExecutor, + reqwest_request_executor::ReqwestRequestExecutor, +}; +use wp_api_integration_tests::mock::{MockExecutor, response_helpers}; + +#[tokio::test] +#[parallel] +async fn login_spec_1_valid_site_works_correctly() { + // Spec Example 1 + assert_eq!( + login_url("https://vanilla.wpmt.co").await, + "https://vanilla.wpmt.co/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_2_local_development_environment() { + // Spec Example 2 + let executor = MockExecutor::with_execute_fn(|request| match request.url().0.as_str() { + "http://localhost/" | "https://localhost/" => { + Ok(response_helpers::with_api_root("http://localhost/wp-json")) + } + "http://localhost/wp-json" | "https://localhost/wp-json" => { + Ok(response_helpers::json_response_from_path(Path::new( + "../native/swift/Tests/wordpress-api/Resources/Responses/localhost-json-root.json", + ))) + } + _ => panic!("Unexpected request URL: {:#?}", request.url()), + }); + let error = discovery_helper(Arc::new(executor), vec![], "http://localhost/") + .await + .expect_err("Expected api discovery to fail") + .to_fetch_and_parse_api_root_failure(); + if let FetchAndParseApiRootFailure::ApplicationPasswordsNotSupported { reason, .. } = error { + assert_eq!( + reason, + Some(ApplicationPasswordsNotSupportedReason::SiteIsLocalDevelopmentEnvironment) + ); + } else { + panic!( + "Expected FetchAndParseApiRootFailure::ApplicationPasswordsNotSupported, got: {:?}", + error + ); + } +} + +#[tokio::test] +#[parallel] +async fn login_spec_3_admin_url_provided() { + // Spec Example 3 + assert_eq!( + login_url("https://vanilla.wpmt.co/wp-login.php").await, + "https://vanilla.wpmt.co/wp-admin/authorize-application.php" + ); + assert_eq!( + login_url("https://vanilla.wpmt.co/wp-admin").await, + "https://vanilla.wpmt.co/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_4_auth_https_support() { + // Spec Example 4 + assert_eq!( + login_url("http://vanilla.wpmt.co").await, + "https://vanilla.wpmt.co/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_5_http_only_site() { + // Spec Example 5 + let error = login_err("http://no-https.wpmt.co") + .await + .to_fetch_and_parse_api_root_failure(); + if let FetchAndParseApiRootFailure::ApplicationPasswordsNotSupported { reason, .. } = error { + assert_eq!( + reason, + Some(ApplicationPasswordsNotSupportedReason::ApplicationPasswordsDisabledForHttpSite) + ); + } else { + panic!( + "Expected FetchAndParseApiRootFailure::ApplicationPasswordsNotSupported, got: {:?}", + error + ); + } +} + +#[tokio::test] +#[parallel] +async fn login_spec_6_http_only_site_with_application_passwords_enabled() { + // Spec Example 6 + assert_eq!( + login_url("http://no-https-with-application-passwords.wpmt.co").await, + "http://no-https-with-application-passwords.wpmt.co/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_7_aggressively_cached_site_with_no_link_header() { + // Spec Example 7 + assert_eq!( + login_url("https://aggressive-caching.wpmt.co").await, + "https://aggressive-caching.wpmt.co/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_8_site_with_application_passwords_disabled_by_wordfence() { + // Spec Example 8 + let error = login_err("https://wordfence.wpmt.co") + .await + .to_fetch_and_parse_api_root_failure(); + if let FetchAndParseApiRootFailure::ApplicationPasswordsNotSupported { + reason: + Some(ApplicationPasswordsNotSupportedReason::ApplicationPasswordBlockedByPlugin { + ref plugin, + }), + .. + } = error + { + assert_eq!(plugin.name, "Wordfence"); + } else { + panic!( + "Expected ApplicationPasswordsNotSupportedReason::ApplicationPasswordBlockedByPlugin, got: {:?}", + error + ); + } +} + +#[tokio::test] +#[parallel] +async fn login_spec_9_not_a_wordpress_site() { + // Spec Example 9 + assert_eq!( + login_err("google.com").await.to_find_api_root_failure(), + FindApiRootFailure::ProbablyNotAWordPressSite + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_10_wordpress_subdirectory_with_link_header() { + // Spec Example 10 + assert_eq!( + login_url("https://subdirectory.wpmt.co/index.php?link_header=true").await, + "https://subdirectory.wpmt.co/wordpress/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_11_wordpress_subdirectory_with_link_tag() { + // Spec Example 11 + assert_eq!( + login_url("https://subdirectory.wpmt.co/index.php?link_tag=true").await, + "https://subdirectory.wpmt.co/wordpress/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_12_wordpress_subdirectory_with_redirect() { + // Spec Example 12 + assert_eq!( + login_url("https://subdirectory.wpmt.co/index.php?redirect=true").await, + "https://subdirectory.wpmt.co/wordpress/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_13_wordpress_http_basic_with_missing_credentials() { + // Spec Example 13 (with missing credentials) + let expected_hostname = "https://basic-auth.wpmt.co/"; + let reason = login_err(expected_hostname) + .await + .to_fetch_home_page_reason(); + if let RequestExecutionErrorReason::HttpAuthenticationRequiredError { hostname, .. } = reason { + assert_eq!(hostname, expected_hostname); + } else { + panic!( + "Expected RequestExecutionErrorReason::HttpAuthenticationRequiredError, got: {:?}", + reason + ); + } +} + +#[tokio::test] +#[parallel] +async fn login_spec_13_wordpress_http_basic_with_invalid_credentials() { + // Spec Example 13 (with invalid credentials) + let expected_hostname = "https://basic-auth.wpmt.co/"; + let reason = discovery_helper( + Arc::new(ReqwestRequestExecutor::default()), + vec![Arc::new(ApiDiscoveryAuthenticationMiddleware::new( + "invalid".to_string(), + "invalid".to_string(), + ))], + expected_hostname, + ) + .await + .expect_err("Expected api discovery to fail") + .to_fetch_home_page_reason(); + if let RequestExecutionErrorReason::HttpAuthenticationRejectedError { hostname, .. } = reason { + assert_eq!(hostname, expected_hostname); + } else { + panic!( + "Expected RequestExecutionErrorReason::HttpAuthenticationRejectedError, got: {:?}", + reason + ); + } +} + +#[tokio::test] +#[parallel] +async fn login_spec_13_wordpress_http_basic_with_valid_credentials() { + // Spec Example 13 (with valid credentials) + let login_url = discovery_helper( + Arc::new(ReqwestRequestExecutor::default()), + vec![Arc::new(ApiDiscoveryAuthenticationMiddleware::new( + "test@example.com".to_string(), + "str0ngp4ssw0rd!".to_string(), + ))], + "https://basic-auth.wpmt.co/", + ) + .await + .expect("Expected api discovery to fail"); + assert_eq!( + login_url, + "https://basic-auth.wpmt.co/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_14_wordpress_custom_rest_api_prefix() { + // Spec Example 14 + assert_eq!( + login_url("https://custom-rest-prefix.wpmt.co").await, + "https://custom-rest-prefix.wpmt.co/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_15_wordpress_heavy_rate_limiting() { + // Spec Example 15 + assert_eq!( + login_url("https://aggressive-rate-limiting.wpmt.co").await, + "https://aggressive-rate-limiting.wpmt.co/wp-admin/authorize-application.php" + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_15_wordpress_heavy_rate_limiting_that_never_succeeds() { + // Spec Example 15 + let executor = MockExecutor::with_execute_fn(|request| match request.url().0.as_str() { + "https://aggressive-rate-limiting.wpmt.co/" => Ok(response_helpers::retry_response(1)), + "https://aggressive-rate-limiting.wpmt.co/wp-json" => { + Ok(response_helpers::empty_response(200)) + } + _ => panic!("Unexpected request URL: {:#?}", request.url()), + }); + let retry_middleware = RetryAfterMiddleware::new(3, 1); + let request_execution_error_reason = discovery_helper( + Arc::new(executor), + vec![Arc::new(retry_middleware)], + "https://aggressive-rate-limiting.wpmt.co", + ) + .await + .expect_err("Expected api discovery to fail") + .to_fetch_home_page_reason(); + assert_eq!( + request_execution_error_reason, + RequestExecutionErrorReason::MisconfiguredRateLimitError, + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_16_invalid_url() { + // Spec Example 16 + + let request_execution_error_reason = + login_err("https://valid-looking-url-but-not-actually.foo") + .await + .to_fetch_home_page_reason(); + + assert!( + matches!( + request_execution_error_reason, + RequestExecutionErrorReason::NonExistentSiteError { .. } + ), + "Expected RequestExecutionErrorReason::NonExistentSiteError, got: {:?}", + request_execution_error_reason + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_17_invalid_https_fails() { + // Spec Example 17 + let request_execution_error_reason = + login_err("https://wordpress-1315525-4803651.cloudwaysapps.com") + .await + .to_fetch_home_page_reason(); + assert!( + matches!( + request_execution_error_reason, + RequestExecutionErrorReason::InvalidSslError { + reason: InvalidSslErrorReason::CertificateNotValidForName { .. } + } + ), + "Expected RequestExecutionErrorReason::InvalidSslError, got: {:?}", + request_execution_error_reason + ); +} + +#[tokio::test] +#[parallel] +async fn login_spec_17_invalid_https_with_exception_works() { + // Spec Example 17 (with exception) + assert_eq!( + login_url_with_executor( + Arc::new(ReqwestRequestExecutor::new_with_default_timeout(true)), + "https://wordpress-1315525-4803651.cloudwaysapps.com" + ) + .await, + "https://vanilla.wpmt.co/wp-admin/authorize-application.php" + ); +} + +async fn login_url(site_url: &str) -> String { + login_url_with_executor(Arc::new(ReqwestRequestExecutor::default()), site_url).await +} + +async fn login_url_with_executor( + request_executor: Arc, + site_url: &str, +) -> String { + discovery_helper(request_executor, vec![], site_url) + .await + .expect("Expected api discovery to be successful") +} + +trait AutoDiscoveryAttemptFailureExtension { + fn to_find_api_root_failure(self) -> FindApiRootFailure; + fn to_fetch_home_page_reason(self) -> RequestExecutionErrorReason; + fn to_fetch_and_parse_api_root_failure(self) -> FetchAndParseApiRootFailure; +} + +impl AutoDiscoveryAttemptFailureExtension for AutoDiscoveryAttemptFailure { + fn to_find_api_root_failure(self) -> FindApiRootFailure { + if let AutoDiscoveryAttemptFailure::FindApiRoot { + find_api_root_failure, + .. + } = self + { + find_api_root_failure.clone() + } else { + panic!( + "Expected AutoDiscoveryAttemptFailure::FindApiRoot, got: {:?}", + self + ); + } + } + + fn to_fetch_home_page_reason(self) -> RequestExecutionErrorReason { + let error = self.to_find_api_root_failure(); + if let FindApiRootFailure::FetchHomepage { + error: RequestExecutionError::RequestExecutionFailed { reason, .. }, + } = error + { + reason + } else { + panic!( + "Expected FindApiRootFailure::FetchHomepage, got: {:?}", + error + ); + } + } + + fn to_fetch_and_parse_api_root_failure(self) -> FetchAndParseApiRootFailure { + if let AutoDiscoveryAttemptFailure::FetchAndParseApiRoot { + fetch_and_parse_api_root_failure, + .. + } = self + { + fetch_and_parse_api_root_failure + } else { + panic!( + "Expected AutoDiscoveryAttemptFailure::FetchAndParseApiRoot, got: {:?}", + self + ); + } + } +} + +async fn login_err(site_url: &str) -> AutoDiscoveryAttemptFailure { + discovery_helper( + Arc::new(ReqwestRequestExecutor::default()), + vec![], + site_url, + ) + .await + .expect_err("Expected api discovery to fail") +} + +async fn discovery_helper( + request_executor: Arc, + middlewares: Vec>, + site_url: &str, +) -> Result { + let client = WpLoginClient::new( + request_executor, + Arc::new(WpApiMiddlewarePipeline { middlewares }), + ); + client + .api_discovery(site_url.to_string()) + .await + .combined_result() + .map(|success| { + success + .api_details + .find_application_passwords_authentication_url() + .expect("If the discovery is successful, authentication url has to be `Some`") + }) + .map_err(|e| e.clone()) +}