Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions Sources/WordPressData/Swift/Blog+Post.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CoreData
import Foundation
import WordPressKit

// MARK: - Lookup posts

Expand Down Expand Up @@ -38,7 +39,12 @@ extension Blog {
@objc(lookupLocalPostWithForeignID:inContext:)
public func lookupLocalPost(withForeignID foreignID: UUID, in context: NSManagedObjectContext) -> AbstractPost? {
let request = NSFetchRequest<AbstractPost>(entityName: NSStringFromClass(AbstractPost.self))
request.predicate = NSPredicate(format: "blog = %@ AND original = NULL AND (postID = NULL OR postID <= 0) AND \(#keyPath(AbstractPost.foreignID)) == %@", self, foreignID as NSUUID)
request.predicate = NSPredicate(
format:
"blog = %@ AND original = NULL AND (postID = NULL OR postID <= 0) AND \(#keyPath(AbstractPost.foreignID)) == %@",
self,
foreignID as NSUUID
)
request.fetchLimit = 1
return (try? context.fetch(request))?.first
}
Expand All @@ -61,12 +67,21 @@ extension Blog {
post.foreignID = UUID()

if let categoryID = settings?.defaultCategoryID,
categoryID != PostCategory.uncategorized,
let category = try? PostCategory.lookup(withBlogID: objectID, categoryID: categoryID, in: context) {
categoryID != PostCategory.uncategorized,
let category = try? PostCategory.lookup(withBlogID: objectID, categoryID: categoryID, in: context)
{
post.addCategoriesObject(category)
}

post.postFormat = settings?.defaultPostFormat

if let allowComments = settings?.commentsAllowed?.boolValue {
post.commentsStatus = (allowComments ? RemotePostDiscussionState.open : .closed).rawValue
}
if let allowPings = settings?.pingbackInboundEnabled?.boolValue {
post.pingsStatus = (allowPings ? RemotePostDiscussionState.open : .closed).rawValue
}

post.postType = Post.typeDefaultIdentifier

if let userID, let author = getAuthorWith(id: userID) {
Expand Down
4 changes: 2 additions & 2 deletions Sources/WordPressData/Swift/BlogSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ open class BlogSettings: NSManagedObject {

/// Represents whether comments are allowed, or not.
///
@NSManaged public var commentsAllowed: Bool
@NSManaged public var commentsAllowed: NSNumber?

/// Contains a list of words, space separated, that would cause a comment to be automatically blocklisted.
///
Expand Down Expand Up @@ -138,7 +138,7 @@ open class BlogSettings: NSManagedObject {

/// If set to true, 3rd party sites will be allowed to post pingbacks.
///
@NSManaged public var pingbackInboundEnabled: Bool
@NSManaged public var pingbackInboundEnabled: NSNumber?

/// When Outbound Pingbacks are enabled, 3rd party sites that get linked will be notified.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,11 @@ struct CustomPostSettingsViewModelTests {

// MARK: - Test Helpers

private func makePostWithDisabledConnection(status: PostStatus = .publish) throws -> AnyPostWithEditContext {
private func makePostWithDisabledConnection(
status: PostStatus = .publish,
commentStatus: PostCommentStatus? = .open,
pingStatus: PostPingStatus? = .open
) throws -> AnyPostWithEditContext {
// Mirrors the real server response observed when a published post has a
// connection that was already shared (server returns enabled: false).
let json = #"""
Expand Down Expand Up @@ -225,8 +229,8 @@ private func makePostWithDisabledConnection(status: PostStatus = .publish) throw
author: nil,
excerpt: nil,
featuredMedia: nil,
commentStatus: .open,
pingStatus: .open,
commentStatus: commentStatus,
pingStatus: pingStatus,
format: nil,
meta: nil,
sticky: nil,
Expand Down
211 changes: 201 additions & 10 deletions Tests/KeystoneTests/Tests/Features/Posts/PostSettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Testing
import Foundation
import CoreData
import JetpackSocial
import SwiftUI
import WordPressAPIInternal
@testable import WordPress
@testable import WordPressData
Expand Down Expand Up @@ -1053,14 +1054,14 @@ struct PostSettingsTests {
func testDefaultsInheritsClosedDiscussion() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).with(siteName: "Test").build()
blog.settings?.commentsAllowed = false
blog.settings?.pingbackInboundEnabled = false
blog.settings?.commentsAllowed = NSNumber(value: false)
blog.settings?.pingbackInboundEnabled = NSNumber(value: false)

let settings = PostSettings.defaults(from: blog)
let params = settings.makeCreateParameters(taxonomies: [])

#expect(!settings.allowComments)
#expect(!settings.allowPings)
#expect(settings.allowComments == false)
#expect(settings.allowPings == false)
#expect(params.commentStatus == .closed)
#expect(params.pingStatus == .closed)
}
Expand All @@ -1069,17 +1070,198 @@ struct PostSettingsTests {
func testDefaultsInheritsOpenDiscussion() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).with(siteName: "Test").build()
blog.settings?.commentsAllowed = true
blog.settings?.pingbackInboundEnabled = true
blog.settings?.commentsAllowed = NSNumber(value: true)
blog.settings?.pingbackInboundEnabled = NSNumber(value: true)

let settings = PostSettings.defaults(from: blog)
let params = settings.makeCreateParameters(taxonomies: [])

#expect(settings.allowComments)
#expect(settings.allowPings)
#expect(settings.allowComments == true)
#expect(settings.allowPings == true)
#expect(params.commentStatus == .open)
#expect(params.pingStatus == .open)
}

// MARK: - PostSettings discussion tri-state

@Test("New AbstractPost with unset comment status yields nil allowComments")
func testInitFromNewPostHasUnknownDiscussion() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).build()
let post = PostBuilder(context, blog: blog).build()
post.commentsStatus = nil
post.pingsStatus = nil

let settings = PostSettings(from: post)

#expect(settings.allowComments == nil)
#expect(settings.allowPings == nil)
}

@Test("Existing AbstractPost maps stored comment status to non-nil")
func testInitFromExistingPostHasKnownDiscussion() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).build()
let post = PostBuilder(context, blog: blog).build()
post.commentsStatus = "closed"
post.pingsStatus = "open"

let settings = PostSettings(from: post)

#expect(settings.allowComments == false)
#expect(settings.allowPings == true)
}

@Test("REST post with unset comment status yields nil discussion settings")
func testInitFromRestPostHasUnknownDiscussion() {
let post = makeRemotePost(commentStatus: nil, pingStatus: nil)

let settings = PostSettings(from: post)

#expect(settings.allowComments == nil)
#expect(settings.allowPings == nil)
}

@Test("makeCreateParameters omits comment status when unknown")
func testMakeCreateParametersOmitsUnknownDiscussion() {
var settings = PostSettings()
settings.allowComments = nil
settings.allowPings = nil

let params = settings.makeCreateParameters()

#expect(params.commentStatus == nil)
#expect(params.pingStatus == nil)
}

@Test("makeUpdateParameters omits comment/ping status when unknown")
func testMakeUpdateParametersOmitsUnknownDiscussion() {
let post = makeRemotePost()
var settings = PostSettings(from: post)
settings.allowComments = nil
settings.allowPings = nil

let params = settings.makeUpdateParameters(from: post)

#expect(params.commentStatus == nil)
#expect(params.pingStatus == nil)
}

@Test("apply leaves stored comment/ping status untouched when unknown")
func testApplyLeavesDiscussionUntouchedWhenUnknown() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).build()
let post = PostBuilder(context, blog: blog).build()
post.commentsStatus = "closed"
post.pingsStatus = "closed"

var settings = PostSettings(from: post)
settings.allowComments = nil
settings.allowPings = nil
settings.apply(to: post)

#expect(post.commentsStatus == "closed")
#expect(post.pingsStatus == "closed")
}

// MARK: - Blog.createPost() discussion seeding

@Test("createPost seeds comment/ping status from blog discussion defaults")
func testCreatePostSeedsDiscussionDefaults() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).with(siteName: "Test").build()
blog.settings?.commentsAllowed = NSNumber(value: false)
blog.settings?.pingbackInboundEnabled = NSNumber(value: true)

let post = blog.createPost()

#expect(post.commentsStatus == "closed")
#expect(post.pingsStatus == "open")
}

// MARK: - Unreadable site discussion defaults

@Test("defaults yields nil discussion when site defaults are unreadable")
func testDefaultsUnknownWhenSiteDefaultsUnreadable() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).with(siteName: "Test").build()
blog.settings?.commentsAllowed = nil
blog.settings?.pingbackInboundEnabled = nil

let settings = PostSettings.defaults(from: blog)

#expect(settings.allowComments == nil)
#expect(settings.allowPings == nil)
}

@Test("createPost leaves comment status unset when site defaults are unreadable")
func testCreatePostNoSeedWhenUnreadable() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).with(siteName: "Test").build()
blog.settings?.commentsAllowed = nil
blog.settings?.pingbackInboundEnabled = nil

let post = blog.createPost()

#expect(post.commentsStatus == nil)
#expect(post.pingsStatus == nil)
}

// MARK: - Discussion row visibility gate (CMM-2077)

@Test("Discussion row is hidden for a new post with unknown discussion defaults")
func testDiscussionRowHiddenWhenDefaultsUnknown() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).build()
let post = PostBuilder(context, blog: blog).build()
post.commentsStatus = nil
post.pingsStatus = nil

let viewModel = PostSettingsViewModel(post: post)

#expect(viewModel.settings.allowComments == nil)
#expect(!viewModel.visibleMoreOptions.contains(.discussion))
}

@Test("Discussion row is shown when the post has a known comment status")
func testDiscussionRowShownWhenStatusKnown() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).build()
let post = PostBuilder(context, blog: blog).build()
post.commentsStatus = "open"
post.pingsStatus = "open"

let viewModel = PostSettingsViewModel(post: post)

#expect(viewModel.settings.allowComments == true)
#expect(viewModel.visibleMoreOptions.contains(.discussion))
}

@Test("Discussion row is shown when only the ping status is known")
func testDiscussionRowShownWhenOnlyPingStatusKnown() {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).build()
let post = PostBuilder(context, blog: blog).build()
post.commentsStatus = nil
post.pingsStatus = "open"

let viewModel = PostSettingsViewModel(post: post)

#expect(viewModel.settings.allowComments == nil)
#expect(viewModel.settings.allowPings == true)
#expect(viewModel.visibleMoreOptions.contains(.discussion))
}

@Test("Discussion view only shows sections for known settings")
func testDiscussionViewSectionVisibility() {
let commentsOnlyView = makeDiscussionView(allowComments: true, allowPings: nil)
#expect(commentsOnlyView.showsCommentsSection)
#expect(!commentsOnlyView.showsPingsSection)

let pingsOnlyView = makeDiscussionView(allowComments: nil, allowPings: true)
#expect(!pingsOnlyView.showsCommentsSection)
#expect(pingsOnlyView.showsPingsSection)
}
}

// MARK: - Test Helpers
Expand All @@ -1090,12 +1272,21 @@ private extension SiteTaxonomy {
}
}

private func makeDiscussionView(allowComments: Bool?, allowPings: Bool?) -> PostDiscussionSettingsView {
var settings = PostSettings()
settings.allowComments = allowComments
settings.allowPings = allowPings
return PostDiscussionSettingsView(postSettings: .constant(settings))
}

private func makeRemotePost(
tags: [TermId]? = nil,
categories: [TermId]? = nil,
featuredMedia: MediaId? = nil,
format: PostFormat? = nil,
meta: PostMeta? = nil,
commentStatus: PostCommentStatus? = .open,
pingStatus: PostPingStatus? = .open,
additionalFields: WpAdditionalFields? = nil
) -> AnyPostWithEditContext {
AnyPostWithEditContext(
Expand All @@ -1117,8 +1308,8 @@ private func makeRemotePost(
author: nil,
excerpt: nil,
featuredMedia: featuredMedia,
commentStatus: .open,
pingStatus: .open,
commentStatus: commentStatus,
pingStatus: pingStatus,
format: format,
meta: meta,
sticky: nil,
Expand Down
30 changes: 30 additions & 0 deletions Tests/KeystoneTests/Tests/Services/BlogServiceTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#import "WordPressTest-Swift.h"

@import WordPressData;
@import WordPressKitModels;

@import OCMock;

Expand Down Expand Up @@ -68,4 +69,33 @@ - (void)cleanUpNSUserDefaultValues
[UserSettings setDefaultDotComUUID:nil];
}

- (void)testUpdateSettingsAppliesPresentValuesIncludingFalse
{
self.blog.settings.commentsAllowed = @YES;
self.blog.settings.commentsCloseAutomatically = YES;
self.blog.settings.pingbackOutboundEnabled = YES;
self.blog.settings.relatedPostsEnabled = YES;
self.blog.settings.ampEnabled = YES;
self.blog.settings.sharingDisabledLikes = YES;

RemoteBlogSettings *remoteSettings = [RemoteBlogSettings new];
remoteSettings.commentsAllowed = @NO;
remoteSettings.commentsCloseAutomatically = @NO;
remoteSettings.pingbackOutboundEnabled = @NO;
remoteSettings.relatedPostsEnabled = @NO;
remoteSettings.ampEnabled = @NO;
remoteSettings.sharingDisabledLikes = @NO;
remoteSettings.tagline = @"New tagline";

[self.blogService updateSettings:self.blog.settings withRemoteSettings:remoteSettings];

XCTAssertEqualObjects(self.blog.settings.commentsAllowed, @NO);
XCTAssertFalse(self.blog.settings.commentsCloseAutomatically);
XCTAssertFalse(self.blog.settings.pingbackOutboundEnabled);
XCTAssertFalse(self.blog.settings.relatedPostsEnabled);
XCTAssertFalse(self.blog.settings.ampEnabled);
XCTAssertFalse(self.blog.settings.sharingDisabledLikes);
XCTAssertEqualObjects(self.blog.settings.tagline, @"New tagline");
}

@end
Loading