From 545823a7c481efd71a56fcdaaf35cf0f27b24295 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Tue, 2 Jul 2024 18:14:12 -0400 Subject: [PATCH 01/22] Add application password uuid to test_credentials --- native/kotlin/api/android/build.gradle.kts | 10 ++++++++-- scripts/setup-test-site.sh | 2 ++ wp_api/build.rs | 14 +++++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/native/kotlin/api/android/build.gradle.kts b/native/kotlin/api/android/build.gradle.kts index 2f8186f83..2f7d41392 100644 --- a/native/kotlin/api/android/build.gradle.kts +++ b/native/kotlin/api/android/build.gradle.kts @@ -33,6 +33,7 @@ android { buildConfigField("String", "TEST_SITE_URL", "\"${it.siteUrl}\"") buildConfigField("String", "TEST_ADMIN_USERNAME", "\"${it.adminUsername}\"") buildConfigField("String", "TEST_ADMIN_PASSWORD", "\"${it.adminPassword}\"") + buildConfigField("String", "TEST_ADMIN_PASSWORD_UUID", "\"${it.adminPasswordUuid}\"") buildConfigField( "String", "TEST_SUBSCRIBER_USERNAME", @@ -43,6 +44,7 @@ android { "TEST_SUBSCRIBER_PASSWORD", "\"${it.subscriberPassword}\"" ) + buildConfigField("String", "TEST_SUBSCRIBER_PASSWORD_UUID", "\"${it.subscriberPasswordUuid}\"") } } } @@ -163,8 +165,10 @@ fun readTestCredentials(): TestCredentials? { siteUrl = siteUrl, adminUsername = lines[1], adminPassword = lines[2], - subscriberUsername = lines[3], - subscriberPassword = lines[4] + adminPasswordUuid = lines[3], + subscriberUsername = lines[4], + subscriberPassword = lines[5], + subscriberPasswordUuid = lines[6] ) } @@ -172,6 +176,8 @@ data class TestCredentials( val siteUrl: String, val adminUsername: String, val adminPassword: String, + val adminPasswordUuid: String, val subscriberUsername: String, val subscriberPassword: String, + val subscriberPasswordUuid: String, ) diff --git a/scripts/setup-test-site.sh b/scripts/setup-test-site.sh index 3aa95ef39..182f50251 100755 --- a/scripts/setup-test-site.sh +++ b/scripts/setup-test-site.sh @@ -80,9 +80,11 @@ wp plugin delete wordpress-importer printf "http://localhost\ntest@example.com\n" ## Create an Application password for the admin user, and store it where it can be used by the test suite wp user application-password create test@example.com test --porcelain + wp user application-password list test@example.com --fields=uuid --format=csv | sed -n '2 p' printf "themedemos\n" ## Create an Application password for a subscriber user, and store it where it can be used by the test suite wp user application-password create themedemos test --porcelain + wp user application-password list themedemos --fields=uuid --format=csv | sed -n '2 p' } >> /tmp/test_credentials ## Used for integration tests diff --git a/wp_api/build.rs b/wp_api/build.rs index 840f0cea2..747c7b0e9 100644 --- a/wp_api/build.rs +++ b/wp_api/build.rs @@ -30,8 +30,10 @@ struct TestCredentials { site_url: String, admin_username: String, admin_password: String, + admin_password_uuid: String, subscriber_username: String, subscriber_password: String, + subscriber_password_uuid: String, } impl TestCredentials { @@ -44,8 +46,10 @@ impl TestCredentials { site_url: lines[0].to_string(), admin_username: lines[1].to_string(), admin_password: lines[2].to_string(), - subscriber_username: lines[3].to_string(), - subscriber_password: lines[4].to_string(), + admin_password_uuid: lines[3].to_string(), + subscriber_username: lines[4].to_string(), + subscriber_password: lines[5].to_string(), + subscriber_password_uuid: lines[6].to_string(), }); } } @@ -58,14 +62,18 @@ impl TestCredentials { pub const TEST_CREDENTIALS_SITE_URL: &str = "{}"; pub const TEST_CREDENTIALS_ADMIN_USERNAME: &str = "{}"; pub const TEST_CREDENTIALS_ADMIN_PASSWORD: &str = "{}"; +pub const TEST_CREDENTIALS_ADMIN_PASSWORD_UUID: &str = "{}"; pub const TEST_CREDENTIALS_SUBSCRIBER_USERNAME: &str = "{}"; pub const TEST_CREDENTIALS_SUBSCRIBER_PASSWORD: &str = "{}"; +pub const TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID: &str = "{}"; "#, self.site_url, self.admin_username, self.admin_password, + self.admin_password_uuid, self.subscriber_username, - self.subscriber_password + self.subscriber_password, + self.subscriber_password_uuid ) .trim() .to_string() From b9452c1b10c30928f933560b46af05aa63917cb0 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Tue, 2 Jul 2024 18:33:34 -0400 Subject: [PATCH 02/22] Implement retrieve application passwords --- wp_api/src/application_passwords.rs | 8 ++ .../application_passwords_endpoint.rs | 37 +++++++- .../tests/test_application_passwords_immut.rs | 90 ++++++++++++++++++- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/wp_api/src/application_passwords.rs b/wp_api/src/application_passwords.rs index 958ae83d9..755908e01 100644 --- a/wp_api/src/application_passwords.rs +++ b/wp_api/src/application_passwords.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use serde::{Deserialize, Serialize}; use wp_contextual::WpContextual; @@ -53,6 +55,12 @@ pub struct ApplicationPasswordUuid { pub uuid: String, } +impl Display for ApplicationPasswordUuid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.uuid) + } +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, uniffi::Record)] #[serde(transparent)] pub struct ApplicationPasswordAppId { diff --git a/wp_api/src/request/endpoint/application_passwords_endpoint.rs b/wp_api/src/request/endpoint/application_passwords_endpoint.rs index e56381534..516d94aef 100644 --- a/wp_api/src/request/endpoint/application_passwords_endpoint.rs +++ b/wp_api/src/request/endpoint/application_passwords_endpoint.rs @@ -1,13 +1,21 @@ use wp_derive_request_builder::WpDerivedRequest; -use crate::application_passwords::SparseApplicationPasswordField; +use crate::application_passwords::{ + ApplicationPasswordUuid, ApplicationPasswordWithEditContext, + ApplicationPasswordWithEmbedContext, ApplicationPasswordWithViewContext, + SparseApplicationPassword, SparseApplicationPasswordField, +}; use crate::users::UserId; #[derive(WpDerivedRequest)] #[SparseField(SparseApplicationPasswordField)] enum ApplicationPasswordsRequest { - #[contextual_get(url = "/users//application-passwords", output = Vec)] + #[contextual_get(url = "/users//application-passwords", output = Vec)] List, + #[contextual_get(url = "/users//application-passwords/", output = SparseApplicationPassword)] + Retrieve, + #[contextual_get(url = "/users//application-passwords/introspect", output = SparseApplicationPassword)] + RetrieveCurrent, } #[cfg(test)] @@ -49,6 +57,31 @@ mod tests { ); } + #[rstest] + fn retrieve_current_application_passwords_with_edit_context( + endpoint: ApplicationPasswordsRequestEndpoint, + ) { + validate_endpoint( + endpoint.retrieve_current_with_edit_context(&UserId(2)), + "/users/2/application-passwords/introspect?context=edit", + ); + } + + #[rstest] + fn retrieve_application_passwords_with_embed_context( + endpoint: ApplicationPasswordsRequestEndpoint, + ) { + validate_endpoint( + endpoint.retrieve_with_embed_context( + &UserId(2), + &ApplicationPasswordUuid { + uuid: "584a87d5-4f18-4c33-a315-4c05ed1fc485".to_string(), + }, + ), + "/users/2/application-passwords/584a87d5-4f18-4c33-a315-4c05ed1fc485?context=embed", + ); + } + #[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")] diff --git a/wp_api/tests/test_application_passwords_immut.rs b/wp_api/tests/test_application_passwords_immut.rs index 056af5260..c1e5539c4 100644 --- a/wp_api/tests/test_application_passwords_immut.rs +++ b/wp_api/tests/test_application_passwords_immut.rs @@ -1,11 +1,15 @@ +use integration_test_common::request_builder_as_subscriber; use rstest::*; use rstest_reuse::{self, apply, template}; -use wp_api::application_passwords::{SparseApplicationPassword, SparseApplicationPasswordField}; +use wp_api::application_passwords::{ + ApplicationPasswordUuid, 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, + TEST_CREDENTIALS_ADMIN_PASSWORD_UUID, TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID, }; pub mod integration_test_common; @@ -74,6 +78,90 @@ async fn list_application_passwords_ensure_last_ip() { assert!(list.first().unwrap().last_ip.is_some()); } +#[tokio::test] +async fn retrieve_current_application_passwords_with_edit_context() { + let a = request_builder() + .application_passwords() + .retrieve_current_with_edit_context(&FIRST_USER_ID) + .await + .assert_response(); + assert_eq!( + a.uuid, + ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string() + } + ); +} + +#[tokio::test] +async fn retrieve_current_application_passwords_with_embed_context() { + let a = request_builder_as_subscriber() + .application_passwords() + .retrieve_current_with_embed_context(&SECOND_USER_ID) + .await + .assert_response(); + assert_eq!( + a.uuid, + ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID.to_string() + } + ); +} + +#[tokio::test] +async fn retrieve_current_application_passwords_with_view_context() { + let a = request_builder() + .application_passwords() + .retrieve_current_with_view_context(&FIRST_USER_ID) + .await + .assert_response(); + assert_eq!( + a.uuid, + ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string() + } + ); +} + +#[tokio::test] +async fn retrieve_application_passwords_with_edit_context() { + let uuid = ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string(), + }; + let a = request_builder() + .application_passwords() + .retrieve_with_edit_context(&FIRST_USER_ID, &uuid) + .await + .assert_response(); + assert_eq!(a.uuid, uuid); +} + +#[tokio::test] +async fn retrieve_application_passwords_with_embed_context() { + let uuid = ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string(), + }; + let a = request_builder() + .application_passwords() + .retrieve_with_embed_context(&FIRST_USER_ID, &uuid) + .await + .assert_response(); + assert_eq!(a.uuid, uuid); +} + +#[tokio::test] +async fn retrieve_application_passwords_with_view_context() { + let uuid = ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID.to_string(), + }; + let a = request_builder() + .application_passwords() + .retrieve_with_view_context(&SECOND_USER_ID, &uuid) + .await + .assert_response(); + assert_eq!(a.uuid, uuid); +} + fn validate_sparse_application_password_fields( app_password: &SparseApplicationPassword, fields: &[SparseApplicationPasswordField], From 0ecd9bfbf214bd46544f1eccbf80b72d3867922c Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Tue, 2 Jul 2024 19:09:03 -0400 Subject: [PATCH 03/22] Implement create application passwords --- wp_api/src/application_passwords.rs | 9 ++++ .../application_passwords_endpoint.rs | 12 ++++- .../tests/test_application_passwords_mut.rs | 54 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 wp_api/tests/test_application_passwords_mut.rs diff --git a/wp_api/src/application_passwords.rs b/wp_api/src/application_passwords.rs index 755908e01..b6bab1eb3 100644 --- a/wp_api/src/application_passwords.rs +++ b/wp_api/src/application_passwords.rs @@ -73,3 +73,12 @@ pub struct IpAddress { #[serde(alias = "last_ip")] pub value: String, } + +#[derive(Debug, Serialize, uniffi::Record)] +pub struct ApplicationPasswordCreateParams { + /// A UUID provided by the application to uniquely identify it. + /// It is recommended to use an UUID v5 with the URL or DNS namespace. + pub app_id: Option, + /// The name of the application password. + pub name: String, +} diff --git a/wp_api/src/request/endpoint/application_passwords_endpoint.rs b/wp_api/src/request/endpoint/application_passwords_endpoint.rs index 516d94aef..59dc7b7f1 100644 --- a/wp_api/src/request/endpoint/application_passwords_endpoint.rs +++ b/wp_api/src/request/endpoint/application_passwords_endpoint.rs @@ -1,7 +1,7 @@ use wp_derive_request_builder::WpDerivedRequest; use crate::application_passwords::{ - ApplicationPasswordUuid, ApplicationPasswordWithEditContext, + ApplicationPasswordCreateParams, ApplicationPasswordUuid, ApplicationPasswordWithEditContext, ApplicationPasswordWithEmbedContext, ApplicationPasswordWithViewContext, SparseApplicationPassword, SparseApplicationPasswordField, }; @@ -10,6 +10,8 @@ use crate::users::UserId; #[derive(WpDerivedRequest)] #[SparseField(SparseApplicationPasswordField)] enum ApplicationPasswordsRequest { + #[post(url = "/users//application-passwords", params = &ApplicationPasswordCreateParams, output = ApplicationPasswordWithEditContext)] + Create, #[contextual_get(url = "/users//application-passwords", output = Vec)] List, #[contextual_get(url = "/users//application-passwords/", output = SparseApplicationPassword)] @@ -31,6 +33,14 @@ mod tests { use rstest::*; use std::sync::Arc; + #[rstest] + fn create_user(endpoint: ApplicationPasswordsRequestEndpoint) { + validate_endpoint( + endpoint.create(&UserId(1)), + "/users/1/application-passwords", + ); + } + #[rstest] fn list_application_passwords_with_edit_context(endpoint: ApplicationPasswordsRequestEndpoint) { validate_endpoint( diff --git a/wp_api/tests/test_application_passwords_mut.rs b/wp_api/tests/test_application_passwords_mut.rs new file mode 100644 index 000000000..ea0dfcccb --- /dev/null +++ b/wp_api/tests/test_application_passwords_mut.rs @@ -0,0 +1,54 @@ +use integration_test_common::AssertResponse; +use wp_api::{application_passwords::ApplicationPasswordCreateParams, users::UserId}; +use wp_db::DbUserMeta; + +use crate::integration_test_common::{request_builder, FIRST_USER_ID}; + +pub mod integration_test_common; +pub mod wp_db; + +#[tokio::test] +async fn create_application_password() { + wp_db::run_and_restore(|mut db| async move { + let password_name = "IntegrationTest"; + // Assert that the application password is not in DB + assert!( + !db_application_password_meta_for_user(&mut db, &FIRST_USER_ID) + .await + .unwrap() + .meta_value + .contains(password_name) + ); + + // Create an application password using the API + let params = ApplicationPasswordCreateParams { + app_id: None, + name: password_name.to_string(), + }; + let created_application_password = request_builder() + .application_passwords() + .create(&FIRST_USER_ID, ¶ms) + .await + .assert_response(); + + // Assert that the application password is in DB + let db_user_meta_after_update = + db_application_password_meta_for_user(&mut db, &FIRST_USER_ID).await; + assert!(db_user_meta_after_update.is_some()); + let meta_value = db_user_meta_after_update.unwrap().meta_value; + assert!(meta_value.contains(password_name)); + assert!(meta_value.contains(&created_application_password.uuid.uuid)); + }) + .await; +} + +async fn db_application_password_meta_for_user( + db: &mut wp_db::WordPressDb, + user_id: &UserId, +) -> Option { + db.user_meta(user_id.0 as u64) + .await + .unwrap() + .into_iter() + .find(|m| m.meta_key == "_application_passwords") +} From b478f11685ae69c3500f3739683aefbdd3801c5a Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Tue, 2 Jul 2024 19:23:01 -0400 Subject: [PATCH 04/22] Implement delete all application passwords --- wp_api/src/application_passwords.rs | 6 +++ .../application_passwords_endpoint.rs | 16 ++++++-- .../tests/test_application_passwords_mut.rs | 38 ++++++++++++++++++- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/wp_api/src/application_passwords.rs b/wp_api/src/application_passwords.rs index b6bab1eb3..49a77e85f 100644 --- a/wp_api/src/application_passwords.rs +++ b/wp_api/src/application_passwords.rs @@ -82,3 +82,9 @@ pub struct ApplicationPasswordCreateParams { /// The name of the application password. pub name: String, } + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ApplicationPasswordDeleteResponse { + pub deleted: bool, + pub count: i32, +} diff --git a/wp_api/src/request/endpoint/application_passwords_endpoint.rs b/wp_api/src/request/endpoint/application_passwords_endpoint.rs index 59dc7b7f1..54d8e55de 100644 --- a/wp_api/src/request/endpoint/application_passwords_endpoint.rs +++ b/wp_api/src/request/endpoint/application_passwords_endpoint.rs @@ -1,9 +1,9 @@ use wp_derive_request_builder::WpDerivedRequest; use crate::application_passwords::{ - ApplicationPasswordCreateParams, ApplicationPasswordUuid, ApplicationPasswordWithEditContext, - ApplicationPasswordWithEmbedContext, ApplicationPasswordWithViewContext, - SparseApplicationPassword, SparseApplicationPasswordField, + ApplicationPasswordCreateParams, ApplicationPasswordDeleteResponse, ApplicationPasswordUuid, + ApplicationPasswordWithEditContext, ApplicationPasswordWithEmbedContext, + ApplicationPasswordWithViewContext, SparseApplicationPassword, SparseApplicationPasswordField, }; use crate::users::UserId; @@ -12,6 +12,8 @@ use crate::users::UserId; enum ApplicationPasswordsRequest { #[post(url = "/users//application-passwords", params = &ApplicationPasswordCreateParams, output = ApplicationPasswordWithEditContext)] Create, + #[delete(url = "/users//application-passwords", output = ApplicationPasswordDeleteResponse)] + DeleteAll, #[contextual_get(url = "/users//application-passwords", output = Vec)] List, #[contextual_get(url = "/users//application-passwords/", output = SparseApplicationPassword)] @@ -41,6 +43,14 @@ mod tests { ); } + #[rstest] + fn delete_user(endpoint: ApplicationPasswordsRequestEndpoint) { + validate_endpoint( + endpoint.delete_all(&UserId(1)), + "/users/1/application-passwords", + ); + } + #[rstest] fn list_application_passwords_with_edit_context(endpoint: ApplicationPasswordsRequestEndpoint) { validate_endpoint( diff --git a/wp_api/tests/test_application_passwords_mut.rs b/wp_api/tests/test_application_passwords_mut.rs index ea0dfcccb..87cc0703e 100644 --- a/wp_api/tests/test_application_passwords_mut.rs +++ b/wp_api/tests/test_application_passwords_mut.rs @@ -1,8 +1,10 @@ -use integration_test_common::AssertResponse; use wp_api::{application_passwords::ApplicationPasswordCreateParams, users::UserId}; use wp_db::DbUserMeta; -use crate::integration_test_common::{request_builder, FIRST_USER_ID}; +use crate::integration_test_common::{ + request_builder, AssertResponse, FIRST_USER_ID, SECOND_USER_ID, + TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID, +}; pub mod integration_test_common; pub mod wp_db; @@ -42,6 +44,38 @@ async fn create_application_password() { .await; } +#[tokio::test] +async fn delete_all_application_passwords() { + wp_db::run_and_restore(|mut db| async move { + // Assert that the application password is in DB + assert!( + db_application_password_meta_for_user(&mut db, &SECOND_USER_ID) + .await + .unwrap() + .meta_value + .contains(TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID) + ); + // Delete the user's application passwords using the API and ensure it's successful + let application_password_delete_response = request_builder() + .application_passwords() + .delete_all(&SECOND_USER_ID) + .await + .assert_response(); + + // Assert that the application password is deleted and no longer in DB + assert!(application_password_delete_response.deleted); + assert_eq!(application_password_delete_response.count, 1); + assert!( + !db_application_password_meta_for_user(&mut db, &SECOND_USER_ID) + .await + .unwrap() + .meta_value + .contains(TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID) + ); + }) + .await; +} + async fn db_application_password_meta_for_user( db: &mut wp_db::WordPressDb, user_id: &UserId, From 301321e0a7142ef370822cc9c4e21547024b9374 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Tue, 2 Jul 2024 19:46:51 -0400 Subject: [PATCH 05/22] Implement delete single application password --- wp_api/src/application_passwords.rs | 6 +++ .../application_passwords_endpoint.rs | 28 ++++++++--- .../tests/test_application_passwords_mut.rs | 46 +++++++++++++++++-- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/wp_api/src/application_passwords.rs b/wp_api/src/application_passwords.rs index 49a77e85f..f10b83bcd 100644 --- a/wp_api/src/application_passwords.rs +++ b/wp_api/src/application_passwords.rs @@ -85,6 +85,12 @@ pub struct ApplicationPasswordCreateParams { #[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct ApplicationPasswordDeleteResponse { + pub deleted: bool, + pub previous: ApplicationPasswordWithEditContext, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct ApplicationPasswordDeleteAllResponse { pub deleted: bool, pub count: i32, } diff --git a/wp_api/src/request/endpoint/application_passwords_endpoint.rs b/wp_api/src/request/endpoint/application_passwords_endpoint.rs index 54d8e55de..63fe7464a 100644 --- a/wp_api/src/request/endpoint/application_passwords_endpoint.rs +++ b/wp_api/src/request/endpoint/application_passwords_endpoint.rs @@ -1,9 +1,10 @@ use wp_derive_request_builder::WpDerivedRequest; use crate::application_passwords::{ - ApplicationPasswordCreateParams, ApplicationPasswordDeleteResponse, ApplicationPasswordUuid, - ApplicationPasswordWithEditContext, ApplicationPasswordWithEmbedContext, - ApplicationPasswordWithViewContext, SparseApplicationPassword, SparseApplicationPasswordField, + ApplicationPasswordCreateParams, ApplicationPasswordDeleteAllResponse, + ApplicationPasswordDeleteResponse, ApplicationPasswordUuid, ApplicationPasswordWithEditContext, + ApplicationPasswordWithEmbedContext, ApplicationPasswordWithViewContext, + SparseApplicationPassword, SparseApplicationPasswordField, }; use crate::users::UserId; @@ -12,7 +13,9 @@ use crate::users::UserId; enum ApplicationPasswordsRequest { #[post(url = "/users//application-passwords", params = &ApplicationPasswordCreateParams, output = ApplicationPasswordWithEditContext)] Create, - #[delete(url = "/users//application-passwords", output = ApplicationPasswordDeleteResponse)] + #[delete(url = "/users//application-passwords/", output = ApplicationPasswordDeleteResponse)] + Delete, + #[delete(url = "/users//application-passwords", output = ApplicationPasswordDeleteAllResponse)] DeleteAll, #[contextual_get(url = "/users//application-passwords", output = Vec)] List, @@ -36,7 +39,7 @@ mod tests { use std::sync::Arc; #[rstest] - fn create_user(endpoint: ApplicationPasswordsRequestEndpoint) { + fn create_application_password(endpoint: ApplicationPasswordsRequestEndpoint) { validate_endpoint( endpoint.create(&UserId(1)), "/users/1/application-passwords", @@ -44,7 +47,20 @@ mod tests { } #[rstest] - fn delete_user(endpoint: ApplicationPasswordsRequestEndpoint) { + fn delete_single_application_password(endpoint: ApplicationPasswordsRequestEndpoint) { + validate_endpoint( + endpoint.delete( + &UserId(2), + &ApplicationPasswordUuid { + uuid: "584a87d5-4f18-4c33-a315-4c05ed1fc485".to_string(), + }, + ), + "/users/2/application-passwords/584a87d5-4f18-4c33-a315-4c05ed1fc485", + ); + } + + #[rstest] + fn delete_all_application_passwords(endpoint: ApplicationPasswordsRequestEndpoint) { validate_endpoint( endpoint.delete_all(&UserId(1)), "/users/1/application-passwords", diff --git a/wp_api/tests/test_application_passwords_mut.rs b/wp_api/tests/test_application_passwords_mut.rs index 87cc0703e..4bf9cfdfd 100644 --- a/wp_api/tests/test_application_passwords_mut.rs +++ b/wp_api/tests/test_application_passwords_mut.rs @@ -1,4 +1,7 @@ -use wp_api::{application_passwords::ApplicationPasswordCreateParams, users::UserId}; +use wp_api::{ + application_passwords::{ApplicationPasswordCreateParams, ApplicationPasswordUuid}, + users::UserId, +}; use wp_db::DbUserMeta; use crate::integration_test_common::{ @@ -44,6 +47,41 @@ async fn create_application_password() { .await; } +#[tokio::test] +async fn delete_single_application_password() { + wp_db::run_and_restore(|mut db| async move { + let uuid = ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID.to_string(), + }; + // Assert that the application password is in DB + assert!( + db_application_password_meta_for_user(&mut db, &SECOND_USER_ID) + .await + .unwrap() + .meta_value + .contains(TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID) + ); + // Delete the user's application passwords using the API and ensure it's successful + let response = request_builder() + .application_passwords() + .delete(&SECOND_USER_ID, &uuid) + .await + .assert_response(); + + // Assert that the application password is deleted and no longer in DB + assert!(response.deleted); + assert_eq!(response.previous.uuid, uuid); + assert!( + !db_application_password_meta_for_user(&mut db, &SECOND_USER_ID) + .await + .unwrap() + .meta_value + .contains(TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID) + ); + }) + .await; +} + #[tokio::test] async fn delete_all_application_passwords() { wp_db::run_and_restore(|mut db| async move { @@ -56,15 +94,15 @@ async fn delete_all_application_passwords() { .contains(TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID) ); // Delete the user's application passwords using the API and ensure it's successful - let application_password_delete_response = request_builder() + let response = request_builder() .application_passwords() .delete_all(&SECOND_USER_ID) .await .assert_response(); // Assert that the application password is deleted and no longer in DB - assert!(application_password_delete_response.deleted); - assert_eq!(application_password_delete_response.count, 1); + assert!(response.deleted); + assert_eq!(response.count, 1); assert!( !db_application_password_meta_for_user(&mut db, &SECOND_USER_ID) .await From c749d45b002841ca045132f56ac49a8ef72aec41 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Tue, 2 Jul 2024 21:35:56 -0400 Subject: [PATCH 06/22] Add serial_test --- Cargo.lock | 41 +++++++++++++++++++ wp_api/Cargo.toml | 1 + .../tests/test_application_passwords_immut.rs | 12 ++++++ .../tests/test_application_passwords_mut.rs | 4 ++ .../test_manual_request_builder_immut.rs | 2 + wp_api/tests/test_plugins_immut.rs | 5 +++ wp_api/tests/test_plugins_mut.rs | 4 ++ wp_api/tests/test_users_immut.rs | 16 ++++++++ wp_api/tests/test_users_mut.rs | 14 +++++++ 9 files changed, 99 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 71c3401da..495ecb99c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1691,6 +1691,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -1726,6 +1735,12 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "security-framework" version = "2.11.0" @@ -1810,6 +1825,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3016,6 +3056,7 @@ dependencies = [ "rstest_reuse", "serde", "serde_json", + "serial_test", "sqlx", "thiserror", "tokio", diff --git a/wp_api/Cargo.toml b/wp_api/Cargo.toml index d550d2da9..ee4523f3d 100644 --- a/wp_api/Cargo.toml +++ b/wp_api/Cargo.toml @@ -29,6 +29,7 @@ reqwest = "0.12" rstest = { workspace = true } rstest_reuse = { workspace = true } serde_json = { workspace = true } +serial_test = "3.1" sqlx = { version = "0.7", features = [ "chrono", "mysql", "runtime-tokio", "tls-native-tls" ]} tokio = { version = "1.37", features = [ "full" ] } diff --git a/wp_api/tests/test_application_passwords_immut.rs b/wp_api/tests/test_application_passwords_immut.rs index c1e5539c4..8afa9e62f 100644 --- a/wp_api/tests/test_application_passwords_immut.rs +++ b/wp_api/tests/test_application_passwords_immut.rs @@ -1,6 +1,7 @@ use integration_test_common::request_builder_as_subscriber; use rstest::*; use rstest_reuse::{self, apply, template}; +use serial_test::parallel; use wp_api::application_passwords::{ ApplicationPasswordUuid, SparseApplicationPassword, SparseApplicationPasswordField, }; @@ -17,6 +18,7 @@ pub mod reusable_test_cases; #[apply(filter_fields_cases)] #[tokio::test] +#[parallel] async fn filter_application_passwords( #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, #[case] fields: &[SparseApplicationPasswordField], @@ -32,6 +34,7 @@ async fn filter_application_passwords( #[rstest] #[tokio::test] +#[parallel] async fn list_application_passwords_with_edit_context( #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, ) { @@ -44,6 +47,7 @@ async fn list_application_passwords_with_edit_context( #[rstest] #[tokio::test] +#[parallel] async fn list_application_passwords_with_embed_context( #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, ) { @@ -56,6 +60,7 @@ async fn list_application_passwords_with_embed_context( #[rstest] #[tokio::test] +#[parallel] async fn list_application_passwords_with_view_context( #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, ) { @@ -69,6 +74,7 @@ async fn list_application_passwords_with_view_context( // 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] +#[parallel] async fn list_application_passwords_ensure_last_ip() { let list = request_builder() .application_passwords() @@ -79,6 +85,7 @@ async fn list_application_passwords_ensure_last_ip() { } #[tokio::test] +#[parallel] async fn retrieve_current_application_passwords_with_edit_context() { let a = request_builder() .application_passwords() @@ -94,6 +101,7 @@ async fn retrieve_current_application_passwords_with_edit_context() { } #[tokio::test] +#[parallel] async fn retrieve_current_application_passwords_with_embed_context() { let a = request_builder_as_subscriber() .application_passwords() @@ -109,6 +117,7 @@ async fn retrieve_current_application_passwords_with_embed_context() { } #[tokio::test] +#[parallel] async fn retrieve_current_application_passwords_with_view_context() { let a = request_builder() .application_passwords() @@ -124,6 +133,7 @@ async fn retrieve_current_application_passwords_with_view_context() { } #[tokio::test] +#[parallel] async fn retrieve_application_passwords_with_edit_context() { let uuid = ApplicationPasswordUuid { uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string(), @@ -137,6 +147,7 @@ async fn retrieve_application_passwords_with_edit_context() { } #[tokio::test] +#[parallel] async fn retrieve_application_passwords_with_embed_context() { let uuid = ApplicationPasswordUuid { uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string(), @@ -150,6 +161,7 @@ async fn retrieve_application_passwords_with_embed_context() { } #[tokio::test] +#[parallel] async fn retrieve_application_passwords_with_view_context() { let uuid = ApplicationPasswordUuid { uuid: TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID.to_string(), diff --git a/wp_api/tests/test_application_passwords_mut.rs b/wp_api/tests/test_application_passwords_mut.rs index 4bf9cfdfd..0df949c8b 100644 --- a/wp_api/tests/test_application_passwords_mut.rs +++ b/wp_api/tests/test_application_passwords_mut.rs @@ -1,3 +1,4 @@ +use serial_test::serial; use wp_api::{ application_passwords::{ApplicationPasswordCreateParams, ApplicationPasswordUuid}, users::UserId, @@ -13,6 +14,7 @@ pub mod integration_test_common; pub mod wp_db; #[tokio::test] +#[serial] async fn create_application_password() { wp_db::run_and_restore(|mut db| async move { let password_name = "IntegrationTest"; @@ -48,6 +50,7 @@ async fn create_application_password() { } #[tokio::test] +#[serial] async fn delete_single_application_password() { wp_db::run_and_restore(|mut db| async move { let uuid = ApplicationPasswordUuid { @@ -83,6 +86,7 @@ async fn delete_single_application_password() { } #[tokio::test] +#[serial] async fn delete_all_application_passwords() { wp_db::run_and_restore(|mut db| async move { // Assert that the application password is in DB diff --git a/wp_api/tests/test_manual_request_builder_immut.rs b/wp_api/tests/test_manual_request_builder_immut.rs index 4bbe97b52..a95de4d62 100644 --- a/wp_api/tests/test_manual_request_builder_immut.rs +++ b/wp_api/tests/test_manual_request_builder_immut.rs @@ -5,6 +5,7 @@ use integration_test_common::{ use reusable_test_cases::list_users_cases; use rstest::*; use rstest_reuse::{self, apply}; +use serial_test::parallel; use wp_api::{ generate, users::UserWithEditContext, @@ -20,6 +21,7 @@ pub mod reusable_test_cases; #[apply(list_users_cases)] #[tokio::test] +#[parallel] async fn list_users_with_edit_context(#[case] params: UserListParams) { let authentication = WpAuthentication::from_username_and_password( TEST_CREDENTIALS_ADMIN_USERNAME.to_string(), diff --git a/wp_api/tests/test_plugins_immut.rs b/wp_api/tests/test_plugins_immut.rs index b4e41bed6..1eabce0a9 100644 --- a/wp_api/tests/test_plugins_immut.rs +++ b/wp_api/tests/test_plugins_immut.rs @@ -1,5 +1,6 @@ use rstest::*; use rstest_reuse::{self, apply, template}; +use serial_test::parallel; use wp_api::{ generate, plugins::{PluginListParams, PluginSlug, PluginStatus, SparsePlugin, SparsePluginField}, @@ -14,6 +15,7 @@ pub mod integration_test_common; #[apply(filter_fields_cases)] #[tokio::test] +#[parallel] async fn filter_plugins( #[case] fields: &[SparsePluginField], #[values( @@ -34,6 +36,7 @@ async fn filter_plugins( #[apply(filter_fields_cases)] #[tokio::test] +#[parallel] async fn filter_retrieve_plugin( #[case] fields: &[SparsePluginField], #[values(CLASSIC_EDITOR_PLUGIN_SLUG, HELLO_DOLLY_PLUGIN_SLUG)] slug: &str, @@ -53,6 +56,7 @@ async fn filter_retrieve_plugin( #[case(generate!(PluginListParams, (search, Some("foo".to_string())), (status, Some(PluginStatus::Inactive))))] #[trace] #[tokio::test] +#[parallel] async fn list_plugins( #[case] params: PluginListParams, #[values(WpContext::Edit, WpContext::Embed, WpContext::View)] context: WpContext, @@ -87,6 +91,7 @@ async fn list_plugins( #[case(HELLO_DOLLY_PLUGIN_SLUG.into(), "Matt Mullenweg", "http://wordpress.org/plugins/hello-dolly/")] #[trace] #[tokio::test] +#[parallel] async fn retrieve_plugin( #[case] plugin_slug: PluginSlug, #[case] expected_author: &str, diff --git a/wp_api/tests/test_plugins_mut.rs b/wp_api/tests/test_plugins_mut.rs index 1608d3d39..bc5020f0e 100644 --- a/wp_api/tests/test_plugins_mut.rs +++ b/wp_api/tests/test_plugins_mut.rs @@ -1,5 +1,6 @@ use integration_test_common::AssertResponse; use rstest::rstest; +use serial_test::serial; use wp_api::plugins::{PluginCreateParams, PluginSlug, PluginStatus, PluginUpdateParams}; use crate::integration_test_common::{ @@ -11,6 +12,7 @@ pub mod integration_test_common; pub mod wp_db; #[tokio::test] +#[serial] async fn create_plugin() { run_and_restore_wp_content_plugins(|| { wp_db::run_and_restore(|mut _db| async move { @@ -36,6 +38,7 @@ async fn create_plugin() { #[case(PluginSlug::new(CLASSIC_EDITOR_PLUGIN_SLUG.into()), PluginStatus::Inactive)] #[trace] #[tokio::test] +#[serial] async fn update_plugin(#[case] slug: PluginSlug, #[case] new_status: PluginStatus) { run_and_restore_wp_content_plugins(|| { wp_db::run_and_restore(|mut _db| async move { @@ -52,6 +55,7 @@ async fn update_plugin(#[case] slug: PluginSlug, #[case] new_status: PluginStatu } #[tokio::test] +#[serial] async fn delete_plugin() { run_and_restore_wp_content_plugins(|| { wp_db::run_and_restore(|mut _db| async move { diff --git a/wp_api/tests/test_users_immut.rs b/wp_api/tests/test_users_immut.rs index 2c9abcc8d..15d3844d9 100644 --- a/wp_api/tests/test_users_immut.rs +++ b/wp_api/tests/test_users_immut.rs @@ -1,6 +1,7 @@ use reusable_test_cases::list_users_cases; use rstest::*; use rstest_reuse::{self, apply, template}; +use serial_test::parallel; use wp_api::{ generate, users::{ @@ -19,6 +20,7 @@ pub mod reusable_test_cases; #[apply(filter_fields_cases)] #[tokio::test] +#[parallel] async fn filter_users(#[case] fields: &[SparseUserField]) { request_builder() .users() @@ -31,6 +33,7 @@ async fn filter_users(#[case] fields: &[SparseUserField]) { #[apply(filter_fields_cases)] #[tokio::test] +#[parallel] async fn filter_retrieve_user(#[case] fields: &[SparseUserField]) { let user = request_builder() .users() @@ -42,6 +45,7 @@ async fn filter_retrieve_user(#[case] fields: &[SparseUserField]) { #[apply(filter_fields_cases)] #[tokio::test] +#[parallel] async fn filter_retrieve_current_user(#[case] fields: &[SparseUserField]) { let user = request_builder() .users() @@ -53,6 +57,7 @@ async fn filter_retrieve_current_user(#[case] fields: &[SparseUserField]) { #[apply(list_users_cases)] #[tokio::test] +#[parallel] async fn list_users_with_edit_context(#[case] params: UserListParams) { request_builder() .users() @@ -63,6 +68,7 @@ async fn list_users_with_edit_context(#[case] params: UserListParams) { #[apply(list_users_cases)] #[tokio::test] +#[parallel] async fn list_users_with_embed_context(#[case] params: UserListParams) { request_builder() .users() @@ -73,6 +79,7 @@ async fn list_users_with_embed_context(#[case] params: UserListParams) { #[apply(list_users_cases)] #[tokio::test] +#[parallel] async fn list_users_with_view_context(#[case] params: UserListParams) { request_builder() .users() @@ -84,6 +91,7 @@ async fn list_users_with_view_context(#[case] params: UserListParams) { #[apply(list_users_has_published_posts_cases)] #[trace] #[tokio::test] +#[parallel] async fn list_users_with_edit_context_has_published_posts( #[case] has_published_posts: Option, ) { @@ -100,6 +108,7 @@ async fn list_users_with_edit_context_has_published_posts( #[apply(list_users_has_published_posts_cases)] #[trace] #[tokio::test] +#[parallel] async fn list_users_with_embed_context_has_published_posts( #[case] has_published_posts: Option, ) { @@ -116,6 +125,7 @@ async fn list_users_with_embed_context_has_published_posts( #[apply(list_users_has_published_posts_cases)] #[trace] #[tokio::test] +#[parallel] async fn list_users_with_view_context_has_published_posts( #[case] has_published_posts: Option, ) { @@ -132,6 +142,7 @@ async fn list_users_with_view_context_has_published_posts( #[rstest] #[trace] #[tokio::test] +#[parallel] async fn retrieve_user_with_edit_context(#[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId) { let user = request_builder() .users() @@ -144,6 +155,7 @@ async fn retrieve_user_with_edit_context(#[values(FIRST_USER_ID, SECOND_USER_ID) #[rstest] #[trace] #[tokio::test] +#[parallel] async fn retrieve_user_with_embed_context( #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, ) { @@ -158,6 +170,7 @@ async fn retrieve_user_with_embed_context( #[rstest] #[trace] #[tokio::test] +#[parallel] async fn retrieve_user_with_view_context(#[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId) { let user = request_builder() .users() @@ -168,6 +181,7 @@ async fn retrieve_user_with_view_context(#[values(FIRST_USER_ID, SECOND_USER_ID) } #[tokio::test] +#[parallel] async fn retrieve_me_with_edit_context() { let user = request_builder() .users() @@ -179,6 +193,7 @@ async fn retrieve_me_with_edit_context() { } #[tokio::test] +#[parallel] async fn retrieve_me_with_embed_context() { let user = request_builder() .users() @@ -190,6 +205,7 @@ async fn retrieve_me_with_embed_context() { } #[tokio::test] +#[parallel] async fn retrieve_me_with_view_context() { let user = request_builder() .users() diff --git a/wp_api/tests/test_users_mut.rs b/wp_api/tests/test_users_mut.rs index 0e7916870..2144db86b 100644 --- a/wp_api/tests/test_users_mut.rs +++ b/wp_api/tests/test_users_mut.rs @@ -1,4 +1,5 @@ use integration_test_common::AssertResponse; +use serial_test::serial; use wp_api::users::{UserCreateParams, UserDeleteParams, UserUpdateParams}; use wp_db::{DbUser, DbUserMeta}; @@ -8,6 +9,7 @@ pub mod integration_test_common; pub mod wp_db; #[tokio::test] +#[serial] async fn create_user() { wp_db::run_and_restore(|mut db| async move { let username = "t_username"; @@ -35,6 +37,7 @@ async fn create_user() { } #[tokio::test] +#[serial] async fn delete_user() { wp_db::run_and_restore(|mut db| async move { // Delete the user using the API and ensure it's successful @@ -57,6 +60,7 @@ async fn delete_user() { } #[tokio::test] +#[serial] async fn delete_current_user() { wp_db::run_and_restore(|mut db| async move { // Delete the user using the API and ensure it's successful @@ -82,6 +86,7 @@ async fn delete_current_user() { } #[tokio::test] +#[serial] async fn update_user_name() { let new_name = "new_name"; let params = UserUpdateParams { @@ -95,6 +100,7 @@ async fn update_user_name() { } #[tokio::test] +#[serial] async fn update_user_first_name() { let new_first_name = "new_first_name"; let params = UserUpdateParams { @@ -108,6 +114,7 @@ async fn update_user_first_name() { } #[tokio::test] +#[serial] async fn update_user_last_name() { let new_last_name = "new_last_name"; let params = UserUpdateParams { @@ -121,6 +128,7 @@ async fn update_user_last_name() { } #[tokio::test] +#[serial] async fn update_user_email() { let new_email = "new_email@example.com"; let params = UserUpdateParams { @@ -134,6 +142,7 @@ async fn update_user_email() { } #[tokio::test] +#[serial] async fn update_user_url() { let new_url = "https://new_url"; let params = UserUpdateParams { @@ -147,6 +156,7 @@ async fn update_user_url() { } #[tokio::test] +#[serial] async fn update_user_description() { let new_description = "new_description"; let params = UserUpdateParams { @@ -160,6 +170,7 @@ async fn update_user_description() { } #[tokio::test] +#[serial] async fn update_user_nickname() { let new_nickname = "new_nickname"; let params = UserUpdateParams { @@ -173,6 +184,7 @@ async fn update_user_nickname() { } #[tokio::test] +#[serial] async fn update_user_slug() { let new_slug = "new_slug"; let params = UserUpdateParams { @@ -186,6 +198,7 @@ async fn update_user_slug() { } #[tokio::test] +#[serial] async fn update_user_roles() { wp_db::run_and_restore(|_| async move { let new_role = "author"; @@ -205,6 +218,7 @@ async fn update_user_roles() { } #[tokio::test] +#[serial] async fn update_user_password() { wp_db::run_and_restore(|_| async move { let new_password = "new_password"; From 5f3fd1692e02d1d5255d940ddb3df69b82add9ce Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 13:47:58 -0400 Subject: [PATCH 07/22] Implement update application password --- wp_api/src/application_passwords.rs | 9 +++ .../application_passwords_endpoint.rs | 62 ++++++++++++++++--- .../tests/test_application_passwords_mut.rs | 49 ++++++++++++++- wp_api/tests/test_login_immut.rs | 2 + 4 files changed, 113 insertions(+), 9 deletions(-) diff --git a/wp_api/src/application_passwords.rs b/wp_api/src/application_passwords.rs index f10b83bcd..efef38b37 100644 --- a/wp_api/src/application_passwords.rs +++ b/wp_api/src/application_passwords.rs @@ -94,3 +94,12 @@ pub struct ApplicationPasswordDeleteAllResponse { pub deleted: bool, pub count: i32, } + +#[derive(Debug, Serialize, uniffi::Record)] +pub struct ApplicationPasswordUpdateParams { + /// A UUID provided by the application to uniquely identify it. + /// It is recommended to use an UUID v5 with the URL or DNS namespace. + pub app_id: Option, + /// The name of the application password. + pub name: String, +} diff --git a/wp_api/src/request/endpoint/application_passwords_endpoint.rs b/wp_api/src/request/endpoint/application_passwords_endpoint.rs index 63fe7464a..6d09a0830 100644 --- a/wp_api/src/request/endpoint/application_passwords_endpoint.rs +++ b/wp_api/src/request/endpoint/application_passwords_endpoint.rs @@ -2,9 +2,9 @@ use wp_derive_request_builder::WpDerivedRequest; use crate::application_passwords::{ ApplicationPasswordCreateParams, ApplicationPasswordDeleteAllResponse, - ApplicationPasswordDeleteResponse, ApplicationPasswordUuid, ApplicationPasswordWithEditContext, - ApplicationPasswordWithEmbedContext, ApplicationPasswordWithViewContext, - SparseApplicationPassword, SparseApplicationPasswordField, + ApplicationPasswordDeleteResponse, ApplicationPasswordUpdateParams, ApplicationPasswordUuid, + ApplicationPasswordWithEditContext, ApplicationPasswordWithEmbedContext, + ApplicationPasswordWithViewContext, SparseApplicationPassword, SparseApplicationPasswordField, }; use crate::users::UserId; @@ -23,6 +23,8 @@ enum ApplicationPasswordsRequest { Retrieve, #[contextual_get(url = "/users//application-passwords/introspect", output = SparseApplicationPassword)] RetrieveCurrent, + #[post(url = "/users//application-passwords/", params = &ApplicationPasswordUpdateParams, output = ApplicationPasswordWithEditContext)] + Update, } #[cfg(test)] @@ -93,6 +95,21 @@ mod tests { ); } + #[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, + ); + } + #[rstest] fn retrieve_current_application_passwords_with_edit_context( endpoint: ApplicationPasswordsRequestEndpoint, @@ -103,6 +120,21 @@ mod tests { ); } + #[rstest] + #[case(WpContext::Edit, &[SparseApplicationPasswordField::Uuid], "/users/2/application-passwords/introspect?context=edit&_fields=uuid")] + #[case(WpContext::View, &[SparseApplicationPasswordField::Uuid, SparseApplicationPasswordField::Name], "/users/2/application-passwords/introspect?context=view&_fields=uuid%2Cname")] + fn filter_retrieve_current_application_passwords( + endpoint: ApplicationPasswordsRequestEndpoint, + #[case] context: WpContext, + #[case] fields: &[SparseApplicationPasswordField], + #[case] expected_path: &str, + ) { + validate_endpoint( + endpoint.filter_retrieve_current(&UserId(2), context, fields), + expected_path, + ); + } + #[rstest] fn retrieve_application_passwords_with_embed_context( endpoint: ApplicationPasswordsRequestEndpoint, @@ -119,20 +151,36 @@ mod tests { } #[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( + #[case(WpContext::Edit, &[SparseApplicationPasswordField::Uuid], "/users/2/application-passwords/584a87d5-4f18-4c33-a315-4c05ed1fc485?context=edit&_fields=uuid")] + #[case(WpContext::View, &[SparseApplicationPasswordField::Uuid, SparseApplicationPasswordField::Name], "/users/2/application-passwords/584a87d5-4f18-4c33-a315-4c05ed1fc485?context=view&_fields=uuid%2Cname")] + fn filter_retrieve_application_passwords( endpoint: ApplicationPasswordsRequestEndpoint, #[case] context: WpContext, #[case] fields: &[SparseApplicationPasswordField], #[case] expected_path: &str, ) { + let uuid = ApplicationPasswordUuid { + uuid: "584a87d5-4f18-4c33-a315-4c05ed1fc485".to_string(), + }; validate_endpoint( - endpoint.filter_list(&UserId(2), context, fields), + endpoint.filter_retrieve(&UserId(2), &uuid, context, fields), expected_path, ); } + #[rstest] + fn update_application_password(endpoint: ApplicationPasswordsRequestEndpoint) { + validate_endpoint( + endpoint.update( + &UserId(2), + &ApplicationPasswordUuid { + uuid: "584a87d5-4f18-4c33-a315-4c05ed1fc485".to_string(), + }, + ), + "/users/2/application-passwords/584a87d5-4f18-4c33-a315-4c05ed1fc485", + ); + } + #[fixture] fn endpoint(fixture_api_base_url: Arc) -> ApplicationPasswordsRequestEndpoint { ApplicationPasswordsRequestEndpoint::new(fixture_api_base_url) diff --git a/wp_api/tests/test_application_passwords_mut.rs b/wp_api/tests/test_application_passwords_mut.rs index 0df949c8b..a969f975c 100644 --- a/wp_api/tests/test_application_passwords_mut.rs +++ b/wp_api/tests/test_application_passwords_mut.rs @@ -1,6 +1,9 @@ +use integration_test_common::TEST_CREDENTIALS_ADMIN_PASSWORD_UUID; use serial_test::serial; use wp_api::{ - application_passwords::{ApplicationPasswordCreateParams, ApplicationPasswordUuid}, + application_passwords::{ + ApplicationPasswordCreateParams, ApplicationPasswordUpdateParams, ApplicationPasswordUuid, + }, users::UserId, }; use wp_db::DbUserMeta; @@ -18,7 +21,7 @@ pub mod wp_db; async fn create_application_password() { wp_db::run_and_restore(|mut db| async move { let password_name = "IntegrationTest"; - // Assert that the application password is not in DB + // Assert that the application password name is not in DB assert!( !db_application_password_meta_for_user(&mut db, &FIRST_USER_ID) .await @@ -49,6 +52,48 @@ async fn create_application_password() { .await; } +#[tokio::test] +#[serial] +async fn update_application_password() { + wp_db::run_and_restore(|mut db| async move { + let password_name = "IntegrationTest"; + // Assert that the application password name is not in DB + assert!( + !db_application_password_meta_for_user(&mut db, &FIRST_USER_ID) + .await + .unwrap() + .meta_value + .contains(password_name) + ); + + // Update the application password to use the new name using the API + let params = ApplicationPasswordUpdateParams { + app_id: None, + name: password_name.to_string(), + }; + let created_application_password = request_builder() + .application_passwords() + .update( + &FIRST_USER_ID, + &ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string(), + }, + ¶ms, + ) + .await + .assert_response(); + + // Assert that the application password is in DB + let db_user_meta_after_update = + db_application_password_meta_for_user(&mut db, &FIRST_USER_ID).await; + assert!(db_user_meta_after_update.is_some()); + let meta_value = db_user_meta_after_update.unwrap().meta_value; + assert!(meta_value.contains(password_name)); + assert!(meta_value.contains(&created_application_password.uuid.uuid)); + }) + .await; +} + #[tokio::test] #[serial] async fn delete_single_application_password() { diff --git a/wp_api/tests/test_login_immut.rs b/wp_api/tests/test_login_immut.rs index f4184fe13..d2ed5155c 100644 --- a/wp_api/tests/test_login_immut.rs +++ b/wp_api/tests/test_login_immut.rs @@ -1,5 +1,6 @@ use integration_test_common::{AssertResponse, AsyncWpNetworking}; use rstest::rstest; +use serial_test::serial; use std::sync::Arc; use wp_api::login::WpLoginClient; @@ -26,6 +27,7 @@ const ORCHESTREMETROPOLITAIN_AUTH_URL: &str = ORCHESTREMETROPOLITAIN_AUTH_URL )] #[tokio::test] +#[serial] async fn test_login_flow(#[case] site_url: &str, #[case] expected_auth_url: &str) { let client = WpLoginClient::new(Arc::new(AsyncWpNetworking::default())); let url_discovery = client From c7c4e8d87ac6bcb14917cd11bdd810db98bf4e4c Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 13:57:47 -0400 Subject: [PATCH 08/22] Add filter retrieve integration tests for application passwords --- .../tests/test_application_passwords_immut.rs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/wp_api/tests/test_application_passwords_immut.rs b/wp_api/tests/test_application_passwords_immut.rs index 8afa9e62f..1eb466f31 100644 --- a/wp_api/tests/test_application_passwords_immut.rs +++ b/wp_api/tests/test_application_passwords_immut.rs @@ -19,7 +19,7 @@ pub mod reusable_test_cases; #[apply(filter_fields_cases)] #[tokio::test] #[parallel] -async fn filter_application_passwords( +async fn filter_list_application_passwords( #[values(FIRST_USER_ID, SECOND_USER_ID)] user_id: UserId, #[case] fields: &[SparseApplicationPasswordField], ) { @@ -32,6 +32,39 @@ async fn filter_application_passwords( .for_each(|p| validate_sparse_application_password_fields(p, fields)); } +#[apply(filter_fields_cases)] +#[tokio::test] +#[parallel] +async fn filter_retrieve_application_password(#[case] fields: &[SparseApplicationPasswordField]) { + let p = request_builder() + .application_passwords() + .filter_retrieve( + &FIRST_USER_ID, + &ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string(), + }, + WpContext::Edit, + fields, + ) + .await + .assert_response(); + validate_sparse_application_password_fields(&p, fields); +} + +#[apply(filter_fields_cases)] +#[tokio::test] +#[parallel] +async fn filter_retrieve_current_application_password( + #[case] fields: &[SparseApplicationPasswordField], +) { + let p = request_builder() + .application_passwords() + .filter_retrieve_current(&FIRST_USER_ID, WpContext::Edit, fields) + .await + .assert_response(); + validate_sparse_application_password_fields(&p, fields); +} + #[rstest] #[tokio::test] #[parallel] From a522c1005f6719566924d50a5cc49e975a229ccb Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 14:18:18 -0400 Subject: [PATCH 09/22] Add cannot list & cannot read application password integration tests --- wp_api/src/api_error.rs | 4 ++ wp_api/tests/integration_test_common.rs | 2 + .../tests/test_application_passwords_err.rs | 48 +++++++++++++++++++ .../tests/test_application_passwords_mut.rs | 6 +-- 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 wp_api/tests/test_application_passwords_err.rs diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index 2013c9f22..11697df7d 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -73,8 +73,12 @@ pub enum WpRestErrorCode { CannotEditRoles, #[serde(rename = "rest_cannot_install_plugin")] CannotInstallPlugin, + #[serde(rename = "rest_cannot_list_application_passwords")] + CannotListApplicationPasswords, #[serde(rename = "rest_cannot_manage_plugins")] CannotManagePlugins, + #[serde(rename = "rest_cannot_read_application_password")] + CannotReadApplicationPassword, #[serde(rename = "rest_cannot_view_plugin")] CannotViewPlugin, #[serde(rename = "rest_cannot_view_plugins")] diff --git a/wp_api/tests/integration_test_common.rs b/wp_api/tests/integration_test_common.rs index 94236b281..c5cef1750 100644 --- a/wp_api/tests/integration_test_common.rs +++ b/wp_api/tests/integration_test_common.rs @@ -101,8 +101,10 @@ fn expected_status_code_for_wp_rest_error_code(error_code: &WpRestErrorCode) -> WpRestErrorCode::CannotEdit => 403, WpRestErrorCode::CannotEditRoles => 403, WpRestErrorCode::CannotInstallPlugin => 403, + WpRestErrorCode::CannotListApplicationPasswords => 403, WpRestErrorCode::CannotManageNetworkPlugins => 403, WpRestErrorCode::CannotManagePlugins => 403, + WpRestErrorCode::CannotReadApplicationPassword => 403, WpRestErrorCode::CannotViewPlugin => 403, WpRestErrorCode::CannotViewPlugins => 403, WpRestErrorCode::ForbiddenContext => 403, diff --git a/wp_api/tests/test_application_passwords_err.rs b/wp_api/tests/test_application_passwords_err.rs new file mode 100644 index 000000000..b8542fec1 --- /dev/null +++ b/wp_api/tests/test_application_passwords_err.rs @@ -0,0 +1,48 @@ +// TODO +#![allow(unused)] +use integration_test_common::request_builder_as_subscriber; +use rstest::*; +use rstest_reuse::{self, apply, template}; +use serial_test::parallel; +use wp_api::application_passwords::{ + ApplicationPasswordUuid, SparseApplicationPassword, SparseApplicationPasswordField, +}; +use wp_api::users::UserId; +use wp_api::{WpContext, WpRestErrorCode}; + +use crate::integration_test_common::{ + request_builder, AssertWpError, FIRST_USER_ID, SECOND_USER_ID, + TEST_CREDENTIALS_ADMIN_PASSWORD_UUID, TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID, +}; + +pub mod integration_test_common; +pub mod reusable_test_cases; + +#[rstest] +#[tokio::test] +#[parallel] +async fn list_application_passwords_err_cannot_list_application_passwords() { + // Second user (subscriber) doesn't have access to the first users' application passwords + request_builder_as_subscriber() + .application_passwords() + .list_with_edit_context(&FIRST_USER_ID) + .await + .assert_wp_error(WpRestErrorCode::CannotListApplicationPasswords); +} + +#[rstest] +#[tokio::test] +#[parallel] +async fn retrieve_application_password_err_cannot_read_application_password() { + // Second user (subscriber) doesn't have access to the first users' application passwords + request_builder_as_subscriber() + .application_passwords() + .retrieve_with_edit_context( + &FIRST_USER_ID, + &ApplicationPasswordUuid { + uuid: FIRST_USER_ID.to_string(), + }, + ) + .await + .assert_wp_error(WpRestErrorCode::CannotReadApplicationPassword); +} diff --git a/wp_api/tests/test_application_passwords_mut.rs b/wp_api/tests/test_application_passwords_mut.rs index a969f975c..4212875bc 100644 --- a/wp_api/tests/test_application_passwords_mut.rs +++ b/wp_api/tests/test_application_passwords_mut.rs @@ -23,7 +23,7 @@ async fn create_application_password() { let password_name = "IntegrationTest"; // Assert that the application password name is not in DB assert!( - !db_application_password_meta_for_user(&mut db, &FIRST_USER_ID) + !db_application_password_meta_for_user(&mut db, &SECOND_USER_ID) .await .unwrap() .meta_value @@ -37,13 +37,13 @@ async fn create_application_password() { }; let created_application_password = request_builder() .application_passwords() - .create(&FIRST_USER_ID, ¶ms) + .create(&SECOND_USER_ID, ¶ms) .await .assert_response(); // Assert that the application password is in DB let db_user_meta_after_update = - db_application_password_meta_for_user(&mut db, &FIRST_USER_ID).await; + db_application_password_meta_for_user(&mut db, &SECOND_USER_ID).await; assert!(db_user_meta_after_update.is_some()); let meta_value = db_user_meta_after_update.unwrap().meta_value; assert!(meta_value.contains(password_name)); From 16c5aa5de1e2d9a56112b96c6c8c330418649a6d Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 15:19:30 -0400 Subject: [PATCH 10/22] Implement create_application_password_err_cannot_create_application_passwords --- wp_api/src/api_error.rs | 2 ++ wp_api/tests/integration_test_common.rs | 1 + .../tests/test_application_passwords_err.rs | 25 ++++++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index 11697df7d..1fda560dd 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -63,6 +63,8 @@ pub struct UnrecognizedWpRestError { #[derive(Debug, Deserialize, PartialEq, Eq, uniffi::Error)] pub enum WpRestErrorCode { + #[serde(rename = "rest_cannot_create_application_passwords")] + CannotCreateApplicationPasswords, #[serde(rename = "rest_cannot_create_user")] CannotCreateUser, #[serde(rename = "rest_cannot_delete_active_plugin")] diff --git a/wp_api/tests/integration_test_common.rs b/wp_api/tests/integration_test_common.rs index c5cef1750..abd354933 100644 --- a/wp_api/tests/integration_test_common.rs +++ b/wp_api/tests/integration_test_common.rs @@ -94,6 +94,7 @@ impl AssertWpError for Result { fn expected_status_code_for_wp_rest_error_code(error_code: &WpRestErrorCode) -> u16 { match error_code { + WpRestErrorCode::CannotCreateApplicationPasswords => 403, WpRestErrorCode::CannotActivatePlugin => 403, WpRestErrorCode::CannotCreateUser => 403, WpRestErrorCode::CannotDeactivatePlugin => 403, diff --git a/wp_api/tests/test_application_passwords_err.rs b/wp_api/tests/test_application_passwords_err.rs index b8542fec1..9050fdff6 100644 --- a/wp_api/tests/test_application_passwords_err.rs +++ b/wp_api/tests/test_application_passwords_err.rs @@ -1,11 +1,12 @@ // TODO #![allow(unused)] -use integration_test_common::request_builder_as_subscriber; +use integration_test_common::{request_builder_as_subscriber, run_wp_cli_command}; use rstest::*; use rstest_reuse::{self, apply, template}; -use serial_test::parallel; +use serial_test::{parallel, serial}; use wp_api::application_passwords::{ - ApplicationPasswordUuid, SparseApplicationPassword, SparseApplicationPasswordField, + ApplicationPasswordCreateParams, ApplicationPasswordUuid, SparseApplicationPassword, + SparseApplicationPasswordField, }; use wp_api::users::UserId; use wp_api::{WpContext, WpRestErrorCode}; @@ -17,6 +18,7 @@ use crate::integration_test_common::{ pub mod integration_test_common; pub mod reusable_test_cases; +pub mod wp_db; #[rstest] #[tokio::test] @@ -46,3 +48,20 @@ async fn retrieve_application_password_err_cannot_read_application_password() { .await .assert_wp_error(WpRestErrorCode::CannotReadApplicationPassword); } + +#[rstest] +#[tokio::test] +#[serial] +async fn create_application_password_err_cannot_create_application_passwords() { + request_builder_as_subscriber() + .application_passwords() + .create( + &FIRST_USER_ID, + &ApplicationPasswordCreateParams { + app_id: None, + name: "foo".to_string(), + }, + ) + .await + .assert_wp_error(WpRestErrorCode::CannotCreateApplicationPasswords); +} From dc631f93521361858d05281d068aac83da63d667 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 15:19:55 -0400 Subject: [PATCH 11/22] Add a way to run wp-cli commands from Rust --- Makefile | 3 +++ wp_api/tests/integration_test_common.rs | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/Makefile b/Makefile index ef98dc025..4ec16d210 100644 --- a/Makefile +++ b/Makefile @@ -225,3 +225,6 @@ setup-rust-android-targets: i686-linux-android \ armv7-linux-androideabi \ aarch64-linux-android + +run-wp-cli-command: + docker exec -it wordpress /bin/bash -c "wp --allow-root $(ARGS)" diff --git a/wp_api/tests/integration_test_common.rs b/wp_api/tests/integration_test_common.rs index abd354933..e7bb6acb3 100644 --- a/wp_api/tests/integration_test_common.rs +++ b/wp_api/tests/integration_test_common.rs @@ -223,3 +223,13 @@ impl AssertResponse for Result { self.unwrap() } } + +pub fn run_wp_cli_command(args: impl AsRef) -> std::process::ExitStatus { + Command::new("make") + .arg("-C") + .arg("../") + .arg("run-wp-cli-command") + .arg(format!("ARGS={}", args.as_ref())) + .status() + .expect("Failed to run wp-cli command") +} From 5b8acbda88102de2c8542e6c1e05d19f87ab431d Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 15:35:23 -0400 Subject: [PATCH 12/22] Delete application password integration tests --- wp_api/src/api_error.rs | 6 ++ wp_api/tests/integration_test_common.rs | 3 + .../tests/test_application_passwords_err.rs | 57 ++++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index 1fda560dd..7d647a5dc 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -69,8 +69,14 @@ pub enum WpRestErrorCode { CannotCreateUser, #[serde(rename = "rest_cannot_delete_active_plugin")] CannotDeleteActivePlugin, + #[serde(rename = "rest_cannot_delete_application_password")] + CannotDeleteApplicationPassword, + #[serde(rename = "rest_cannot_delete_application_passwords")] + CannotDeleteApplicationPasswords, #[serde(rename = "rest_cannot_edit")] CannotEdit, + #[serde(rename = "rest_cannot_edit_application_password")] + CannotEditApplicationPassword, #[serde(rename = "rest_cannot_edit_roles")] CannotEditRoles, #[serde(rename = "rest_cannot_install_plugin")] diff --git a/wp_api/tests/integration_test_common.rs b/wp_api/tests/integration_test_common.rs index e7bb6acb3..c1d7926d2 100644 --- a/wp_api/tests/integration_test_common.rs +++ b/wp_api/tests/integration_test_common.rs @@ -99,7 +99,10 @@ fn expected_status_code_for_wp_rest_error_code(error_code: &WpRestErrorCode) -> WpRestErrorCode::CannotCreateUser => 403, WpRestErrorCode::CannotDeactivatePlugin => 403, WpRestErrorCode::CannotDeleteActivePlugin => 400, + WpRestErrorCode::CannotDeleteApplicationPassword => 403, + WpRestErrorCode::CannotDeleteApplicationPasswords => 403, WpRestErrorCode::CannotEdit => 403, + WpRestErrorCode::CannotEditApplicationPassword => 403, WpRestErrorCode::CannotEditRoles => 403, WpRestErrorCode::CannotInstallPlugin => 403, WpRestErrorCode::CannotListApplicationPasswords => 403, diff --git a/wp_api/tests/test_application_passwords_err.rs b/wp_api/tests/test_application_passwords_err.rs index 9050fdff6..06b5affb5 100644 --- a/wp_api/tests/test_application_passwords_err.rs +++ b/wp_api/tests/test_application_passwords_err.rs @@ -5,8 +5,8 @@ use rstest::*; use rstest_reuse::{self, apply, template}; use serial_test::{parallel, serial}; use wp_api::application_passwords::{ - ApplicationPasswordCreateParams, ApplicationPasswordUuid, SparseApplicationPassword, - SparseApplicationPasswordField, + ApplicationPasswordCreateParams, ApplicationPasswordUpdateParams, ApplicationPasswordUuid, + SparseApplicationPassword, SparseApplicationPasswordField, }; use wp_api::users::UserId; use wp_api::{WpContext, WpRestErrorCode}; @@ -51,8 +51,9 @@ async fn retrieve_application_password_err_cannot_read_application_password() { #[rstest] #[tokio::test] -#[serial] +#[parallel] async fn create_application_password_err_cannot_create_application_passwords() { + // Second user (subscriber) can not create an application password for the first user request_builder_as_subscriber() .application_passwords() .create( @@ -65,3 +66,53 @@ async fn create_application_password_err_cannot_create_application_passwords() { .await .assert_wp_error(WpRestErrorCode::CannotCreateApplicationPasswords); } + +#[rstest] +#[tokio::test] +#[parallel] +async fn update_application_password_err_cannot_edit_application_password() { + // Second user (subscriber) can not update an application password of the first user + request_builder_as_subscriber() + .application_passwords() + .update( + &FIRST_USER_ID, + &ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string(), + }, + &ApplicationPasswordUpdateParams { + app_id: None, + name: "foo".to_string(), + }, + ) + .await + .assert_wp_error(WpRestErrorCode::CannotEditApplicationPassword); +} + +#[rstest] +#[tokio::test] +#[parallel] +async fn delete_application_password_err_cannot_delete_application_password() { + // Second user (subscriber) can not delete an application password of the first user + request_builder_as_subscriber() + .application_passwords() + .delete( + &FIRST_USER_ID, + &ApplicationPasswordUuid { + uuid: TEST_CREDENTIALS_ADMIN_PASSWORD_UUID.to_string(), + }, + ) + .await + .assert_wp_error(WpRestErrorCode::CannotDeleteApplicationPassword); +} + +#[rstest] +#[tokio::test] +#[parallel] +async fn delete_application_passwords_err_cannot_delete_application_passwords() { + // Second user (subscriber) can not delete all application passwords of the first user + request_builder_as_subscriber() + .application_passwords() + .delete_all(&FIRST_USER_ID) + .await + .assert_wp_error(WpRestErrorCode::CannotDeleteApplicationPasswords); +} From 9de5c47b5230b33c29262d7b97779d17983aa87c Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 15:56:14 -0400 Subject: [PATCH 13/22] Retrieve application password cannot instrospect error tests --- wp_api/src/api_error.rs | 6 ++ wp_api/tests/integration_test_common.rs | 102 ++++++++++-------- .../tests/test_application_passwords_err.rs | 38 +++++-- wp_api/tests/test_users_err.rs | 25 ++--- 4 files changed, 102 insertions(+), 69 deletions(-) diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index 7d647a5dc..17a7869d8 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -63,6 +63,8 @@ pub struct UnrecognizedWpRestError { #[derive(Debug, Deserialize, PartialEq, Eq, uniffi::Error)] pub enum WpRestErrorCode { + #[serde(rename = "rest_application_password_not_found")] + ApplicationPasswordNotFound, #[serde(rename = "rest_cannot_create_application_passwords")] CannotCreateApplicationPasswords, #[serde(rename = "rest_cannot_create_user")] @@ -79,6 +81,8 @@ pub enum WpRestErrorCode { CannotEditApplicationPassword, #[serde(rename = "rest_cannot_edit_roles")] CannotEditRoles, + #[serde(rename = "rest_cannot_introspect_app_password_for_non_authenticated_user")] + CannotIntrospectAppPasswordForNonAuthenticatedUser, #[serde(rename = "rest_cannot_install_plugin")] CannotInstallPlugin, #[serde(rename = "rest_cannot_list_application_passwords")] @@ -139,6 +143,8 @@ pub enum WpRestErrorCode { /// resulting in `CannotManagePlugins` error instead. #[serde(rename = "rest_cannot_deactivate_plugin")] CannotDeactivatePlugin, + #[serde(rename = "rest_no_authenticated_app_password")] + NoAuthenticatedAppPassword, // If `force=true` is missing from delete user request. #[serde(rename = "rest_trash_not_supported")] TrashNotSupported, diff --git a/wp_api/tests/integration_test_common.rs b/wp_api/tests/integration_test_common.rs index c1d7926d2..71c8f97aa 100644 --- a/wp_api/tests/integration_test_common.rs +++ b/wp_api/tests/integration_test_common.rs @@ -47,14 +47,23 @@ pub fn request_builder_as_subscriber() -> WpRequestBuilder { .expect("Site url is generated by our tooling") } +pub fn request_builder_as_unauthenticated() -> WpRequestBuilder { + WpRequestBuilder::new( + TEST_CREDENTIALS_SITE_URL.to_string(), + WpAuthentication::None, + Arc::new(AsyncWpNetworking::default()), + ) + .expect("Site url is generated by our tooling") +} + pub trait AssertWpError { fn assert_wp_error(self, expected_error_code: WpRestErrorCode); } impl AssertWpError for Result { fn assert_wp_error(self, expected_error_code: WpRestErrorCode) { - let expected_status_code = - expected_status_code_for_wp_rest_error_code(&expected_error_code); + let expected_status_codes = + expected_status_codes_for_wp_rest_error_code(&expected_error_code); let err = self.unwrap_err(); if let WpApiError::RestError { rest_error: @@ -71,10 +80,12 @@ impl AssertWpError for Result { "Incorrect error code. Expected '{:?}', found '{:?}'. Response was: '{:?}'", expected_error_code, error_code, response ); - assert_eq!( - expected_status_code, status_code, - "Incorrect status code. Expected '{:?}', found '{:?}'. Response was: '{:?}'", - expected_status_code, status_code, response + assert!( + expected_status_codes.contains(&status_code), + "Incorrect status code. Expected one of '{:?}', found '{:?}'. Response was: '{:?}'", + expected_status_codes, + status_code, + response ); } else if let WpApiError::RestError { rest_error: WpRestErrorWrapper::Unrecognized(unrecognized_error), @@ -92,45 +103,48 @@ impl AssertWpError for Result { } } -fn expected_status_code_for_wp_rest_error_code(error_code: &WpRestErrorCode) -> u16 { +fn expected_status_codes_for_wp_rest_error_code(error_code: &WpRestErrorCode) -> &[u16] { match error_code { - WpRestErrorCode::CannotCreateApplicationPasswords => 403, - WpRestErrorCode::CannotActivatePlugin => 403, - WpRestErrorCode::CannotCreateUser => 403, - WpRestErrorCode::CannotDeactivatePlugin => 403, - WpRestErrorCode::CannotDeleteActivePlugin => 400, - WpRestErrorCode::CannotDeleteApplicationPassword => 403, - WpRestErrorCode::CannotDeleteApplicationPasswords => 403, - WpRestErrorCode::CannotEdit => 403, - WpRestErrorCode::CannotEditApplicationPassword => 403, - WpRestErrorCode::CannotEditRoles => 403, - WpRestErrorCode::CannotInstallPlugin => 403, - WpRestErrorCode::CannotListApplicationPasswords => 403, - WpRestErrorCode::CannotManageNetworkPlugins => 403, - WpRestErrorCode::CannotManagePlugins => 403, - WpRestErrorCode::CannotReadApplicationPassword => 403, - WpRestErrorCode::CannotViewPlugin => 403, - WpRestErrorCode::CannotViewPlugins => 403, - WpRestErrorCode::ForbiddenContext => 403, - WpRestErrorCode::ForbiddenOrderBy => 403, - WpRestErrorCode::ForbiddenWho => 403, - WpRestErrorCode::NetworkOnlyPlugin => 400, - WpRestErrorCode::PluginNotFound => 404, - WpRestErrorCode::InvalidParam => 400, - WpRestErrorCode::TrashNotSupported => 501, - WpRestErrorCode::Unauthorized => 401, - WpRestErrorCode::UserCannotDelete => 403, - WpRestErrorCode::UserCannotView => 403, - WpRestErrorCode::UserCreate => 500, - WpRestErrorCode::UserExists => 400, - WpRestErrorCode::UserInvalidArgument => 400, - WpRestErrorCode::UserInvalidEmail => 400, - WpRestErrorCode::UserInvalidId => 404, - WpRestErrorCode::UserInvalidPassword => 400, - WpRestErrorCode::UserInvalidReassign => 400, - WpRestErrorCode::UserInvalidRole => 400, - WpRestErrorCode::UserInvalidSlug => 400, - WpRestErrorCode::UserInvalidUsername => 400, + WpRestErrorCode::ApplicationPasswordNotFound => &[403], + WpRestErrorCode::CannotCreateApplicationPasswords => &[403], + WpRestErrorCode::CannotActivatePlugin => &[403], + WpRestErrorCode::CannotCreateUser => &[403], + WpRestErrorCode::CannotDeactivatePlugin => &[403], + WpRestErrorCode::CannotDeleteActivePlugin => &[400], + WpRestErrorCode::CannotDeleteApplicationPassword => &[403], + WpRestErrorCode::CannotDeleteApplicationPasswords => &[403], + WpRestErrorCode::CannotEdit => &[403], + WpRestErrorCode::CannotEditApplicationPassword => &[403], + WpRestErrorCode::CannotEditRoles => &[403], + WpRestErrorCode::CannotIntrospectAppPasswordForNonAuthenticatedUser => &[401, 403], + WpRestErrorCode::CannotInstallPlugin => &[403], + WpRestErrorCode::CannotListApplicationPasswords => &[403], + WpRestErrorCode::CannotManageNetworkPlugins => &[403], + WpRestErrorCode::CannotManagePlugins => &[403], + WpRestErrorCode::CannotReadApplicationPassword => &[403], + WpRestErrorCode::CannotViewPlugin => &[403], + WpRestErrorCode::CannotViewPlugins => &[403], + WpRestErrorCode::ForbiddenContext => &[403], + WpRestErrorCode::ForbiddenOrderBy => &[403], + WpRestErrorCode::ForbiddenWho => &[403], + WpRestErrorCode::NetworkOnlyPlugin => &[400], + WpRestErrorCode::NoAuthenticatedAppPassword => &[401], + WpRestErrorCode::PluginNotFound => &[404], + WpRestErrorCode::InvalidParam => &[400], + WpRestErrorCode::TrashNotSupported => &[501], + WpRestErrorCode::Unauthorized => &[401], + WpRestErrorCode::UserCannotDelete => &[403], + WpRestErrorCode::UserCannotView => &[403], + WpRestErrorCode::UserCreate => &[500], + WpRestErrorCode::UserExists => &[400], + WpRestErrorCode::UserInvalidArgument => &[400], + WpRestErrorCode::UserInvalidEmail => &[400], + WpRestErrorCode::UserInvalidId => &[404], + WpRestErrorCode::UserInvalidPassword => &[400], + WpRestErrorCode::UserInvalidReassign => &[400], + WpRestErrorCode::UserInvalidRole => &[400], + WpRestErrorCode::UserInvalidSlug => &[400], + WpRestErrorCode::UserInvalidUsername => &[400], } } diff --git a/wp_api/tests/test_application_passwords_err.rs b/wp_api/tests/test_application_passwords_err.rs index 06b5affb5..b3e0520e6 100644 --- a/wp_api/tests/test_application_passwords_err.rs +++ b/wp_api/tests/test_application_passwords_err.rs @@ -1,19 +1,14 @@ -// TODO -#![allow(unused)] -use integration_test_common::{request_builder_as_subscriber, run_wp_cli_command}; +use integration_test_common::{request_builder_as_subscriber, request_builder_as_unauthenticated}; use rstest::*; -use rstest_reuse::{self, apply, template}; -use serial_test::{parallel, serial}; +use serial_test::parallel; use wp_api::application_passwords::{ ApplicationPasswordCreateParams, ApplicationPasswordUpdateParams, ApplicationPasswordUuid, - SparseApplicationPassword, SparseApplicationPasswordField, }; -use wp_api::users::UserId; -use wp_api::{WpContext, WpRestErrorCode}; +use wp_api::WpRestErrorCode; use crate::integration_test_common::{ request_builder, AssertWpError, FIRST_USER_ID, SECOND_USER_ID, - TEST_CREDENTIALS_ADMIN_PASSWORD_UUID, TEST_CREDENTIALS_SUBSCRIBER_PASSWORD_UUID, + TEST_CREDENTIALS_ADMIN_PASSWORD_UUID, }; pub mod integration_test_common; @@ -116,3 +111,28 @@ async fn delete_application_passwords_err_cannot_delete_application_passwords() .await .assert_wp_error(WpRestErrorCode::CannotDeleteApplicationPasswords); } + +#[rstest] +#[tokio::test] +#[parallel] +async fn retrieve_application_password_err_cannot_introspect_app_password_for_non_authenticated_user_401( +) { + // Unauthenticated user can not retrieve the current application password for the second user + request_builder_as_unauthenticated() + .application_passwords() + .retrieve_current_with_edit_context(&SECOND_USER_ID) + .await + .assert_wp_error(WpRestErrorCode::CannotIntrospectAppPasswordForNonAuthenticatedUser); +} + +#[rstest] +#[tokio::test] +#[parallel] +async fn retrieve_application_password_err_cannot_introspect_app_password_for_another_user_403() { + // First user can not retrieve the current application password for the second user + request_builder() + .application_passwords() + .retrieve_current_with_edit_context(&SECOND_USER_ID) + .await + .assert_wp_error(WpRestErrorCode::CannotIntrospectAppPasswordForNonAuthenticatedUser); +} diff --git a/wp_api/tests/test_users_err.rs b/wp_api/tests/test_users_err.rs index 94e9b67ce..e0f5eee5f 100644 --- a/wp_api/tests/test_users_err.rs +++ b/wp_api/tests/test_users_err.rs @@ -1,17 +1,15 @@ -use std::sync::Arc; - -use integration_test_common::{AsyncWpNetworking, SECOND_USER_EMAIL, TEST_CREDENTIALS_SITE_URL}; +use integration_test_common::SECOND_USER_EMAIL; use wp_api::{ users::{ UserCreateParams, UserDeleteParams, UserId, UserListParams, UserUpdateParams, WpApiParamUsersHasPublishedPosts, WpApiParamUsersOrderBy, WpApiParamUsersWho, }, - WpAuthentication, WpRestErrorCode, + WpRestErrorCode, }; use crate::integration_test_common::{ - request_builder, request_builder_as_subscriber, AssertWpError, FIRST_USER_ID, SECOND_USER_ID, - SECOND_USER_SLUG, + request_builder, request_builder_as_subscriber, request_builder_as_unauthenticated, + AssertWpError, FIRST_USER_ID, SECOND_USER_ID, SECOND_USER_SLUG, }; pub mod integration_test_common; @@ -163,16 +161,11 @@ async fn retrieve_user_err_user_invalid_id() { #[tokio::test] async fn retrieve_user_err_unauthorized() { - wp_api::WpRequestBuilder::new( - TEST_CREDENTIALS_SITE_URL.to_string(), - WpAuthentication::None, - Arc::new(AsyncWpNetworking::default()), - ) - .expect("Site url is generated by our tooling") - .users() - .retrieve_me_with_edit_context() - .await - .assert_wp_error(WpRestErrorCode::Unauthorized); + request_builder_as_unauthenticated() + .users() + .retrieve_me_with_edit_context() + .await + .assert_wp_error(WpRestErrorCode::Unauthorized); } #[tokio::test] From 115655dc84986b181c11719dba332b1f3889eef6 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 16:48:28 -0400 Subject: [PATCH 14/22] Add integration test for ApplicationPasswordNotFound --- wp_api/src/api_error.rs | 13 +++++++++++-- wp_api/tests/integration_test_common.rs | 5 ++++- wp_api/tests/test_application_passwords_err.rs | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/wp_api/src/api_error.rs b/wp_api/src/api_error.rs index 17a7869d8..36f9966c8 100644 --- a/wp_api/src/api_error.rs +++ b/wp_api/src/api_error.rs @@ -122,6 +122,17 @@ pub enum WpRestErrorCode { #[serde(rename = "rest_user_invalid_slug")] UserInvalidSlug, // --- + // Untested, because we are unable to create the necessary conditions for them + // --- + #[serde(rename = "application_passwords_disabled")] + ApplicationPasswordsDisabled, + #[serde(rename = "application_passwords_disabled_for_user")] + ApplicationPasswordsDisabledForUser, + #[serde(rename = "rest_cannot_manage_application_passwords")] + CannotManageApplicationPasswords, + #[serde(rename = "rest_no_authenticated_app_password")] + NoAuthenticatedAppPassword, + // --- // Untested, because we believe these errors require multisite // --- #[serde(rename = "rest_cannot_manage_network_plugins")] @@ -143,8 +154,6 @@ pub enum WpRestErrorCode { /// resulting in `CannotManagePlugins` error instead. #[serde(rename = "rest_cannot_deactivate_plugin")] CannotDeactivatePlugin, - #[serde(rename = "rest_no_authenticated_app_password")] - NoAuthenticatedAppPassword, // If `force=true` is missing from delete user request. #[serde(rename = "rest_trash_not_supported")] TrashNotSupported, diff --git a/wp_api/tests/integration_test_common.rs b/wp_api/tests/integration_test_common.rs index 71c8f97aa..ca7301d61 100644 --- a/wp_api/tests/integration_test_common.rs +++ b/wp_api/tests/integration_test_common.rs @@ -105,7 +105,9 @@ impl AssertWpError for Result { fn expected_status_codes_for_wp_rest_error_code(error_code: &WpRestErrorCode) -> &[u16] { match error_code { - WpRestErrorCode::ApplicationPasswordNotFound => &[403], + WpRestErrorCode::ApplicationPasswordsDisabled => &[501], + WpRestErrorCode::ApplicationPasswordsDisabledForUser => &[501], + WpRestErrorCode::ApplicationPasswordNotFound => &[404], WpRestErrorCode::CannotCreateApplicationPasswords => &[403], WpRestErrorCode::CannotActivatePlugin => &[403], WpRestErrorCode::CannotCreateUser => &[403], @@ -113,6 +115,7 @@ fn expected_status_codes_for_wp_rest_error_code(error_code: &WpRestErrorCode) -> WpRestErrorCode::CannotDeleteActivePlugin => &[400], WpRestErrorCode::CannotDeleteApplicationPassword => &[403], WpRestErrorCode::CannotDeleteApplicationPasswords => &[403], + WpRestErrorCode::CannotManageApplicationPasswords => &[401, 403], WpRestErrorCode::CannotEdit => &[403], WpRestErrorCode::CannotEditApplicationPassword => &[403], WpRestErrorCode::CannotEditRoles => &[403], diff --git a/wp_api/tests/test_application_passwords_err.rs b/wp_api/tests/test_application_passwords_err.rs index b3e0520e6..cc6245a6a 100644 --- a/wp_api/tests/test_application_passwords_err.rs +++ b/wp_api/tests/test_application_passwords_err.rs @@ -136,3 +136,19 @@ async fn retrieve_application_password_err_cannot_introspect_app_password_for_an .await .assert_wp_error(WpRestErrorCode::CannotIntrospectAppPasswordForNonAuthenticatedUser); } + +#[rstest] +#[tokio::test] +#[parallel] +async fn retrieve_application_password_err_application_password_not_found() { + request_builder() + .application_passwords() + .retrieve_with_edit_context( + &FIRST_USER_ID, + &ApplicationPasswordUuid { + uuid: "foo".to_string(), + }, + ) + .await + .assert_wp_error(WpRestErrorCode::ApplicationPasswordNotFound); +} From 49971672512d9c49df9372210fe3a4d53e24b516 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 17:00:50 -0400 Subject: [PATCH 15/22] Add ApplicationPasswordsEndpointTest in Kotlin wrapper --- .../ApplicationPasswordsEndpointTest.kt | 53 +++++++++++++++++++ .../integrationTest/kotlin/TestCredentials.kt | 10 ++-- 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 native/kotlin/api/kotlin/src/integrationTest/kotlin/ApplicationPasswordsEndpointTest.kt diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApplicationPasswordsEndpointTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApplicationPasswordsEndpointTest.kt new file mode 100644 index 000000000..e907e12b6 --- /dev/null +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApplicationPasswordsEndpointTest.kt @@ -0,0 +1,53 @@ +package rs.wordpress.api.kotlin + +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import uniffi.wp_api.ApplicationPasswordUuid +import uniffi.wp_api.wpAuthenticationFromUsernameAndPassword +import kotlin.test.assertEquals + +class ApplicationPasswordsEndpointTest { + private val testCredentials = TestCredentials.INSTANCE + private val siteUrl = testCredentials.siteUrl + private val authentication = wpAuthenticationFromUsernameAndPassword( + username = testCredentials.adminUsername, password = testCredentials.adminPassword + ) + private val client = WpApiClient(siteUrl, authentication) + + @Test + fun testApplicationPasswordListRequest() = runTest { + val result = client.request { requestBuilder -> + requestBuilder.applicationPasswords().listWithEditContext(FIRST_USER_ID) + } + assert(result is WpRequestSuccess) + val applicationPasswordList = (result as WpRequestSuccess).data + assertEquals( + ApplicationPasswordUuid(testCredentials.adminPasswordUuid), + applicationPasswordList.first().uuid + ) + } + + @Test + fun testApplicationPasswordRetrieveRequest() = runTest { + val uuid = ApplicationPasswordUuid(testCredentials.adminPasswordUuid) + val result = client.request { requestBuilder -> + requestBuilder.applicationPasswords().retrieveWithEditContext(FIRST_USER_ID, uuid) + } + assert(result is WpRequestSuccess) + val applicationPasswordList = (result as WpRequestSuccess).data + assertEquals(uuid, applicationPasswordList.uuid) + } + + @Test + fun testApplicationPasswordRetrieveCurrentRequest() = runTest { + val result = client.request { requestBuilder -> + requestBuilder.applicationPasswords().retrieveCurrentWithEditContext(FIRST_USER_ID) + } + assert(result is WpRequestSuccess) + val applicationPasswordList = (result as WpRequestSuccess).data + assertEquals( + ApplicationPasswordUuid(testCredentials.adminPasswordUuid), + applicationPasswordList.uuid + ) + } +} \ No newline at end of file diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/TestCredentials.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/TestCredentials.kt index 9d8dc2e3e..ae7fc84c0 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/TestCredentials.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/TestCredentials.kt @@ -6,8 +6,10 @@ data class TestCredentials( val siteUrl: String, val adminUsername: String, val adminPassword: String, + val adminPasswordUuid: String, val subscriberUsername: String, - val subscriberPassword: String + val subscriberPassword: String, + val subscriberPasswordUuid: String ) { companion object { val INSTANCE: TestCredentials by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { @@ -22,8 +24,10 @@ data class TestCredentials( siteUrl = lineList[0], adminUsername = lineList[1], adminPassword = lineList[2], - subscriberUsername = lineList[3], - subscriberPassword = lineList[4], + adminPasswordUuid = lineList[3], + subscriberUsername = lineList[4], + subscriberPassword = lineList[5], + subscriberPasswordUuid = lineList[6], ) } } From df46c50160923b338b3b29ab88a7117220d29cb5 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 17:11:47 -0400 Subject: [PATCH 16/22] Test password sparse field for application passwords --- wp_api/src/request/endpoint/application_passwords_endpoint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wp_api/src/request/endpoint/application_passwords_endpoint.rs b/wp_api/src/request/endpoint/application_passwords_endpoint.rs index 6d09a0830..b3d459d44 100644 --- a/wp_api/src/request/endpoint/application_passwords_endpoint.rs +++ b/wp_api/src/request/endpoint/application_passwords_endpoint.rs @@ -152,7 +152,7 @@ mod tests { #[rstest] #[case(WpContext::Edit, &[SparseApplicationPasswordField::Uuid], "/users/2/application-passwords/584a87d5-4f18-4c33-a315-4c05ed1fc485?context=edit&_fields=uuid")] - #[case(WpContext::View, &[SparseApplicationPasswordField::Uuid, SparseApplicationPasswordField::Name], "/users/2/application-passwords/584a87d5-4f18-4c33-a315-4c05ed1fc485?context=view&_fields=uuid%2Cname")] + #[case(WpContext::View, &[SparseApplicationPasswordField::Uuid, SparseApplicationPasswordField::Password], "/users/2/application-passwords/584a87d5-4f18-4c33-a315-4c05ed1fc485?context=view&_fields=uuid%2Cpassword")] fn filter_retrieve_application_passwords( endpoint: ApplicationPasswordsRequestEndpoint, #[case] context: WpContext, From 10ae993f5f3faa7aaf01fda766ac32f3a255b087 Mon Sep 17 00:00:00 2001 From: Oguz Kocer Date: Wed, 3 Jul 2024 18:03:47 -0400 Subject: [PATCH 17/22] Update temporary test_credentials generated by Buildkite --- .buildkite/pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 69d281c29..f4c50a76e 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -121,7 +121,7 @@ steps: echo "--- 🧹 Linting" # This is a temporary step until we implement a more graceful way to handle missing credentials - printf "site_url\nadmin_username\nadmin_password\nsubscriber_username\nsubscriber_password\n" > test_credentials + printf "site_url\nadmin_username\nadmin_password\nadmin_password_uuid\nsubscriber_username\nsubscriber_password\nsubscriber_password_uuid\n" > test_credentials cd ./native/kotlin ./gradlew detektMain detektTest From 2f8e84a1b80ca38213f68a1949912469903df1ab Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:03:48 -0600 Subject: [PATCH 18/22] Add Swift compatibility for Application Passwords --- native/swift/Sources/wordpress-api/Exports.swift | 7 +++++++ native/swift/Sources/wordpress-api/WordPressAPI.swift | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/native/swift/Sources/wordpress-api/Exports.swift b/native/swift/Sources/wordpress-api/Exports.swift index 55031ee88..6052be892 100644 --- a/native/swift/Sources/wordpress-api/Exports.swift +++ b/native/swift/Sources/wordpress-api/Exports.swift @@ -41,4 +41,11 @@ public typealias PluginCreateParams = WordPressAPIInternal.PluginCreateParams public typealias PluginDeleteResponse = WordPressAPIInternal.PluginDeleteResponse public typealias PluginsRequestExecutor = WordPressAPIInternal.PluginsRequestExecutor +// MARK: – Application Passwords + +public typealias SparseApplicationPassword = WordPressAPIInternal.SparseApplicationPassword +public typealias ApplicationPasswordWithEditContext = WordPressAPIInternal.ApplicationPasswordWithEditContext +public typealias ApplicationPasswordWithViewContext = WordPressAPIInternal.ApplicationPasswordWithViewContext +public typealias ApplicationPasswordWithEmbedContext = WordPressAPIInternal.ApplicationPasswordWithEmbedContext + #endif diff --git a/native/swift/Sources/wordpress-api/WordPressAPI.swift b/native/swift/Sources/wordpress-api/WordPressAPI.swift index c271bab69..76e51301e 100644 --- a/native/swift/Sources/wordpress-api/WordPressAPI.swift +++ b/native/swift/Sources/wordpress-api/WordPressAPI.swift @@ -47,6 +47,10 @@ public struct WordPressAPI { self.requestBuilder.plugins() } + public var applicationPasswords: ApplicationPasswordsRequestExecutor { + self.requestBuilder.applicationPasswords() + } + package func perform(request: WpNetworkRequest) async throws -> WpNetworkResponse { try await withCheckedThrowingContinuation { continuation in self.perform(request: request) { result in From 13b88990476410e0687c6c338eec976c82d5f84c Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:03:59 -0600 Subject: [PATCH 19/22] Add Application Passwords to Swift Example App --- .../Example/Example.xcodeproj/project.pbxproj | 56 +++++++++++---- .../swift/Example/Example/ContentView.swift | 71 ------------------- native/swift/Example/Example/ExampleApp.swift | 29 +++++++- .../swift/Example/Example/ListViewData.swift | 64 +++++++++++++++++ .../swift/Example/Example/ListViewModel.swift | 58 +++++++++++++++ .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Example/{ => Resources}/Info.plist | 0 .../swift/Example/Example/UI/ListView.swift | 46 ++++++++++++ .../Example/Example/{ => UI}/LoginView.swift | 3 +- .../Preview Assets.xcassets/Contents.json | 0 .../Example/Example/UI/RootListView.swift | 59 +++++++++++++++ .../Example/Example/UserListViewModel.swift | 69 ------------------ .../Example/WordPressAPI+Extensions.swift | 17 +++++ 15 files changed, 316 insertions(+), 156 deletions(-) delete mode 100644 native/swift/Example/Example/ContentView.swift create mode 100644 native/swift/Example/Example/ListViewData.swift create mode 100644 native/swift/Example/Example/ListViewModel.swift rename native/swift/Example/Example/{ => Resources}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename native/swift/Example/Example/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename native/swift/Example/Example/{ => Resources}/Assets.xcassets/Contents.json (100%) rename native/swift/Example/Example/{ => Resources}/Info.plist (100%) create mode 100644 native/swift/Example/Example/UI/ListView.swift rename native/swift/Example/Example/{ => UI}/LoginView.swift (98%) rename native/swift/Example/Example/{ => UI}/Preview Content/Preview Assets.xcassets/Contents.json (100%) create mode 100644 native/swift/Example/Example/UI/RootListView.swift delete mode 100644 native/swift/Example/Example/UserListViewModel.swift create mode 100644 native/swift/Example/Example/WordPressAPI+Extensions.swift diff --git a/native/swift/Example/Example.xcodeproj/project.pbxproj b/native/swift/Example/Example.xcodeproj/project.pbxproj index 70544f3e8..7f04acd5c 100644 --- a/native/swift/Example/Example.xcodeproj/project.pbxproj +++ b/native/swift/Example/Example.xcodeproj/project.pbxproj @@ -7,25 +7,31 @@ objects = { /* Begin PBXBuildFile section */ + 242D648E2C3602C1007CA96C /* ListViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D648D2C3602C1007CA96C /* ListViewData.swift */; }; + 242D64922C360687007CA96C /* RootListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D64912C360687007CA96C /* RootListView.swift */; }; + 242D64942C3608C6007CA96C /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D64932C3608C6007CA96C /* ListView.swift */; }; + 242D64962C360EB3007CA96C /* WordPressAPI+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D64952C360EB3007CA96C /* WordPressAPI+Extensions.swift */; }; 2479BF812B621CB60014A01D /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2479BF802B621CB60014A01D /* ExampleApp.swift */; }; - 2479BF832B621CB60014A01D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2479BF822B621CB60014A01D /* ContentView.swift */; }; 2479BF852B621CB70014A01D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2479BF842B621CB70014A01D /* Assets.xcassets */; }; 2479BF892B621CB70014A01D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2479BF882B621CB70014A01D /* Preview Assets.xcassets */; }; 2479BF912B621CCA0014A01D /* WordPressAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 2479BF902B621CCA0014A01D /* WordPressAPI */; }; - 2479BF932B621E9B0014A01D /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2479BF922B621E9B0014A01D /* UserListViewModel.swift */; }; + 2479BF932B621E9B0014A01D /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2479BF922B621E9B0014A01D /* ListViewModel.swift */; }; 24A3C32F2BA8F96F00162AD1 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */; }; 24A3C3362BAA874C00162AD1 /* LoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C3352BAA874C00162AD1 /* LoginManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 242D648D2C3602C1007CA96C /* ListViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewData.swift; sourceTree = ""; }; + 242D64912C360687007CA96C /* RootListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootListView.swift; sourceTree = ""; }; + 242D64932C3608C6007CA96C /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = ""; }; + 242D64952C360EB3007CA96C /* WordPressAPI+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WordPressAPI+Extensions.swift"; sourceTree = ""; }; 2479BF7D2B621CB60014A01D /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2479BF802B621CB60014A01D /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; - 2479BF822B621CB60014A01D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 2479BF842B621CB70014A01D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2479BF882B621CB70014A01D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 2479BF922B621E9B0014A01D /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = ""; }; + 2479BF922B621E9B0014A01D /* ListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewModel.swift; sourceTree = ""; }; 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; - 24A3C3342BAA45B800162AD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 24A3C3342BAA45B800162AD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24A3C3352BAA874C00162AD1 /* LoginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -41,6 +47,26 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 242D64972C363960007CA96C /* UI */ = { + isa = PBXGroup; + children = ( + 2479BF872B621CB70014A01D /* Preview Content */, + 242D64932C3608C6007CA96C /* ListView.swift */, + 242D64912C360687007CA96C /* RootListView.swift */, + 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */, + ); + path = UI; + sourceTree = ""; + }; + 242D64982C363996007CA96C /* Resources */ = { + isa = PBXGroup; + children = ( + 24A3C3342BAA45B800162AD1 /* Info.plist */, + 2479BF842B621CB70014A01D /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; 2479BF742B621CB60014A01D = { isa = PBXGroup; children = ( @@ -60,14 +86,13 @@ 2479BF7F2B621CB60014A01D /* Example */ = { isa = PBXGroup; children = ( - 24A3C3342BAA45B800162AD1 /* Info.plist */, + 242D64972C363960007CA96C /* UI */, + 242D64982C363996007CA96C /* Resources */, 2479BF802B621CB60014A01D /* ExampleApp.swift */, - 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */, - 2479BF822B621CB60014A01D /* ContentView.swift */, - 2479BF842B621CB70014A01D /* Assets.xcassets */, - 2479BF872B621CB70014A01D /* Preview Content */, - 2479BF922B621E9B0014A01D /* UserListViewModel.swift */, + 2479BF922B621E9B0014A01D /* ListViewModel.swift */, 24A3C3352BAA874C00162AD1 /* LoginManager.swift */, + 242D648D2C3602C1007CA96C /* ListViewData.swift */, + 242D64952C360EB3007CA96C /* WordPressAPI+Extensions.swift */, ); path = Example; sourceTree = ""; @@ -156,11 +181,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2479BF832B621CB60014A01D /* ContentView.swift in Sources */, + 242D64922C360687007CA96C /* RootListView.swift in Sources */, 2479BF812B621CB60014A01D /* ExampleApp.swift in Sources */, 24A3C32F2BA8F96F00162AD1 /* LoginView.swift in Sources */, - 2479BF932B621E9B0014A01D /* UserListViewModel.swift in Sources */, + 2479BF932B621E9B0014A01D /* ListViewModel.swift in Sources */, 24A3C3362BAA874C00162AD1 /* LoginManager.swift in Sources */, + 242D64962C360EB3007CA96C /* WordPressAPI+Extensions.swift in Sources */, + 242D648E2C3602C1007CA96C /* ListViewData.swift in Sources */, + 242D64942C3608C6007CA96C /* ListView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -390,7 +418,7 @@ /* Begin XCSwiftPackageProductDependency section */ 2479BF902B621CCA0014A01D /* WordPressAPI */ = { isa = XCSwiftPackageProductDependency; - productName = "WordPressAPI"; + productName = WordPressAPI; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/native/swift/Example/Example/ContentView.swift b/native/swift/Example/Example/ContentView.swift deleted file mode 100644 index d77b76887..000000000 --- a/native/swift/Example/Example/ContentView.swift +++ /dev/null @@ -1,71 +0,0 @@ -import SwiftUI -import WordPressAPI - -struct ContentView: View { - - @State - private var viewModel: UserListViewModel - - @EnvironmentObject - var loginManager: LoginManager - - init(viewModel: UserListViewModel) { - self.viewModel = viewModel - } - - var body: some View { - Group { - if viewModel.users.isEmpty { - VStack { - ProgressView().progressViewStyle(.circular) - Text("Fetching users") - } - .padding() - } else { - List(viewModel.users) { - Text($0.name) - } - } - } - .onAppear(perform: viewModel.startFetching) -// .onDisappear(perform: viewModel.stopFetching) - .alert( - isPresented: $viewModel.shouldPresentAlert, - error: viewModel.error, - actions: { error in // 2 - if let suggestion = error.recoverySuggestion { - Button(suggestion, action: { - // Recover from an error - }) - } - }, message: { error in // 3 - if let failureReason = error.failureReason { - Text(failureReason) - } else { - Text("Something went wrong") - } - }).toolbar(content: { - #if os(macOS) - ToolbarItem { - Button("Log Out") { - Task { - await loginManager.logout() - } - } - } - #else - ToolbarItem(placement: .bottomBar) { - Button("Log Out") { - Task { - await loginManager.logout() - } - } - } - #endif - }) - } -} -// -// #Preview { -// ContentView() -// } diff --git a/native/swift/Example/Example/ExampleApp.swift b/native/swift/Example/Example/ExampleApp.swift index ad73d32be..ea5b318f7 100644 --- a/native/swift/Example/Example/ExampleApp.swift +++ b/native/swift/Example/Example/ExampleApp.swift @@ -9,7 +9,34 @@ struct ExampleApp: App { var body: some Scene { WindowGroup { if loginManager.isLoggedIn { - ContentView(viewModel: UserListViewModel(loginManager: self.loginManager)) + NavigationView { + // The first column is the sidebar. + RootListView() + + // Initial content of the second column. + EmptyView() + + // Initial content for the third column. + Text("Select a category of settings in the sidebar.") + }.toolbar(content: { + #if os(macOS) + ToolbarItem { + Button("Log Out") { + Task { + await loginManager.logout() + } + } + } + #else + ToolbarItem(placement: .bottomBar) { + Button("Log Out") { + Task { + await loginManager.logout() + } + } + } + #endif + }) } else { LoginView() } diff --git a/native/swift/Example/Example/ListViewData.swift b/native/swift/Example/Example/ListViewData.swift new file mode 100644 index 000000000..25c066a0e --- /dev/null +++ b/native/swift/Example/Example/ListViewData.swift @@ -0,0 +1,64 @@ +import Foundation +import WordPressAPI + +struct ListViewData: Identifiable { + let id: String + let title: String + let subtitle: String + let fields: [String: String] +} + +protocol ListViewDataConvertable: Identifiable { + var asListViewData: ListViewData { get } +} + +extension UserWithEditContext: ListViewDataConvertable { + var asListViewData: ListViewData { + ListViewData(id: "user-\(self.id)", title: self.name, subtitle: self.email, fields: [ + "First Name": self.firstName, + "Last Name": self.lastName, + "Email": self.email + ]) + } +} + +extension UserWithViewContext: ListViewDataConvertable { + var asListViewData: ListViewData { + ListViewData(id: "user-\(self.id)", title: self.name, subtitle: self.slug, fields: [ + "Name": self.name + ]) + } +} + +extension UserWithEmbedContext: ListViewDataConvertable { + var asListViewData: ListViewData { + ListViewData(id: "user-\(self.id)", title: self.name, subtitle: self.slug, fields: [ + "Name": self.name + ]) + } +} + +extension PluginWithEditContext: ListViewDataConvertable { + public var id: String { + self.plugin.slug + } + + var asListViewData: ListViewData { + ListViewData(id: self.plugin.slug, title: self.name, subtitle: self.version, fields: [ + "Author": self.author, + "Author URI": self.authorUri + ]) + } +} + +extension ApplicationPasswordWithEditContext: ListViewDataConvertable { + public var id: String { + self.uuid.uuid + } + + var asListViewData: ListViewData { + ListViewData(id: self.uuid.uuid, title: self.name, subtitle: self.created, fields: [ + "Created": self.created + ]) + } +} diff --git a/native/swift/Example/Example/ListViewModel.swift b/native/swift/Example/Example/ListViewModel.swift new file mode 100644 index 000000000..cb8da680c --- /dev/null +++ b/native/swift/Example/Example/ListViewModel.swift @@ -0,0 +1,58 @@ +import Foundation +import SwiftUI +import WordPressAPI + +@Observable class ListViewModel { + + var listItems: [ListViewData] = [] + var fetchDataTask: Task<[ListViewData], Error> + var isLoading: Bool = false + + var error: MyError? + var shouldPresentAlert = false + + let loginManager: LoginManager + + init(loginManager: LoginManager, fetchDataTask: Task<[ListViewData], Error>) { + self.loginManager = loginManager + self.fetchDataTask = fetchDataTask + } + + func startFetching() { + self.error = nil + self.shouldPresentAlert = false + + Task { @MainActor in + self.isLoading = true + + do { + self.listItems = try await self.fetchDataTask.value + } catch { + self.error = MyError(underlyingError: error) + self.shouldPresentAlert = true + } + + self.isLoading = false + } + } + + func stopFetching() { + self.fetchDataTask.cancel() + } +} + +struct MyError: LocalizedError { + var underlyingError: Error + + var localizedDescription: String { + underlyingError.localizedDescription + } + + var errorDescription: String? { + "Unable to fetch data" + } + + var failureReason: String? { + underlyingError.localizedDescription + } +} diff --git a/native/swift/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json b/native/swift/Example/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from native/swift/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json rename to native/swift/Example/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/native/swift/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/native/swift/Example/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from native/swift/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json rename to native/swift/Example/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/native/swift/Example/Example/Assets.xcassets/Contents.json b/native/swift/Example/Example/Resources/Assets.xcassets/Contents.json similarity index 100% rename from native/swift/Example/Example/Assets.xcassets/Contents.json rename to native/swift/Example/Example/Resources/Assets.xcassets/Contents.json diff --git a/native/swift/Example/Example/Info.plist b/native/swift/Example/Example/Resources/Info.plist similarity index 100% rename from native/swift/Example/Example/Info.plist rename to native/swift/Example/Example/Resources/Info.plist diff --git a/native/swift/Example/Example/UI/ListView.swift b/native/swift/Example/Example/UI/ListView.swift new file mode 100644 index 000000000..17c8468bf --- /dev/null +++ b/native/swift/Example/Example/UI/ListView.swift @@ -0,0 +1,46 @@ +import SwiftUI + +struct ListView: View { + + @State + var viewModel: ListViewModel + + var body: some View { + List(viewModel.listItems) { item in + VStack(alignment: .leading) { + Text(item.title).font(.headline) + Text(item.subtitle).font(.footnote) + } + } + .alert( + isPresented: $viewModel.shouldPresentAlert, + error: viewModel.error, + actions: { error in // 2 + if let suggestion = error.recoverySuggestion { + Button(suggestion, action: { + // Recover from an error + }) + } + }, message: { error in // 3 + if let failureReason = error.failureReason { + Text(failureReason) + } else { + Text("Something went wrong") + } + } + ) + .onAppear(perform: viewModel.startFetching) + .onDisappear(perform: viewModel.stopFetching) + } +} + +#Preview { + + let viewModel = ListViewModel(loginManager: LoginManager(), fetchDataTask: Task(operation: { + [ + ListViewData(id: "1234", title: "Item 1", subtitle: "Subtitle", fields: [:]) + ] + })) + + return ListView(viewModel: viewModel) +} diff --git a/native/swift/Example/Example/LoginView.swift b/native/swift/Example/Example/UI/LoginView.swift similarity index 98% rename from native/swift/Example/Example/LoginView.swift rename to native/swift/Example/Example/UI/LoginView.swift index d5a1860e4..ab6a1e242 100644 --- a/native/swift/Example/Example/LoginView.swift +++ b/native/swift/Example/Example/UI/LoginView.swift @@ -71,8 +71,9 @@ struct LoginView: View { else { abort() // TODO: Better error handling } - + debugPrint(authURL) let loginDetails = try await displayLoginView(withAuthenticationUrl: authURL) + debugPrint(loginDetails) try await loginManager.setLoginCredentials(to: loginDetails) } catch let err { handleLoginError(err) diff --git a/native/swift/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json b/native/swift/Example/Example/UI/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from native/swift/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json rename to native/swift/Example/Example/UI/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/native/swift/Example/Example/UI/RootListView.swift b/native/swift/Example/Example/UI/RootListView.swift new file mode 100644 index 000000000..01cd01a59 --- /dev/null +++ b/native/swift/Example/Example/UI/RootListView.swift @@ -0,0 +1,59 @@ +import SwiftUI +import WordPressAPI + +struct RootListView: View { + + let items = [ + RootListData(name: "Application Passwords", callback: Task(operation: { + try await WordPressAPI.globalInstance.applicationPasswords.listWithEditContext(userId: 1) + .map { $0.asListViewData } + })), + RootListData(name: "Users", callback: Task(operation: { + try await WordPressAPI.globalInstance.users.listWithEditContext(params: .init()) + .map { $0.asListViewData } + })), + RootListData(name: "Plugins", callback: Task(operation: { + try await WordPressAPI.globalInstance.plugins.listWithEditContext(params: .init()) + .map { $0.asListViewData } + })) + ] + + var body: some View { + List(self.items) { data in + RootListViewItem(item: data) + } + } +} + +struct RootListViewItem: View { + let item: RootListData + + var body: some View { + VStack(alignment: .leading, spacing: 4.0) { + NavigationLink { + ListView( + viewModel: ListViewModel( + loginManager: LoginManager(), + fetchDataTask: self.item.callback + ) + ) + } label: { + Text(item.name) + } + } + } +} + +struct RootListData: Identifiable { + + let name: String + let callback: Task<[ListViewData], Error> + + var id: String { + self.name + } +} + +#Preview { + RootListView() +} diff --git a/native/swift/Example/Example/UserListViewModel.swift b/native/swift/Example/Example/UserListViewModel.swift deleted file mode 100644 index 7fac6d018..000000000 --- a/native/swift/Example/Example/UserListViewModel.swift +++ /dev/null @@ -1,69 +0,0 @@ -import Foundation -import SwiftUI -import WordPressAPI - -#if hasFeature(RetroactiveAttribute) -extension UserWithViewContext: @retroactive Identifiable {} -#else -extension UserWithViewContext: Identifiable {} -#endif - -@Observable class UserListViewModel { - - var users: [UserWithViewContext] - var fetchUsersTask: Task? - var error: MyError? - var shouldPresentAlert = false - - let loginManager: LoginManager - - // swiftlint:disable force_try - var api: WordPressAPI { - try! WordPressAPI( - urlSession: .shared, - baseUrl: URL(string: loginManager.getDefaultSiteUrl()!)!, - authenticationStategy: try! loginManager.getLoginCredentials()! - ) - } - // swiftlint:enable force_try - - init(loginManager: LoginManager, users: [UserWithViewContext] = []) { - self.loginManager = loginManager - self.users = users - } - - func startFetching() { - self.error = nil - self.shouldPresentAlert = false - - self.fetchUsersTask = Task { @MainActor in - do { - users = try await api.users.listWithViewContext(params: .init()) - } catch let error { - shouldPresentAlert = true - self.error = MyError(underlyingError: error) - debugPrint(error.localizedDescription) - } - } - } - - func stopFetching() { - self.fetchUsersTask?.cancel() - } -} - -struct MyError: LocalizedError { - var underlyingError: Error - - var localizedDescription: String { - underlyingError.localizedDescription - } - - var errorDescription: String? { - "Unable to fetch users" - } - - var failureReason: String? { - underlyingError.localizedDescription - } -} diff --git a/native/swift/Example/Example/WordPressAPI+Extensions.swift b/native/swift/Example/Example/WordPressAPI+Extensions.swift new file mode 100644 index 000000000..c506c5c80 --- /dev/null +++ b/native/swift/Example/Example/WordPressAPI+Extensions.swift @@ -0,0 +1,17 @@ +import Foundation +import WordPressAPI + +extension WordPressAPI { + static var globalInstance: WordPressAPI { + get throws { + let loginManager = LoginManager() + + return try WordPressAPI( + urlSession: .shared, + baseUrl: URL(string: loginManager.getDefaultSiteUrl()!)!, + authenticationStategy: loginManager.getLoginCredentials()! + ) + } + } + +} From f6415e6a7b46c8057a30247f54deeb0f63032686 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:24:07 -0600 Subject: [PATCH 20/22] Add Swift library support for date parsing --- .../swift/Example/Example/ListViewData.swift | 12 ++++++++++-- .../wordpress-api/Foundation+Extensions.swift | 19 +++++++++++++++++++ .../Foundation+ExtensionsTests.swift | 9 +++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 native/swift/Sources/wordpress-api/Foundation+Extensions.swift create mode 100644 native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift diff --git a/native/swift/Example/Example/ListViewData.swift b/native/swift/Example/Example/ListViewData.swift index 25c066a0e..fcfd24523 100644 --- a/native/swift/Example/Example/ListViewData.swift +++ b/native/swift/Example/Example/ListViewData.swift @@ -56,9 +56,17 @@ extension ApplicationPasswordWithEditContext: ListViewDataConvertable { self.uuid.uuid } + var creationDateString: String { + guard let date = Date.fromWordPressDate(self.created) else { + return self.created + } + + return RelativeDateTimeFormatter().string(for: date) ?? self.created + } + var asListViewData: ListViewData { - ListViewData(id: self.uuid.uuid, title: self.name, subtitle: self.created, fields: [ - "Created": self.created + ListViewData(id: self.uuid.uuid, title: self.name, subtitle: creationDateString, fields: [ + "Created": creationDateString ]) } } diff --git a/native/swift/Sources/wordpress-api/Foundation+Extensions.swift b/native/swift/Sources/wordpress-api/Foundation+Extensions.swift new file mode 100644 index 000000000..760feba26 --- /dev/null +++ b/native/swift/Sources/wordpress-api/Foundation+Extensions.swift @@ -0,0 +1,19 @@ +import Foundation + +public extension Date { + + private static let wordpressDateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(abbreviation: "GMT") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + + return dateFormatter + }() + + /// Parses a date string provided by WordPress APIs (which are assumed to be in GMT) + /// + static func fromWordPressDate(_ string: String) -> Date? { + wordpressDateFormatter.date(from: string) + } +} diff --git a/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift b/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift new file mode 100644 index 000000000..06d3af7b2 --- /dev/null +++ b/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift @@ -0,0 +1,9 @@ +import XCTest +import WordPressAPI + +final class FoundationExtensionsTests: XCTestCase { + + func testWordPressDateTimeParsing() throws { + XCTAssertNotNil(Date.fromWordPressDate("2024-07-04T01:49:37")) + } +} From b9f329e6bbd12f00d2b460d68ecef7f66de5d3c1 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:24:46 -0600 Subject: [PATCH 21/22] Fix sample project path issues --- native/swift/Example/Example.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/native/swift/Example/Example.xcodeproj/project.pbxproj b/native/swift/Example/Example.xcodeproj/project.pbxproj index 7f04acd5c..cd5c0f6ba 100644 --- a/native/swift/Example/Example.xcodeproj/project.pbxproj +++ b/native/swift/Example/Example.xcodeproj/project.pbxproj @@ -316,10 +316,10 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = "\"Example/UI/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Example/Info.plist; + INFOPLIST_FILE = Example/Resources/Info.plist; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -354,10 +354,10 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = "\"Example/UI/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Example/Info.plist; + INFOPLIST_FILE = Example/Resources/Info.plist; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; From 20edfec9f40900f2a525b7baf3f0a640ada81190 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 3 Jul 2024 21:00:07 -0600 Subject: [PATCH 22/22] lintfix --- .../swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift b/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift index 06d3af7b2..47e5a183c 100644 --- a/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift +++ b/native/swift/Tests/wordpress-api/Foundation+ExtensionsTests.swift @@ -2,7 +2,7 @@ import XCTest import WordPressAPI final class FoundationExtensionsTests: XCTestCase { - + func testWordPressDateTimeParsing() throws { XCTAssertNotNil(Date.fromWordPressDate("2024-07-04T01:49:37")) }