From d682547276d6d5271fb8c41eb26542075efd02a5 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Fri, 21 Jun 2024 00:50:46 -0400 Subject: [PATCH] List `/users//application-passwords` --- wp_api/src/application_passwords.rs | 67 ++++++++++ wp_api/src/lib.rs | 28 +++- wp_api/src/request/endpoint.rs | 1 + .../application_passwords_endpoint.rs | 71 ++++++++++ .../tests/test_application_passwords_immut.rs | 124 ++++++++++++++++++ .../generate/helpers_to_generate_tokens.rs | 14 +- 6 files changed, 295 insertions(+), 10 deletions(-) create mode 100644 wp_api/src/application_passwords.rs create mode 100644 wp_api/src/request/endpoint/application_passwords_endpoint.rs create mode 100644 wp_api/tests/test_application_passwords_immut.rs diff --git a/wp_api/src/application_passwords.rs b/wp_api/src/application_passwords.rs new file mode 100644 index 000000000..958ae83d9 --- /dev/null +++ b/wp_api/src/application_passwords.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Serialize}; +use wp_contextual::WpContextual; + +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)] +pub struct SparseApplicationPassword { + #[WpContext(edit, embed, view)] + pub uuid: Option, + #[WpContext(edit, embed, view)] + pub app_id: Option, + #[WpContext(edit, embed, view)] + pub name: Option, + #[WpContext(edit, view)] + pub created: Option, + #[WpContextualOption] + #[WpContext(edit, view)] + pub last_used: Option, + #[WpContextualOption] + #[WpContext(edit, view)] + pub last_ip: Option, + #[WpContextualOption] + #[WpContext(edit)] + pub password: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum SparseApplicationPasswordField { + Uuid, + AppId, + Name, + Created, + LastUsed, + LastIp, + Password, +} + +impl crate::SparseField for SparseApplicationPasswordField { + fn as_str(&self) -> &str { + match self { + Self::Uuid => "uuid", + Self::AppId => "app_id", + Self::Name => "name", + Self::Created => "created", + Self::LastUsed => "last_used", + Self::LastIp => "last_ip", + Self::Password => "password", + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, uniffi::Record)] +#[serde(transparent)] +pub struct ApplicationPasswordUuid { + pub uuid: String, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, uniffi::Record)] +#[serde(transparent)] +pub struct ApplicationPasswordAppId { + pub app_id: String, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, uniffi::Record)] +#[serde(transparent)] +pub struct IpAddress { + #[serde(alias = "last_ip")] + pub value: String, +} diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index d0710f69e..adbb1a324 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -1,9 +1,15 @@ #![allow(dead_code, unused_variables)] use request::{ - endpoint::ApiBaseUrl, plugins_request_builder::PluginsRequestBuilder, - users_request_builder::UsersRequestBuilder, RequestExecutor, UsersRequestBuilder2, - WpNetworkResponse, + endpoint::{ + application_passwords_endpoint::{ + ApplicationPasswordsRequestBuilder2, ApplicationPasswordsRequestExecutor, + }, + ApiBaseUrl, + }, + plugins_request_builder::PluginsRequestBuilder, + users_request_builder::UsersRequestBuilder, + RequestExecutor, UsersRequestBuilder2, WpNetworkResponse, }; use std::sync::Arc; @@ -15,6 +21,7 @@ use plugins::*; use users::*; mod api_error; // re-exported relevant types +pub mod application_passwords; pub mod login; pub mod plugins; pub mod request; @@ -62,6 +69,7 @@ impl WpApiRequestBuilder { #[derive(Debug, uniffi::Object)] pub struct WpRequestBuilder { + application_passwords: Arc, users: Arc, plugins: Arc, } @@ -80,17 +88,29 @@ impl WpRequestBuilder { })? .into(); let request_builder = Arc::new(request::RequestBuilder::new( - request_executor, + request_executor.clone(), authentication.clone(), )); Ok(Self { + application_passwords: ApplicationPasswordsRequestExecutor::new( + ApplicationPasswordsRequestBuilder2::new( + api_base_url.clone(), + request_builder.clone(), + ), + request_executor.clone(), + ) + .into(), users: UsersRequestBuilder::new(api_base_url.clone(), request_builder.clone()).into(), plugins: PluginsRequestBuilder::new(api_base_url.clone(), request_builder.clone()) .into(), }) } + pub fn application_passwords(&self) -> Arc { + self.application_passwords.clone() + } + pub fn users(&self) -> Arc { self.users.clone() } diff --git a/wp_api/src/request/endpoint.rs b/wp_api/src/request/endpoint.rs index b901fdb28..d048839d6 100644 --- a/wp_api/src/request/endpoint.rs +++ b/wp_api/src/request/endpoint.rs @@ -2,6 +2,7 @@ use url::Url; use crate::SparseField; +pub(crate) mod application_passwords_endpoint; pub(crate) mod plugins_endpoint; pub(crate) mod users_endpoint; diff --git a/wp_api/src/request/endpoint/application_passwords_endpoint.rs b/wp_api/src/request/endpoint/application_passwords_endpoint.rs new file mode 100644 index 000000000..2f90efeea --- /dev/null +++ b/wp_api/src/request/endpoint/application_passwords_endpoint.rs @@ -0,0 +1,71 @@ +use wp_derive_request_builder::WpDerivedRequest; + +use crate::application_passwords::SparseApplicationPasswordField; +use crate::users::UserId; + +#[derive(WpDerivedRequest)] +#[SparseField(SparseApplicationPasswordField)] +enum ApplicationPasswordsRequest { + #[contextual_get(url = "/users//application-passwords", output = Vec)] + List, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + request::endpoint::{ + tests::{fixture_api_base_url, validate_endpoint}, + ApiBaseUrl, + }, + WpContext, + }; + use rstest::*; + use std::sync::Arc; + + #[rstest] + fn list_application_passwords_with_edit_context(endpoint: ApplicationPasswordsRequestEndpoint) { + validate_endpoint( + endpoint.list_with_edit_context(UserId(2)), + "/users/2/application-passwords?context=edit", + ); + } + + #[rstest] + fn list_application_passwords_with_embed_context( + endpoint: ApplicationPasswordsRequestEndpoint, + ) { + validate_endpoint( + endpoint.list_with_embed_context(UserId(71)), + "/users/71/application-passwords?context=embed", + ); + } + + #[rstest] + fn list_application_passwords_with_view_context(endpoint: ApplicationPasswordsRequestEndpoint) { + validate_endpoint( + endpoint.list_with_view_context(UserId(9999)), + "/users/9999/application-passwords?context=view", + ); + } + + #[rstest] + #[case(WpContext::Edit, &[SparseApplicationPasswordField::Uuid], "/users/2/application-passwords?context=edit&_fields=uuid")] + #[case(WpContext::View, &[SparseApplicationPasswordField::Uuid, SparseApplicationPasswordField::Name], "/users/2/application-passwords?context=view&_fields=uuid%2Cname")] + fn filter_list_application_passwords( + endpoint: ApplicationPasswordsRequestEndpoint, + #[case] context: WpContext, + #[case] fields: &[SparseApplicationPasswordField], + #[case] expected_path: &str, + ) { + validate_endpoint( + endpoint.filter_list(UserId(2), context, fields), + expected_path, + ); + } + + #[fixture] + fn endpoint(fixture_api_base_url: Arc) -> ApplicationPasswordsRequestEndpoint { + ApplicationPasswordsRequestEndpoint::new(fixture_api_base_url) + } +} diff --git a/wp_api/tests/test_application_passwords_immut.rs b/wp_api/tests/test_application_passwords_immut.rs new file mode 100644 index 000000000..b0e95a654 --- /dev/null +++ b/wp_api/tests/test_application_passwords_immut.rs @@ -0,0 +1,124 @@ +use rstest::*; +use rstest_reuse::{self, apply, template}; +use wp_api::application_passwords::{SparseApplicationPassword, SparseApplicationPasswordField}; +use wp_api::users::UserId; +use wp_api::WpContext; + +use crate::integration_test_common::{ + request_builder, AssertResponse, FIRST_USER_ID, SECOND_USER_ID, +}; + +pub mod integration_test_common; +pub mod reusable_test_cases; + +#[apply(filter_fields_cases)] +#[tokio::test] +async fn filter_application_passwords( + #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, + #[case] fields: &[SparseApplicationPasswordField], +) { + request_builder() + .application_passwords() + .filter_list(user_id, WpContext::Edit, fields) + .await + .assert_response() + .iter() + .for_each(|p| validate_sparse_application_password_fields(p, fields)); +} + +#[rstest] +#[tokio::test] +async fn list_application_passwords_with_edit_context( + #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, +) { + request_builder() + .application_passwords() + .list_with_edit_context(user_id) + .await + .assert_response(); +} + +#[rstest] +#[tokio::test] +async fn list_application_passwords_with_embed_context( + #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, +) { + request_builder() + .application_passwords() + .list_with_embed_context(user_id) + .await + .assert_response(); +} + +#[rstest] +#[tokio::test] +async fn list_application_passwords_with_view_context( + #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, +) { + request_builder() + .application_passwords() + .list_with_view_context(user_id) + .await + .assert_response(); +} + +// TODO: This might not be a good test case to keep, but it's helpful during initial implementation +// to ensure that the ip address is properly parsed +#[tokio::test] +async fn list_application_passwords_ensure_last_ip() { + let list = request_builder() + .application_passwords() + .list_with_edit_context(FIRST_USER_ID) + .await + .assert_response(); + assert!(list.first().unwrap().last_ip.is_some()); +} + +fn validate_sparse_application_password_fields( + app_password: &SparseApplicationPassword, + fields: &[SparseApplicationPasswordField], +) { + let field_included = |field| { + // If "fields" is empty the server will return all fields + fields.is_empty() || fields.contains(&field) + }; + assert_eq!( + app_password.uuid.is_some(), + field_included(SparseApplicationPasswordField::Uuid) + ); + assert_eq!( + app_password.app_id.is_some(), + field_included(SparseApplicationPasswordField::AppId) + ); + assert_eq!( + app_password.name.is_some(), + field_included(SparseApplicationPasswordField::Name) + ); + assert_eq!( + app_password.created.is_some(), + field_included(SparseApplicationPasswordField::Created) + ); + // Do not test existence of `last_used`, `last_ip` or `password` as there is + // no guarantee that they'll be included even if it's in the requested field list + if !field_included(SparseApplicationPasswordField::LastUsed) { + assert!(app_password.last_used.is_none()); + } + if !field_included(SparseApplicationPasswordField::LastIp) { + assert!(app_password.last_ip.is_none()); + } + if !field_included(SparseApplicationPasswordField::Password) { + assert!(app_password.password.is_none()); + } +} + +#[template] +#[rstest] +#[case(&[])] +#[case(&[SparseApplicationPasswordField::Uuid])] +#[case(&[SparseApplicationPasswordField::AppId])] +#[case(&[SparseApplicationPasswordField::Name])] +#[case(&[SparseApplicationPasswordField::Created])] +#[case(&[SparseApplicationPasswordField::LastUsed])] +#[case(&[SparseApplicationPasswordField::LastIp])] +#[case(&[SparseApplicationPasswordField::Uuid, SparseApplicationPasswordField::Name])] +fn filter_fields_cases(#[case] fields: &[SparseApplicationPasswordField]) {} diff --git a/wp_derive_request_builder/src/generate/helpers_to_generate_tokens.rs b/wp_derive_request_builder/src/generate/helpers_to_generate_tokens.rs index f0cab415c..6752a119b 100644 --- a/wp_derive_request_builder/src/generate/helpers_to_generate_tokens.rs +++ b/wp_derive_request_builder/src/generate/helpers_to_generate_tokens.rs @@ -123,7 +123,9 @@ pub fn fn_context_param(context_and_filter_handler: ContextAndFilterHandler) -> | ContextAndFilterHandler::NoFilterTakeContextAsFunctionName(_) | ContextAndFilterHandler::FilterNoContext => TokenStream::new(), ContextAndFilterHandler::NoFilterTakeContextAsArgument - | ContextAndFilterHandler::FilterTakeContextAsArgument => quote! { context: WpContext, }, + | ContextAndFilterHandler::FilterTakeContextAsArgument => { + quote! { context: crate::WpContext, } + } } } @@ -445,7 +447,7 @@ mod tests { )] #[case( ContextAndFilterHandler::NoFilterTakeContextAsArgument, - "context : WpContext ," + "context : crate :: WpContext ," )] #[case( ContextAndFilterHandler::NoFilterTakeContextAsFunctionName(WpContext::Embed), @@ -453,7 +455,7 @@ mod tests { )] #[case( ContextAndFilterHandler::FilterTakeContextAsArgument, - "context : WpContext ," + "context : crate :: WpContext ," )] fn test_fn_context_param( #[case] context_and_filter_handler: ContextAndFilterHandler, @@ -692,7 +694,7 @@ mod tests { &referenced_params_type("UserListParams"), RequestType::ContextualGet, ContextAndFilterHandler::NoFilterTakeContextAsArgument, - "fn list (& self , context : WpContext , params : &UserListParams ,)")] + "fn list (& self , context : crate :: WpContext , params : &UserListParams ,)")] #[case( PartOf::Endpoint, format_ident!("List"), @@ -700,7 +702,7 @@ mod tests { &referenced_params_type("UserListParams"), RequestType::ContextualGet, ContextAndFilterHandler::FilterTakeContextAsArgument, - "fn filter_list (& self , context : WpContext , params : &UserListParams , fields : & [SparseUserField])")] + "fn filter_list (& self , context : crate :: WpContext , params : &UserListParams , fields : & [SparseUserField])")] #[case( PartOf::Endpoint, format_ident!("Retrieve"), @@ -716,7 +718,7 @@ mod tests { &ParamsType::new(None), RequestType::ContextualGet, ContextAndFilterHandler::FilterTakeContextAsArgument, - "fn filter_retrieve (& self , user_id : UserId , context : WpContext , fields : & [SparseUserField])")] + "fn filter_retrieve (& self , user_id : UserId , context : crate :: WpContext , fields : & [SparseUserField])")] #[case( PartOf::Endpoint, format_ident!("Update"),