diff --git a/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md b/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md new file mode 100644 index 000000000000..357477bf599c --- /dev/null +++ b/Tests/AgentTests/post-editor/custom-post-preserve-private-status.md @@ -0,0 +1,32 @@ +# Preserve Private Visibility When Publishing a Custom Post + +Regression test for the bug where publishing a REST custom post from the pre-publishing sheet flattens a user-selected `private` visibility to a public `publish`. + +## Prerequisites +- Logged in to the app with the test account. +- The site has at least one custom post type registered with REST API support, and the custom post types entry is visible on the My Site screen. If no custom post type is available, fail with "Prerequisite not met: site has no REST custom post type". + +## Steps +1. Navigate to the "My Site" tab. +2. From the blog details menu, tap the **"More"** row (uses an ellipsis icon) to open the Custom Post Types list. +3. Tap one of the available custom post types (e.g., "Books"). +4. Tap the FAB (floating "+" button in the bottom-right corner) to create a new custom post. +5. Enter "CPT private preserve" as the post title. +6. Tap the "Publish" button in the top-right corner to open the pre-publish sheet. +7. From the pre-publish sheet, open "Post Settings". +8. Change the visibility setting to "Private". +9. Return to the pre-publish sheet. +10. Tap "Publish" to commit. +11. Dismiss the confirmation screen by tapping "Done". + +## Verification (REST API) +- Use the WordPress REST API endpoint for the chosen custom post type (e.g., `/wp/v2/?search=CPT+private+preserve&status=private`) to look up the post by title. Authenticate with the application password (private posts are not returned to anonymous requests). +- Verify a post titled "CPT private preserve" exists. +- **Regression assertion:** the post's `status` field is exactly `"private"`, not `"publish"`. A `status` of `"publish"` indicates the bug has regressed. + +## Cleanup (REST API) +- Use the WordPress REST API to trash the post created during this test, regardless of pass or fail. + +## Expected Outcome +- The custom post is published with private visibility and the REST API confirms `status: "private"`. +- The user's `private` selection from Post Settings is preserved on the publish path. diff --git a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift index 7aff6a9ee07a..18cec86586d9 100644 --- a/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift +++ b/WordPress/Classes/ViewRelated/CustomPostTypes/CustomPostEditorService.swift @@ -85,9 +85,10 @@ class CustomPostEditorService { let capabilities = PostSettingsCapabilities(from: details) // At the moment, category & tags are separated from custom taxonomies. We can unify them as taxonomies later, // by which point we won't need this filter logic. - self.taxonomies = (try? blog.taxonomies - .filter { capabilities.customTaxonomySlugs.contains($0.slug) } - .sorted(using: KeyPathComparator(\.name))) ?? [] + self.taxonomies = + (try? blog.taxonomies + .filter { capabilities.customTaxonomySlugs.contains($0.slug) } + .sorted(using: KeyPathComparator(\.name))) ?? [] switch self.state { case let .newPost(params): @@ -122,12 +123,12 @@ class CustomPostEditorService { case (.newPost(let existing), true): var params = settings.makeCreateParameters(from: existing, taxonomies: taxonomies) + params.status = params.status?.normalizedPublishStatus() ?? .publish // Update content if let delegate { let hasTitle = details.supports.map[.title] == .bool(true) let editorContent = try await delegate.editorContent(for: self) - params.status = .publish params.title = hasTitle ? editorContent.title : nil params.content = editorContent.content } @@ -140,7 +141,7 @@ class CustomPostEditorService { case (.existingPost(let post, _), true): var params = settings.makeUpdateParameters(from: post, taxonomies: taxonomies) - params.status = .publish + params.status = PostStatus(settings.status).normalizedPublishStatus() // Update content if let delegate { @@ -162,7 +163,7 @@ class CustomPostEditorService { switch state { case .newPost(let existing): var params = existing - params.status = publish ? .publish : .draft + params.status = publish ? (existing.status?.normalizedPublishStatus() ?? .publish) : .draft params.title = hasTitle ? content.title : nil params.content = content.content try await create(params: params) @@ -181,7 +182,7 @@ class CustomPostEditorService { params = PostUpdateParams(meta: nil) } if publish { - params.status = .publish + params.status = pending.map { PostStatus($0.status).normalizedPublishStatus() } ?? .publish } params.title = hasTitle ? content.title : nil params.content = content.content @@ -195,7 +196,8 @@ class CustomPostEditorService { guard try await !hasBeenModified(post: post) else { throw PostUpdateError.conflicts } let endpoint = details.toPostEndpointType() - let updatedPost = try await wpService.posts().updatePost(endpointType: endpoint, postId: post.id, params: params) + let updatedPost = try await wpService.posts() + .updatePost(endpointType: endpoint, postId: post.id, params: params) state = .existingPost(updatedPost) initialSettings = settings @@ -265,7 +267,8 @@ extension PostCreateParams { params.status = .draft if let categoryID = blog.settings?.defaultCategoryID, - categoryID != PostCategory.uncategorized { + categoryID != PostCategory.uncategorized + { params.categories = [TermId(categoryID.int64Value)] } @@ -278,3 +281,18 @@ extension PostCreateParams { return params } } + +private extension PostStatus { + /// Maps a user-selected status to the one used by a publish action. + /// `.future` and `.private` are preserved because they carry their own + /// publishing semantics (scheduled, password/private visibility); every + /// other selection — draft or pending — collapses to `.publish` so the + /// post is published normally. + func normalizedPublishStatus() -> PostStatus { + switch self { + case .future: return .future + case .private: return .private + default: return .publish + } + } +}