diff --git a/scripts/setup-test-site.sh b/scripts/setup-test-site.sh index 90252b1b..d777a05a 100755 --- a/scripts/setup-test-site.sh +++ b/scripts/setup-test-site.sh @@ -138,6 +138,20 @@ create_nav_menu_item_autosave() { curl --silent --user "$ADMIN_USERNAME":"$ADMIN_PASSWORD" -H "Content-Type: application/json" -d "{\"title\":\"nav_menu_item_autosave_$autosave_number\", \"author\": $ADMIN_USER_ID}" "http://localhost/wp-json/wp/v2/menu-items/$nav_menu_item_id/autosaves" } +create_navigation_revision() { + local revision_number="$1" + local navigation_id="$2" + + curl --silent --user "$ADMIN_USERNAME":"$ADMIN_PASSWORD" -H "Content-Type: application/json" -d "{\"content\":\"content_revision_$revision_number\", \"author\": $ADMIN_USER_ID}" "http://localhost/wp-json/wp/v2/navigation/$navigation_id" > /dev/null +} + +create_navigation_autosave() { + local autosave_number="$1" + local navigation_id="$2" + + curl --silent --user "$ADMIN_USERNAME":"$ADMIN_PASSWORD" -H "Content-Type: application/json" -d "{\"content\":\"content_autosave_$autosave_number\", \"author\": $ADMIN_USER_ID}" "http://localhost/wp-json/wp/v2/navigation/$navigation_id/autosaves" +} + create_test_credentials () { local SITE_URL local ADMIN_USERNAME @@ -243,6 +257,22 @@ create_test_credentials () { NAVIGATION_RESPONSE="$(curl --silent --user "$ADMIN_USERNAME":"$ADMIN_PASSWORD" -H "Content-Type: application/json" -d '{"title":"Integration Test Navigation","content":"","status":"publish"}' http://localhost/wp-json/wp/v2/navigation)" NAVIGATION_ID="$(echo "$NAVIGATION_RESPONSE" | jq -r '.id')" + echo "Setting up navigation with 10 revisions for integration tests.." + # Create revisions for the navigation + for i in {1..10}; + do + create_navigation_revision "$i" "$NAVIGATION_ID" + done + # Generating revisions don't return an id, but since we just created the `NAVIGATION_ID`, we can use it to calculate the revision id + REVISION_ID_FOR_NAVIGATION_ID=$((NAVIGATION_ID + 1)) + + echo "Setting up navigation with autosave for integration tests.." + # Create navigation as author user to enable proper autosave behavior (same requirement as posts/pages) + AUTOSAVED_NAVIGATION_ID="$(wp post create --post_type=wp_navigation --post_title='Autosaved Navigation FOR INTEGRATION TESTS' --post_content='' --post_status=publish --post_author="$AUTHOR_USER_ID" --porcelain)" + # Create autosave as admin user (different from navigation author) and capture its ID + AUTOSAVE_NAVIGATION_RESPONSE="$(create_navigation_autosave "1" "$AUTOSAVED_NAVIGATION_ID")" + AUTOSAVE_ID_FOR_AUTOSAVED_NAVIGATION_ID="$(echo "$AUTOSAVE_NAVIGATION_RESPONSE" | jq -r '.id')" + rm -rf /app/test_credentials.json jo -p \ site_url="$SITE_URL" \ @@ -281,6 +311,9 @@ create_test_credentials () { nav_menu_item_id="$NAV_MENU_ITEM_ID" \ autosave_id_for_nav_menu_item_id="$AUTOSAVE_ID_FOR_NAV_MENU_ITEM_ID" \ navigation_id="$NAVIGATION_ID" \ + revision_id_for_navigation_id="$REVISION_ID_FOR_NAVIGATION_ID" \ + autosaved_navigation_id="$AUTOSAVED_NAVIGATION_ID" \ + autosave_id_for_autosaved_navigation_id="$AUTOSAVE_ID_FOR_AUTOSAVED_NAVIGATION_ID" \ > /app/test_credentials.json } create_test_credentials diff --git a/wp_api/src/api_client.rs b/wp_api/src/api_client.rs index d7efbdb5..0501789c 100644 --- a/wp_api/src/api_client.rs +++ b/wp_api/src/api_client.rs @@ -19,6 +19,12 @@ use crate::{ }, nav_menu_items_endpoint::{NavMenuItemsRequestBuilder, NavMenuItemsRequestExecutor}, nav_menus_endpoint::{NavMenusRequestBuilder, NavMenusRequestExecutor}, + navigation_autosaves_endpoint::{ + NavigationAutosavesRequestBuilder, NavigationAutosavesRequestExecutor, + }, + navigation_revisions_endpoint::{ + NavigationRevisionsRequestBuilder, NavigationRevisionsRequestExecutor, + }, navigations_endpoint::{NavigationsRequestBuilder, NavigationsRequestExecutor}, plugins_endpoint::{PluginsRequestBuilder, PluginsRequestExecutor}, post_autosaves_endpoint::{AutosavesRequestBuilder, AutosavesRequestExecutor}, @@ -54,6 +60,8 @@ pub struct WpApiRequestBuilder { nav_menu_item_autosaves: Arc, nav_menu_items: Arc, nav_menus: Arc, + navigation_autosaves: Arc, + navigation_revisions: Arc, navigations: Arc, plugins: Arc, post_revisions: Arc, @@ -90,6 +98,8 @@ impl WpApiRequestBuilder { nav_menu_item_autosaves, nav_menu_items, nav_menus, + navigation_autosaves, + navigation_revisions, navigations, plugins, post_revisions, @@ -136,6 +146,8 @@ pub struct WpApiClient { nav_menu_item_autosaves: Arc, nav_menu_items: Arc, nav_menus: Arc, + navigation_autosaves: Arc, + navigation_revisions: Arc, navigations: Arc, plugins: Arc, post_revisions: Arc, @@ -169,6 +181,8 @@ impl WpApiClient { nav_menu_item_autosaves, nav_menu_items, nav_menus, + navigation_autosaves, + navigation_revisions, navigations, plugins, post_revisions, @@ -212,6 +226,8 @@ api_client_generate_endpoint_impl!(WpApi, menu_locations); api_client_generate_endpoint_impl!(WpApi, nav_menu_item_autosaves); api_client_generate_endpoint_impl!(WpApi, nav_menu_items); api_client_generate_endpoint_impl!(WpApi, nav_menus); +api_client_generate_endpoint_impl!(WpApi, navigation_autosaves); +api_client_generate_endpoint_impl!(WpApi, navigation_revisions); api_client_generate_endpoint_impl!(WpApi, navigations); api_client_generate_endpoint_impl!(WpApi, plugins); api_client_generate_endpoint_impl!(WpApi, post_revisions); diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 88e238b7..0aeb4a78 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -22,6 +22,7 @@ pub mod middleware; pub mod nav_menu_item_revisions; pub mod nav_menu_items; pub mod nav_menus; +pub mod navigation_revisions; pub mod navigations; pub mod parsed_url; pub mod plugins; diff --git a/wp_api/src/navigation_revisions.rs b/wp_api/src/navigation_revisions.rs new file mode 100644 index 00000000..2119222a --- /dev/null +++ b/wp_api/src/navigation_revisions.rs @@ -0,0 +1,111 @@ +use crate::{ + UserId, WpApiParamOrder, + date::WpGmtDateTime, + impl_as_query_value_from_to_string, + navigations::NavigationId, + url_query::{ + AppendUrlQueryPairs, FromUrlQueryPairs, QueryPairs, QueryPairsExtension, UrlQueryPairsMap, + }, + wp_content_i64_id, +}; +use serde::{Deserialize, Serialize}; +use wp_contextual::WpContextual; +use wp_derive::WpDeriveParamsField; + +wp_content_i64_id!(NavigationRevisionId); + +#[derive( + Debug, + Default, + Clone, + Copy, + PartialEq, + Eq, + uniffi::Enum, + strum_macros::EnumString, + strum_macros::Display, +)] +#[strum(serialize_all = "snake_case")] +pub enum WpApiParamNavigationRevisionsOrderBy { + #[default] + Date, + Id, + Include, + IncludeSlugs, + Relevance, + Slug, + Title, +} + +impl_as_query_value_from_to_string!(WpApiParamNavigationRevisionsOrderBy); + +#[derive(Debug, Default, PartialEq, Eq, uniffi::Record, WpDeriveParamsField)] +#[supports_pagination(true)] +pub struct NavigationRevisionListParams { + /// Current page of the collection. + /// Default: `1` + #[uniffi(default = None)] + pub page: Option, + /// Maximum number of items to be returned in result set. + #[uniffi(default = None)] + pub per_page: Option, + /// Limit results to those matching a string. + #[uniffi(default = None)] + pub search: Option, + /// Ensure result set excludes specific IDs. + #[uniffi(default = [])] + pub exclude: Vec, + /// Limit result set to specific IDs. + #[uniffi(default = [])] + pub include: Vec, + /// Offset the result set by a specific number of items. + #[uniffi(default = None)] + pub offset: Option, + /// Order sort attribute ascending or descending. + /// Default: desc + /// One of: asc, desc + #[uniffi(default = None)] + pub order: Option, + /// Sort collection by object attribute. + /// Default: date + /// One of: date, id, include, relevance, slug, include_slugs, title + #[uniffi(default = None)] + #[field_name("orderby")] + pub orderby: Option, +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record, WpContextual)] +pub struct SparseNavigationRevision { + #[WpContext(edit, embed, view)] + pub id: Option, + #[WpContext(edit, embed, view)] + pub author: Option, + #[WpContext(edit, embed, view)] + pub date: Option, + #[WpContext(edit, view)] + pub date_gmt: Option, + #[WpContext(edit, view)] + pub modified: Option, + #[WpContext(edit, view)] + pub modified_gmt: Option, + #[WpContext(edit, embed, view)] + pub parent: Option, + #[WpContext(edit, embed, view)] + pub slug: Option, + #[WpContext(edit, view)] + #[WpContextualField] + pub guid: Option, + #[WpContext(edit, embed, view)] + #[WpContextualField] + pub title: Option, + #[WpContext(edit, view)] + #[WpContextualField] + pub content: Option, + // meta field omitted for now +} + +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct NavigationRevisionDeleteResponse { + pub deleted: bool, + pub previous: NavigationRevisionWithEditContext, +} diff --git a/wp_api/src/post_revisions.rs b/wp_api/src/post_revisions.rs index 418025d3..842d5511 100644 --- a/wp_api/src/post_revisions.rs +++ b/wp_api/src/post_revisions.rs @@ -105,6 +105,7 @@ pub struct SparseAnyPostRevision { #[WpContextualOption] pub excerpt: Option, #[WpContext(edit, view)] + #[WpContextualOption] pub meta: Option, } diff --git a/wp_api/src/posts.rs b/wp_api/src/posts.rs index e2a888e1..4724a1f6 100644 --- a/wp_api/src/posts.rs +++ b/wp_api/src/posts.rs @@ -11,7 +11,7 @@ use crate::{ }; use serde::{Deserialize, Serialize}; use wp_contextual::WpContextual; -use wp_derive::WpDeriveParamsField; +use wp_derive::{WpDeriveParamsField, WpDeserialize}; use wp_serde_helper::{deserialize_from_string_of_json_array, serialize_as_json_string}; #[derive( @@ -474,11 +474,11 @@ pub struct SparsePostExcerpt { pub protected: Option, } -#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +#[derive(Debug, Serialize, WpDeserialize, uniffi::Record)] pub struct PostMeta { #[serde(deserialize_with = "deserialize_from_string_of_json_array")] #[serde(serialize_with = "serialize_as_json_string")] - pub footnotes: Vec, + pub footnotes: Option>, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, uniffi::Record)] diff --git a/wp_api/src/request/endpoint.rs b/wp_api/src/request/endpoint.rs index 62cd1a95..f97fe19a 100644 --- a/wp_api/src/request/endpoint.rs +++ b/wp_api/src/request/endpoint.rs @@ -11,6 +11,8 @@ pub mod menu_locations_endpoint; pub mod nav_menu_item_autosaves_endpoint; pub mod nav_menu_items_endpoint; pub mod nav_menus_endpoint; +pub mod navigation_autosaves_endpoint; +pub mod navigation_revisions_endpoint; pub mod navigations_endpoint; pub mod plugins_endpoint; pub mod post_autosaves_endpoint; diff --git a/wp_api/src/request/endpoint/navigation_autosaves_endpoint.rs b/wp_api/src/request/endpoint/navigation_autosaves_endpoint.rs new file mode 100644 index 00000000..e4ed3879 --- /dev/null +++ b/wp_api/src/request/endpoint/navigation_autosaves_endpoint.rs @@ -0,0 +1,19 @@ +use super::{AsNamespace, DerivedRequest, WpNamespace}; +use crate::{navigation_revisions::NavigationRevisionId, navigations::NavigationId}; +use wp_derive_request_builder::WpDerivedRequest; + +#[derive(WpDerivedRequest)] +enum NavigationAutosavesRequest { + #[contextual_get(url = "/navigation//autosaves", output = Vec, filter_by = crate::navigation_revisions::SparseNavigationRevisionField)] + List, + #[contextual_get(url = "/navigation//autosaves/", output = crate::navigation_revisions::SparseNavigationRevision, filter_by = crate::navigation_revisions::SparseNavigationRevisionField)] + Retrieve, + #[post(url = "/navigation//autosaves", params = &crate::navigations::NavigationCreateParams, output = crate::navigation_revisions::NavigationRevisionWithEditContext)] + Create, +} + +impl DerivedRequest for NavigationAutosavesRequest { + fn namespace() -> impl AsNamespace { + WpNamespace::WpV2 + } +} diff --git a/wp_api/src/request/endpoint/navigation_revisions_endpoint.rs b/wp_api/src/request/endpoint/navigation_revisions_endpoint.rs new file mode 100644 index 00000000..c87d6e79 --- /dev/null +++ b/wp_api/src/request/endpoint/navigation_revisions_endpoint.rs @@ -0,0 +1,29 @@ +use super::{AsNamespace, DerivedRequest, WpNamespace}; +use crate::{ + navigation_revisions::{NavigationRevisionId, NavigationRevisionListParams}, + navigations::NavigationId, +}; +use wp_derive_request_builder::WpDerivedRequest; + +#[derive(WpDerivedRequest)] +enum NavigationRevisionsRequest { + #[contextual_paged(url = "/navigation//revisions", params = &NavigationRevisionListParams, output = Vec, filter_by = crate::navigation_revisions::SparseNavigationRevisionField)] + List, + #[contextual_get(url = "/navigation//revisions/", output = crate::navigation_revisions::SparseNavigationRevision, filter_by = crate::navigation_revisions::SparseNavigationRevisionField)] + Retrieve, + #[delete(url = "/navigation//revisions/", output = crate::navigation_revisions::NavigationRevisionDeleteResponse)] + Delete, +} + +impl DerivedRequest for NavigationRevisionsRequest { + fn namespace() -> impl AsNamespace { + WpNamespace::WpV2 + } + + fn additional_query_pairs(&self) -> Vec<(&str, String)> { + match self { + NavigationRevisionsRequest::Delete => vec![("force", "true".to_string())], + _ => vec![], + } + } +} diff --git a/wp_api_integration_tests/src/lib.rs b/wp_api_integration_tests/src/lib.rs index bcaaf78c..51a9c23d 100644 --- a/wp_api_integration_tests/src/lib.rs +++ b/wp_api_integration_tests/src/lib.rs @@ -48,6 +48,9 @@ pub struct TestCredentials { pub nav_menu_item_id: i64, pub autosave_id_for_nav_menu_item_id: i64, pub navigation_id: i64, + pub revision_id_for_navigation_id: i64, + pub autosaved_navigation_id: i64, + pub autosave_id_for_autosaved_navigation_id: i64, } impl TestCredentials { diff --git a/wp_api_integration_tests/tests/test_navigation_autosaves_err.rs b/wp_api_integration_tests/tests/test_navigation_autosaves_err.rs new file mode 100644 index 00000000..48a3e7d9 --- /dev/null +++ b/wp_api_integration_tests/tests/test_navigation_autosaves_err.rs @@ -0,0 +1,92 @@ +use wp_api::{ + navigation_revisions::NavigationRevisionId, + navigations::{NavigationCreateParams, NavigationId}, +}; +use wp_api_integration_tests::prelude::*; + +#[tokio::test] +#[parallel] +async fn list_err_post_invalid_parent() { + api_client() + .navigation_autosaves() + .list_with_edit_context(&NavigationId(99999999)) + .await + .assert_wp_error(WpErrorCode::PostInvalidParent) +} + +#[tokio::test] +#[parallel] +async fn list_err_cannot_read_as_subscriber() { + api_client_as_subscriber() + .navigation_autosaves() + .list_with_edit_context(&autosaved_navigation_id()) + .await + .assert_wp_error(WpErrorCode::CannotRead) +} + +#[tokio::test] +#[parallel] +async fn retrieve_err_post_invalid_parent() { + api_client() + .navigation_autosaves() + .retrieve_with_edit_context(&NavigationId(99999999), &NavigationRevisionId(1)) + .await + .assert_wp_error(WpErrorCode::PostInvalidParent) +} + +#[tokio::test] +#[parallel] +async fn retrieve_err_post_no_autosave() { + api_client() + .navigation_autosaves() + .retrieve_with_edit_context( + &NavigationId(TestCredentials::instance().navigation_id), + &NavigationRevisionId(1), + ) + .await + .assert_wp_error(WpErrorCode::PostNoAutosave) +} + +#[tokio::test] +#[parallel] +async fn retrieve_err_cannot_read_as_subscriber() { + api_client_as_subscriber() + .navigation_autosaves() + .retrieve_with_edit_context( + &autosaved_navigation_id(), + &autosave_id_for_autosaved_navigation_id(), + ) + .await + .assert_wp_error(WpErrorCode::CannotRead) +} + +#[tokio::test] +#[parallel] +async fn create_err_post_invalid_id() { + api_client() + .navigation_autosaves() + .create(&NavigationId(99999999), &NavigationCreateParams::default()) + .await + .assert_wp_error(WpErrorCode::PostInvalidId) +} + +#[tokio::test] +#[parallel] +async fn create_err_cannot_edit_as_subscriber() { + api_client_as_subscriber() + .navigation_autosaves() + .create( + &autosaved_navigation_id(), + &NavigationCreateParams::default(), + ) + .await + .assert_wp_error(WpErrorCode::CannotEdit) +} + +fn autosaved_navigation_id() -> NavigationId { + NavigationId(TestCredentials::instance().autosaved_navigation_id) +} + +fn autosave_id_for_autosaved_navigation_id() -> NavigationRevisionId { + NavigationRevisionId(TestCredentials::instance().autosave_id_for_autosaved_navigation_id) +} diff --git a/wp_api_integration_tests/tests/test_navigation_autosaves_immut.rs b/wp_api_integration_tests/tests/test_navigation_autosaves_immut.rs new file mode 100644 index 00000000..51e12836 --- /dev/null +++ b/wp_api_integration_tests/tests/test_navigation_autosaves_immut.rs @@ -0,0 +1,211 @@ +use wp_api::{ + navigation_revisions::{ + NavigationRevisionId, SparseNavigationRevisionFieldWithEditContext, + SparseNavigationRevisionFieldWithEmbedContext, + SparseNavigationRevisionFieldWithViewContext, + }, + navigations::NavigationId, +}; +use wp_api_integration_tests::prelude::*; + +#[tokio::test] +#[parallel] +async fn list_with_edit_context() { + api_client() + .navigation_autosaves() + .list_with_edit_context(&autosaved_navigation_id()) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn list_with_embed_context() { + api_client() + .navigation_autosaves() + .list_with_embed_context(&autosaved_navigation_id()) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn list_with_view_context() { + api_client() + .navigation_autosaves() + .list_with_view_context(&autosaved_navigation_id()) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn retrieve_with_edit_context() { + api_client() + .navigation_autosaves() + .retrieve_with_edit_context( + &autosaved_navigation_id(), + &autosave_id_for_autosaved_navigation_id(), + ) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn retrieve_with_embed_context() { + api_client() + .navigation_autosaves() + .retrieve_with_embed_context( + &autosaved_navigation_id(), + &autosave_id_for_autosaved_navigation_id(), + ) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn retrieve_with_view_context() { + api_client() + .navigation_autosaves() + .retrieve_with_view_context( + &autosaved_navigation_id(), + &autosave_id_for_autosaved_navigation_id(), + ) + .await + .assert_response(); +} + +fn autosaved_navigation_id() -> NavigationId { + NavigationId(TestCredentials::instance().autosaved_navigation_id) +} + +fn autosave_id_for_autosaved_navigation_id() -> NavigationRevisionId { + NavigationRevisionId(TestCredentials::instance().autosave_id_for_autosaved_navigation_id) +} + +mod filter { + use super::*; + + wp_api::generate_sparse_navigation_revision_field_with_edit_context_test_cases!(); + wp_api::generate_sparse_navigation_revision_field_with_embed_context_test_cases!(); + wp_api::generate_sparse_navigation_revision_field_with_view_context_test_cases!(); + + #[apply(sparse_navigation_revision_field_with_edit_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithEditContext::Id, SparseNavigationRevisionFieldWithEditContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_edit_context( + #[case] fields: &[SparseNavigationRevisionFieldWithEditContext], + ) { + api_client() + .navigation_autosaves() + .filter_list_with_edit_context(&autosaved_navigation_id(), fields) + .await + .assert_response() + .data + .iter() + .for_each(|autosave| { + autosave.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_navigation_revision_field_with_embed_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithEmbedContext::Id, SparseNavigationRevisionFieldWithEmbedContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_embed_context( + #[case] fields: &[SparseNavigationRevisionFieldWithEmbedContext], + ) { + api_client() + .navigation_autosaves() + .filter_list_with_embed_context(&autosaved_navigation_id(), fields) + .await + .assert_response() + .data + .iter() + .for_each(|autosave| { + autosave.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_navigation_revision_field_with_view_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithViewContext::Id, SparseNavigationRevisionFieldWithViewContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_view_context( + #[case] fields: &[SparseNavigationRevisionFieldWithViewContext], + ) { + api_client() + .navigation_autosaves() + .filter_list_with_view_context(&autosaved_navigation_id(), fields) + .await + .assert_response() + .data + .iter() + .for_each(|autosave| { + autosave.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_navigation_revision_field_with_edit_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithEditContext::Id, SparseNavigationRevisionFieldWithEditContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_edit_context( + #[case] fields: &[SparseNavigationRevisionFieldWithEditContext], + ) { + api_client() + .navigation_autosaves() + .filter_retrieve_with_edit_context( + &autosaved_navigation_id(), + &autosave_id_for_autosaved_navigation_id(), + fields, + ) + .await + .assert_response() + .data + .assert_that_instance_fields_nullability_match_provided_fields(fields); + } + + #[apply(sparse_navigation_revision_field_with_embed_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithEmbedContext::Id, SparseNavigationRevisionFieldWithEmbedContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_embed_context( + #[case] fields: &[SparseNavigationRevisionFieldWithEmbedContext], + ) { + api_client() + .navigation_autosaves() + .filter_retrieve_with_embed_context( + &autosaved_navigation_id(), + &autosave_id_for_autosaved_navigation_id(), + fields, + ) + .await + .assert_response() + .data + .assert_that_instance_fields_nullability_match_provided_fields(fields); + } + + #[apply(sparse_navigation_revision_field_with_view_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithViewContext::Id, SparseNavigationRevisionFieldWithViewContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_view_context( + #[case] fields: &[SparseNavigationRevisionFieldWithViewContext], + ) { + api_client() + .navigation_autosaves() + .filter_retrieve_with_view_context( + &autosaved_navigation_id(), + &autosave_id_for_autosaved_navigation_id(), + fields, + ) + .await + .assert_response() + .data + .assert_that_instance_fields_nullability_match_provided_fields(fields); + } +} diff --git a/wp_api_integration_tests/tests/test_navigation_autosaves_mut.rs b/wp_api_integration_tests/tests/test_navigation_autosaves_mut.rs new file mode 100644 index 00000000..ca400b2f --- /dev/null +++ b/wp_api_integration_tests/tests/test_navigation_autosaves_mut.rs @@ -0,0 +1,29 @@ +use wp_api::navigations::{NavigationCreateParams, NavigationId}; +use wp_api_integration_tests::prelude::*; + +#[tokio::test] +async fn create_autosave() { + let title = "Test Autosave Navigation Title".to_string(); + let content = + "Test autosave navigation content".to_string(); + let params = NavigationCreateParams { + title: Some(title.clone()), + content: Some(content.clone()), + ..Default::default() + }; + + let autosave = api_client() + .navigation_autosaves() + .create(&autosaved_navigation_id(), ¶ms) + .await + .assert_response() + .data; + + // Verify the autosave was created successfully + assert_eq!(autosave.title.raw, Some(title)); + assert_eq!(autosave.content.raw, Some(content)); +} + +fn autosaved_navigation_id() -> NavigationId { + NavigationId(TestCredentials::instance().autosaved_navigation_id) +} diff --git a/wp_api_integration_tests/tests/test_navigation_revisions_err.rs b/wp_api_integration_tests/tests/test_navigation_revisions_err.rs new file mode 100644 index 00000000..cd822809 --- /dev/null +++ b/wp_api_integration_tests/tests/test_navigation_revisions_err.rs @@ -0,0 +1,164 @@ +use wp_api::{ + navigation_revisions::{ + NavigationRevisionId, NavigationRevisionListParams, WpApiParamNavigationRevisionsOrderBy, + }, + navigations::NavigationId, +}; +use wp_api_integration_tests::prelude::*; + +#[tokio::test] +#[parallel] +async fn list_err_post_invalid_parent() { + api_client() + .navigation_revisions() + .list_with_edit_context( + &NavigationId(99999999), + &NavigationRevisionListParams::default(), + ) + .await + .assert_wp_error(WpErrorCode::PostInvalidParent) +} + +#[tokio::test] +#[parallel] +async fn list_err_revision_invalid_offset_number() { + api_client() + .navigation_revisions() + .list_with_edit_context( + &navigation_id(), + &NavigationRevisionListParams { + offset: Some(99999999), + ..Default::default() + }, + ) + .await + .assert_wp_error(WpErrorCode::RevisionInvalidOffsetNumber) +} + +#[tokio::test] +#[parallel] +async fn list_err_revision_invalid_page_number() { + api_client() + .navigation_revisions() + .list_with_edit_context( + &navigation_id(), + &NavigationRevisionListParams { + page: Some(99999999), + ..Default::default() + }, + ) + .await + .assert_wp_error(WpErrorCode::RevisionInvalidPageNumber) +} + +#[tokio::test] +#[parallel] +async fn list_err_cannot_read_as_subscriber() { + api_client_as_subscriber() + .navigation_revisions() + .list_with_edit_context(&navigation_id(), &NavigationRevisionListParams::default()) + .await + .assert_wp_error(WpErrorCode::CannotRead) +} + +#[tokio::test] +#[parallel] +async fn list_err_no_search_term_defined() { + api_client() + .navigation_revisions() + .list_with_edit_context( + &navigation_id(), + &NavigationRevisionListParams { + orderby: Some(WpApiParamNavigationRevisionsOrderBy::Relevance), + search: None, + ..Default::default() + }, + ) + .await + .assert_wp_error(WpErrorCode::NoSearchTermDefined) +} + +#[tokio::test] +#[parallel] +async fn list_err_orderby_include_missing_include() { + api_client() + .navigation_revisions() + .list_with_edit_context( + &navigation_id(), + &NavigationRevisionListParams { + orderby: Some(WpApiParamNavigationRevisionsOrderBy::Include), + include: vec![], + ..Default::default() + }, + ) + .await + .assert_wp_error(WpErrorCode::OrderbyIncludeMissingInclude) +} + +#[tokio::test] +#[parallel] +async fn retrieve_err_post_invalid_parent() { + api_client() + .navigation_revisions() + .retrieve_with_edit_context(&NavigationId(99999999), &revision_id_for_navigation_id()) + .await + .assert_wp_error(WpErrorCode::PostInvalidParent) +} + +#[tokio::test] +#[parallel] +async fn retrieve_err_post_invalid_id() { + api_client() + .navigation_revisions() + .retrieve_with_edit_context(&navigation_id(), &NavigationRevisionId(99999999)) + .await + .assert_wp_error(WpErrorCode::PostInvalidId) +} + +#[tokio::test] +#[parallel] +async fn retrieve_err_cannot_read_as_subscriber() { + api_client_as_subscriber() + .navigation_revisions() + .retrieve_with_edit_context(&navigation_id(), &revision_id_for_navigation_id()) + .await + .assert_wp_error(WpErrorCode::CannotRead) +} + +#[tokio::test] +#[parallel] +async fn delete_err_cannot_delete_as_subscriber() { + api_client_as_subscriber() + .navigation_revisions() + .delete(&navigation_id(), &revision_id_for_navigation_id()) + .await + .assert_wp_error(WpErrorCode::CannotDelete) +} + +#[tokio::test] +#[parallel] +async fn delete_err_post_invalid_parent() { + api_client() + .navigation_revisions() + .delete(&NavigationId(99999999), &revision_id_for_navigation_id()) + .await + .assert_wp_error(WpErrorCode::PostInvalidParent) +} + +#[tokio::test] +#[parallel] +async fn delete_err_post_invalid_id() { + api_client() + .navigation_revisions() + .delete(&navigation_id(), &NavigationRevisionId(99999999)) + .await + .assert_wp_error(WpErrorCode::PostInvalidId) +} + +fn navigation_id() -> NavigationId { + NavigationId(TestCredentials::instance().navigation_id) +} + +fn revision_id_for_navigation_id() -> NavigationRevisionId { + NavigationRevisionId(TestCredentials::instance().revision_id_for_navigation_id) +} diff --git a/wp_api_integration_tests/tests/test_navigation_revisions_immut.rs b/wp_api_integration_tests/tests/test_navigation_revisions_immut.rs new file mode 100644 index 00000000..101dbe51 --- /dev/null +++ b/wp_api_integration_tests/tests/test_navigation_revisions_immut.rs @@ -0,0 +1,237 @@ +use wp_api::{ + navigation_revisions::{ + NavigationRevisionId, NavigationRevisionListParams, + SparseNavigationRevisionFieldWithEditContext, + SparseNavigationRevisionFieldWithEmbedContext, + SparseNavigationRevisionFieldWithViewContext, WpApiParamNavigationRevisionsOrderBy, + }, + navigations::NavigationId, +}; +use wp_api_integration_tests::prelude::*; + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_edit_context(#[case] params: NavigationRevisionListParams) { + api_client() + .navigation_revisions() + .list_with_edit_context(&navigation_id(), ¶ms) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_embed_context(#[case] params: NavigationRevisionListParams) { + api_client() + .navigation_revisions() + .list_with_embed_context(&navigation_id(), ¶ms) + .await + .assert_response(); +} + +#[tokio::test] +#[apply(list_cases)] +#[parallel] +async fn list_with_view_context(#[case] params: NavigationRevisionListParams) { + api_client() + .navigation_revisions() + .list_with_view_context(&navigation_id(), ¶ms) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn retrieve_with_edit_context() { + api_client() + .navigation_revisions() + .retrieve_with_edit_context(&navigation_id(), &revision_id_for_navigation_id()) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn retrieve_with_embed_context() { + api_client() + .navigation_revisions() + .retrieve_with_embed_context(&navigation_id(), &revision_id_for_navigation_id()) + .await + .assert_response(); +} + +#[tokio::test] +#[parallel] +async fn retrieve_with_view_context() { + api_client() + .navigation_revisions() + .retrieve_with_view_context(&navigation_id(), &revision_id_for_navigation_id()) + .await + .assert_response(); +} + +fn navigation_id() -> NavigationId { + NavigationId(TestCredentials::instance().navigation_id) +} + +fn revision_id_for_navigation_id() -> NavigationRevisionId { + NavigationRevisionId(TestCredentials::instance().revision_id_for_navigation_id) +} + +#[template] +#[rstest] +#[case::default(NavigationRevisionListParams::default())] +#[case::page(generate!(NavigationRevisionListParams, (page, Some(1))))] +#[case::per_page(generate!(NavigationRevisionListParams, (per_page, Some(3))))] +#[case::search(generate!(NavigationRevisionListParams, (search, Some("foo".to_string()))))] +#[case::exclude(generate!(NavigationRevisionListParams, (exclude, vec![NavigationRevisionId(1), NavigationRevisionId(2)])))] +#[case::include(generate!(NavigationRevisionListParams, (include, vec![NavigationRevisionId(1)])))] +#[case::offset(generate!(NavigationRevisionListParams, (offset, Some(5))))] +#[case::order(generate!(NavigationRevisionListParams, (order, Some(WpApiParamOrder::Asc))))] +#[case::orderby(generate!(NavigationRevisionListParams, (orderby, Some(WpApiParamNavigationRevisionsOrderBy::Slug))))] +fn list_cases(#[case] params: NavigationRevisionListParams) {} + +mod filter { + use super::*; + + wp_api::generate_sparse_navigation_revision_field_with_edit_context_test_cases!(); + wp_api::generate_sparse_navigation_revision_field_with_embed_context_test_cases!(); + wp_api::generate_sparse_navigation_revision_field_with_view_context_test_cases!(); + + #[apply(sparse_navigation_revision_field_with_edit_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithEditContext::Id, SparseNavigationRevisionFieldWithEditContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_edit_context( + #[case] fields: &[SparseNavigationRevisionFieldWithEditContext], + #[values( + NavigationRevisionListParams::default(), + generate!(NavigationRevisionListParams, (exclude, vec![NavigationRevisionId(2), NavigationRevisionId(3)])), + generate!(NavigationRevisionListParams, (search, Some("foo".to_string()))) + )] + params: NavigationRevisionListParams, + ) { + api_client() + .navigation_revisions() + .filter_list_with_edit_context(&navigation_id(), ¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|post| { + post.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_navigation_revision_field_with_embed_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithEmbedContext::Id, SparseNavigationRevisionFieldWithEmbedContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_embed_context( + #[case] fields: &[SparseNavigationRevisionFieldWithEmbedContext], + #[values( + NavigationRevisionListParams::default(), + generate!(NavigationRevisionListParams, (exclude, vec![NavigationRevisionId(2), NavigationRevisionId(3)])), + generate!(NavigationRevisionListParams, (search, Some("foo".to_string()))) + )] + params: NavigationRevisionListParams, + ) { + api_client() + .navigation_revisions() + .filter_list_with_embed_context(&navigation_id(), ¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|post| { + post.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_navigation_revision_field_with_view_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithViewContext::Id, SparseNavigationRevisionFieldWithViewContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_list_with_view_context( + #[case] fields: &[SparseNavigationRevisionFieldWithViewContext], + #[values( + NavigationRevisionListParams::default(), + generate!(NavigationRevisionListParams, (exclude, vec![NavigationRevisionId(2), NavigationRevisionId(3)])), + generate!(NavigationRevisionListParams, (search, Some("foo".to_string()))) + )] + params: NavigationRevisionListParams, + ) { + api_client() + .navigation_revisions() + .filter_list_with_view_context(&navigation_id(), ¶ms, fields) + .await + .assert_response() + .data + .iter() + .for_each(|post| { + post.assert_that_instance_fields_nullability_match_provided_fields(fields) + }); + } + + #[apply(sparse_navigation_revision_field_with_edit_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithEditContext::Id, SparseNavigationRevisionFieldWithEditContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_edit_context( + #[case] fields: &[SparseNavigationRevisionFieldWithEditContext], + ) { + api_client() + .navigation_revisions() + .filter_retrieve_with_edit_context( + &navigation_id(), + &revision_id_for_navigation_id(), + fields, + ) + .await + .assert_response() + .data + .assert_that_instance_fields_nullability_match_provided_fields(fields); + } + + #[apply(sparse_navigation_revision_field_with_embed_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithEmbedContext::Id, SparseNavigationRevisionFieldWithEmbedContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_embed_context( + #[case] fields: &[SparseNavigationRevisionFieldWithEmbedContext], + ) { + api_client() + .navigation_revisions() + .filter_retrieve_with_embed_context( + &navigation_id(), + &revision_id_for_navigation_id(), + fields, + ) + .await + .assert_response() + .data + .assert_that_instance_fields_nullability_match_provided_fields(fields); + } + + #[apply(sparse_navigation_revision_field_with_view_context_test_cases)] + #[case(&[SparseNavigationRevisionFieldWithViewContext::Id, SparseNavigationRevisionFieldWithViewContext::Author])] + #[tokio::test] + #[parallel] + async fn filter_retrieve_with_view_context( + #[case] fields: &[SparseNavigationRevisionFieldWithViewContext], + ) { + api_client() + .navigation_revisions() + .filter_retrieve_with_view_context( + &navigation_id(), + &revision_id_for_navigation_id(), + fields, + ) + .await + .assert_response() + .data + .assert_that_instance_fields_nullability_match_provided_fields(fields); + } +} diff --git a/wp_api_integration_tests/tests/test_navigation_revisions_mut.rs b/wp_api_integration_tests/tests/test_navigation_revisions_mut.rs new file mode 100644 index 00000000..52583754 --- /dev/null +++ b/wp_api_integration_tests/tests/test_navigation_revisions_mut.rs @@ -0,0 +1,31 @@ +use wp_api::{navigation_revisions::NavigationRevisionId, navigations::NavigationId}; +use wp_api_integration_tests::prelude::*; + +#[tokio::test] +#[serial] +async fn delete_navigation_revision() { + let revision_id = revision_id_for_navigation_id(); + let revision_delete_response = api_client() + .navigation_revisions() + .delete(&navigation_id(), &revision_id) + .await; + + assert!( + revision_delete_response.is_ok(), + "{revision_delete_response:#?}" + ); + + let delete_response = revision_delete_response.unwrap().data; + assert!(delete_response.deleted); + assert_eq!(delete_response.previous.id, revision_id); + + RestoreServer::db().await; +} + +fn navigation_id() -> NavigationId { + NavigationId(TestCredentials::instance().navigation_id) +} + +fn revision_id_for_navigation_id() -> NavigationRevisionId { + NavigationRevisionId(TestCredentials::instance().revision_id_for_navigation_id) +} diff --git a/wp_api_integration_tests/tests/test_pages_mut.rs b/wp_api_integration_tests/tests/test_pages_mut.rs index 43dc12f7..b107001d 100644 --- a/wp_api_integration_tests/tests/test_pages_mut.rs +++ b/wp_api_integration_tests/tests/test_pages_mut.rs @@ -30,16 +30,17 @@ async fn create_page_with_title_and_meta() { &PostCreateParams { title: Some("foo".to_string()), meta: Some(PostMeta { - footnotes: vec![PostFootnote { + footnotes: Some(vec![PostFootnote { id: "bar".to_string(), content: "baz".to_string(), - }], + }]), }), ..Default::default() }, |created_page, page_from_wp_cli| { let meta = created_page.meta.unwrap(); - let footnote = meta.footnotes.first().unwrap(); + let footnotes = meta.footnotes.unwrap(); + let footnote = footnotes.first().unwrap(); assert_eq!(created_page.title.raw, Some("foo".to_string())); assert_eq!(page_from_wp_cli.title, "foo"); assert_eq!(footnote.id, "bar"); @@ -345,14 +346,15 @@ generate_update_test!( update_meta_to_add_footnote, meta, PostMeta { - footnotes: vec![PostFootnote { + footnotes: Some(vec![PostFootnote { id: "foo".to_string(), content: "bar".to_string() - }] + }]) }, |updated_page, _| { let meta = updated_page.meta.unwrap(); - let footnote = meta.footnotes.first().unwrap(); + let footnotes = meta.footnotes.unwrap(); + let footnote = footnotes.first().unwrap(); assert_eq!(footnote.id, "foo"); assert_eq!(footnote.content, "bar"); } diff --git a/wp_api_integration_tests/tests/test_posts_mut.rs b/wp_api_integration_tests/tests/test_posts_mut.rs index aef37842..43db6465 100644 --- a/wp_api_integration_tests/tests/test_posts_mut.rs +++ b/wp_api_integration_tests/tests/test_posts_mut.rs @@ -32,16 +32,17 @@ async fn create_post_with_title_and_meta() { &PostCreateParams { title: Some("foo".to_string()), meta: Some(PostMeta { - footnotes: vec![PostFootnote { + footnotes: Some(vec![PostFootnote { id: "bar".to_string(), content: "baz".to_string(), - }], + }]), }), ..Default::default() }, |created_post, post_from_wp_cli| { let meta = created_post.meta.unwrap(); - let footnote = meta.footnotes.first().unwrap(); + let footnotes = meta.footnotes.unwrap(); + let footnote = footnotes.first().unwrap(); assert_eq!(created_post.title.raw, Some("foo".to_string())); assert_eq!(post_from_wp_cli.title, "foo"); assert_eq!(footnote.id, "bar"); @@ -313,14 +314,15 @@ generate_update_test!( update_meta_to_add_footnote, meta, PostMeta { - footnotes: vec![PostFootnote { + footnotes: Some(vec![PostFootnote { id: "foo".to_string(), content: "bar".to_string() - }] + }]) }, |updated_post, _| { let meta = updated_post.meta.unwrap(); - let footnote = meta.footnotes.first().unwrap(); + let footnotes = meta.footnotes.unwrap(); + let footnote = footnotes.first().unwrap(); assert_eq!(footnote.id, "foo"); assert_eq!(footnote.content, "bar"); } diff --git a/wp_serde_helper/src/lib.rs b/wp_serde_helper/src/lib.rs index bebf82ee..e88d87e5 100644 --- a/wp_serde_helper/src/lib.rs +++ b/wp_serde_helper/src/lib.rs @@ -24,7 +24,7 @@ where struct StringOfJsonArrayVisitor(PhantomData); impl de::Visitor<'_> for StringOfJsonArrayVisitor { - type Value = Vec; + type Value = Option>; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string containing json array") @@ -35,14 +35,16 @@ impl de::Visitor<'_> for StringOfJsonArrayVisitor { E: de::Error, { if v.is_empty() { - Ok(vec![]) + Ok(None) } else { serde_json::from_str(v).map_err(E::custom) } } } -pub fn deserialize_from_string_of_json_array<'de, T, D>(deserializer: D) -> Result, D::Error> +pub fn deserialize_from_string_of_json_array<'de, T, D>( + deserializer: D, +) -> Result>, D::Error> where T: DeserializeOwned, D: de::Deserializer<'de>,