From 1e0dca407d0fb80940e35618d8c950d4ad1c4dc1 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 13 Aug 2026 14:25:51 +1200 Subject: [PATCH] Add unspam and untrash comment endpoints Mirror wp-admin's "Not Spam" and "Restore" actions with dedicated executor methods, implemented as paramless POST variants that send the REST status param's unspam/untrash values. These restore the comment's saved pre-spam/pre-trash status and fire the unspam_comment / untrash_comment hooks, which a plain hold status write would not do. Replaces the CommentStatus.custom("unspam") / custom("untrash") workaround used by the mobile apps. --- CHANGELOG.md | 1 + .../src/request/endpoint/comments_endpoint.rs | 28 ++++++++++ .../tests/test_comments_mut.rs | 52 +++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9b2c9c8..07c269e3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `unspam` and `untrash` comment endpoints, mirroring wp-admin's "Not Spam" and "Restore" actions. They restore the comment's saved pre-spam/pre-trash status, which a plain `hold` status write would not do. Replaces the `CommentStatus.custom("unspam")` / `custom("untrash")` workaround. - WordPress.com `POST /me/transactions` endpoint for redeeming a shopping cart with the account's WordPress.com credits, completing a domain purchase - WordPress.com `GET /sites//purchases` endpoint for listing a site's purchases (plans, domains, and other subscriptions) - Publish the Kotlin bindings' per-endpoint Markdown API reference as an `ai-docs` Maven classifier zip on `rs.wordpress.api:kotlin`, generated from the UniFFI bindings for agent/tooling consumption diff --git a/wp_api/src/request/endpoint/comments_endpoint.rs b/wp_api/src/request/endpoint/comments_endpoint.rs index 2a29bb1fc..5fbe8e2d6 100644 --- a/wp_api/src/request/endpoint/comments_endpoint.rs +++ b/wp_api/src/request/endpoint/comments_endpoint.rs @@ -16,6 +16,16 @@ enum CommentsRequest { Trash, #[post(url = "/comments/", params = &CommentUpdateParams, output = crate::comments::CommentWithEditContext)] Update, + // Unspam and Untrash mirror wp-admin's "Not Spam" and "Restore" actions + // via the dedicated `unspam`/`untrash` values of the REST `status` param. + // They restore the comment's saved pre-spam/pre-trash status from comment + // meta and fire the `unspam_comment`/`untrash_comment` hooks, which a + // plain status write (e.g. `hold`) would not do. The server rejects them + // (HTTP 500) when the comment is not currently spam/trash. + #[post(url = "/comments/", output = crate::comments::CommentWithEditContext)] + Unspam, + #[post(url = "/comments/", output = crate::comments::CommentWithEditContext)] + Untrash, } impl DerivedRequest for CommentsRequest { @@ -23,6 +33,8 @@ impl DerivedRequest for CommentsRequest { match self { CommentsRequest::Delete => vec![("force", true.to_string())], CommentsRequest::Trash => vec![("force", false.to_string())], + CommentsRequest::Unspam => vec![("status", "unspam".to_string())], + CommentsRequest::Untrash => vec![("status", "untrash".to_string())], _ => vec![], } } @@ -288,6 +300,22 @@ mod tests { ); } + #[rstest] + fn unspam_comment(endpoint: CommentsRequestEndpoint) { + validate_wp_v2_endpoint( + endpoint.unspam(&CommentId(54)), + "/comments/54?status=unspam", + ); + } + + #[rstest] + fn untrash_comment(endpoint: CommentsRequestEndpoint) { + validate_wp_v2_endpoint( + endpoint.untrash(&CommentId(54)), + "/comments/54?status=untrash", + ); + } + const EXPECTED_QUERY_PAIRS_FOR_ALL_SPARSE_COMMENT_FIELDS_WITH_EDIT_CONTEXT: &str = "_fields=id%2Cauthor%2Cauthor_email%2Cauthor_ip%2Cauthor_name%2Cauthor_url%2Cauthor_user_agent%2Ccontent%2Cdate%2Cdate_gmt%2Clink%2Cparent%2Cpost%2Cstatus%2Ctype%2Cauthor_avatar_urls"; const ALL_SPARSE_COMMENT_FIELDS_WITH_EDIT_CONTEXT: &[SparseCommentFieldWithEditContext; 16] = &[ SparseCommentFieldWithEditContext::Id, diff --git a/wp_api_integration_tests/tests/test_comments_mut.rs b/wp_api_integration_tests/tests/test_comments_mut.rs index 38c42292e..8f90d1ba4 100644 --- a/wp_api_integration_tests/tests/test_comments_mut.rs +++ b/wp_api_integration_tests/tests/test_comments_mut.rs @@ -84,6 +84,58 @@ async fn trash_comment() { RestoreServer::db().await; } +#[tokio::test] +#[serial] +async fn unspam_comment_restores_previous_status() { + // FIRST_COMMENT_ID is approved in the seed data. Mark it as spam first. + let spammed_comment = api_client() + .comments() + .update( + &FIRST_COMMENT_ID, + &CommentUpdateParams { + status: Some(CommentStatus::Spam), + ..Default::default() + }, + ) + .await + .assert_response() + .data; + assert_eq!(spammed_comment.status, CommentStatus::Spam); + + // Unspam must restore the saved pre-spam status (approved), not `hold`. + let unspammed_comment = api_client() + .comments() + .unspam(&FIRST_COMMENT_ID) + .await + .assert_response() + .data; + assert_eq!(unspammed_comment.status, CommentStatus::Approved); + + RestoreServer::db().await; +} + +#[tokio::test] +#[serial] +async fn untrash_comment_restores_previous_status() { + // FIRST_COMMENT_ID is approved in the seed data. Trash it first. + api_client() + .comments() + .trash(&FIRST_COMMENT_ID, &CommentDeleteParams::default()) + .await + .assert_response(); + + // Untrash must restore the saved pre-trash status (approved), not `hold`. + let untrashed_comment = api_client() + .comments() + .untrash(&FIRST_COMMENT_ID) + .await + .assert_response() + .data; + assert_eq!(untrashed_comment.status, CommentStatus::Approved); + + RestoreServer::db().await; +} + generate_update_test!( update_author, author,