diff --git a/Tests/KeystoneTests/Tests/Networking/WordPressClientFactoryTests.swift b/Tests/KeystoneTests/Tests/Networking/WordPressClientFactoryTests.swift new file mode 100644 index 000000000000..d32fa46b3aab --- /dev/null +++ b/Tests/KeystoneTests/Tests/Networking/WordPressClientFactoryTests.swift @@ -0,0 +1,90 @@ +import OHHTTPStubs +import OHHTTPStubsSwift +import XCTest + +@testable import WordPress +@testable import WordPressData + +final class WordPressClientFactoryTests: CoreDataTestCase { + override func setUp() { + super.setUp() + contextManager.useAsSharedInstance(untilTestFinished: self) + WordPressClientFactory.shared.reset() + HTTPStubs.removeAllStubs() + stub(condition: { _ in true }) { _ in + HTTPStubsResponse( + jsonObject: [:], + statusCode: 200, + headers: ["Content-Type": "application/json"] + ) + } + } + + override func tearDown() { + WordPressClientFactory.shared.reset() + HTTPStubs.removeAllStubs() + super.tearDown() + } + + func testEvictInstanceIsIdempotentAndCreatesANewClient() throws { + let site = try makeSite(dotComID: 123) + let otherSite = try makeSite(dotComID: 456) + let original = WordPressClientFactory.shared.instance(for: site) + let otherOriginal = WordPressClientFactory.shared.instance(for: otherSite) + + WordPressClientFactory.shared.evictInstance(for: site.blogId) + WordPressClientFactory.shared.evictInstance(for: site.blogId) + + let replacement = WordPressClientFactory.shared.instance(for: site) + XCTAssertFalse(original === replacement) + XCTAssertTrue(replacement === WordPressClientFactory.shared.instance(for: site)) + XCTAssertTrue(otherOriginal === WordPressClientFactory.shared.instance(for: otherSite)) + } + + func testResetCreatesANewClient() throws { + let site = try makeSite(dotComID: 123) + let original = WordPressClientFactory.shared.instance(for: site) + + WordPressClientFactory.shared.reset() + + XCTAssertFalse(original === WordPressClientFactory.shared.instance(for: site)) + } + + func testRemovingDefaultAccountEvictsBlogClient() throws { + let blog = makeBlog(dotComID: 123) + try mainContext.save() + let site = try WordPressSite(blog: blog) + let original = WordPressClientFactory.shared.instance(for: site) + let service = AccountService(coreDataStack: contextManager) + service.setDefaultWordPressComAccount(try XCTUnwrap(blog.account)) + + service.removeDefaultWordPressComAccount() + + XCTAssertFalse(original === WordPressClientFactory.shared.instance(for: site)) + } + + func testRemovingBlogEvictsClient() throws { + let blog = makeBlog(dotComID: 123) + try mainContext.save() + let site = try WordPressSite(blog: blog) + let original = WordPressClientFactory.shared.instance(for: site) + + BlogService(coreDataStack: contextManager).remove(blog) + + XCTAssertFalse(original === WordPressClientFactory.shared.instance(for: site)) + } + + private func makeSite(dotComID: Int) throws -> WordPressSite { + try WordPressSite(blog: makeBlog(dotComID: dotComID)) + } + + private func makeBlog(dotComID: Int) -> Blog { + let blog = BlogBuilder(mainContext, dotComID: NSNumber(value: dotComID)) + .with(url: "https://example.wordpress.com") + .isHostedAtWPcom() + .withAnAccount(username: "test-user", authToken: "test-token") + .build() + blog.account?.uuid = UUID().uuidString + return blog + } +} diff --git a/WordPress/Classes/Networking/WordPressClient.swift b/WordPress/Classes/Networking/WordPressClient.swift index 5d22f0e7c0b0..0c3cf2489b7e 100644 --- a/WordPress/Classes/Networking/WordPressClient.swift +++ b/WordPress/Classes/Networking/WordPressClient.swift @@ -25,6 +25,12 @@ public final class WordPressClientFactory: Sendable { } } + public func evictInstance(for blogID: TaggedManagedObjectID) { + instances.withLock { instances in + instances.removeValue(forKey: blogID) + } + } + public func reset() { instances.withLock { dict in dict.removeAll() diff --git a/WordPress/Classes/Services/AccountService+Swift.swift b/WordPress/Classes/Services/AccountService+Swift.swift index 57c9356f7672..1a84e2ee5e86 100644 --- a/WordPress/Classes/Services/AccountService+Swift.swift +++ b/WordPress/Classes/Services/AccountService+Swift.swift @@ -16,7 +16,8 @@ extension AccountService { return } - UserPersistentStoreFactory.instance().set(account.uuid, forKey: AccountService.defaultDotcomAccountUUIDDefaultsKey) + UserPersistentStoreFactory.instance() + .set(account.uuid, forKey: AccountService.defaultDotcomAccountUUIDDefaultsKey) let objectID = TaggedManagedObjectID(account) let notifyAccountChange = { @@ -48,6 +49,11 @@ extension AccountService { return } + account.blogs? + .forEach { + WordPressClientFactory.shared.evictInstance(for: TaggedManagedObjectID($0)) + } + let objectID = TaggedManagedObjectID(account) coreDataStack.performAndSave { context in do { diff --git a/WordPress/Classes/Services/BlogService+Swift.swift b/WordPress/Classes/Services/BlogService+Swift.swift index da2b1dcd7f6e..00acda627b3f 100644 --- a/WordPress/Classes/Services/BlogService+Swift.swift +++ b/WordPress/Classes/Services/BlogService+Swift.swift @@ -6,6 +6,11 @@ import WordPressCore import WordPressAPI extension BlogService { + @objc(evictWordPressClientForBlog:) + public func evictWordPressClient(for blog: Blog) { + WordPressClientFactory.shared.evictInstance(for: TaggedManagedObjectID(blog)) + } + @objc public func unscheduleBloggingReminders(for blog: Blog) { do { let scheduler = try ReminderScheduleCoordinator() @@ -20,11 +25,12 @@ extension BlogService { @objc public func updatePromptSettings(for blog: RemoteBlog?, context: NSManagedObjectContext) { guard let blog, - let jsonSettings = blog.options["blogging_prompts_settings"] as? [String: Any], - let settingsValue = jsonSettings["value"] as? [String: Any], - JSONSerialization.isValidJSONObject(settingsValue), - let data = try? JSONSerialization.data(withJSONObject: settingsValue), - let remoteSettings = try? JSONDecoder().decode(RemoteBloggingPromptsSettings.self, from: data) else { + let jsonSettings = blog.options["blogging_prompts_settings"] as? [String: Any], + let settingsValue = jsonSettings["value"] as? [String: Any], + JSONSerialization.isValidJSONObject(settingsValue), + let data = try? JSONSerialization.data(withJSONObject: settingsValue), + let remoteSettings = try? JSONDecoder().decode(RemoteBloggingPromptsSettings.self, from: data) + else { return } @@ -130,14 +136,19 @@ extension BlogService { guard blog.isSelfHosted, let xmlrpcApi = blog.xmlrpcApi, - let username = blog.username, - let password = blog.password else { + let username = blog.username, + let password = blog.password + else { // Set isXMLRPCDisabled to false if the site is not a self-hosted site. - self.coreDataStack.performAndSave({ context in - guard let blog = try? context.existingObject(with: blogObjectID) as? Blog else { return } - blog.isXMLRPCDisabled = false - }, completion: failure, on: .main) + self.coreDataStack.performAndSave( + { context in + guard let blog = try? context.existingObject(with: blogObjectID) as? Blog else { return } + blog.isXMLRPCDisabled = false + }, + completion: failure, + on: .main + ) return } @@ -170,7 +181,10 @@ extension BlogService { } } - static func blog(with site: JetpackSiteRef, context: NSManagedObjectContext = ContextManager.shared.mainContext) -> Blog? { + static func blog( + with site: JetpackSiteRef, + context: NSManagedObjectContext = ContextManager.shared.mainContext + ) -> Blog? { let blog: Blog? if site.isSelfHostedWithoutJetpack, let xmlRPC = site.xmlRPC { @@ -184,7 +198,15 @@ extension BlogService { } private extension BlogService { - private func findBlogAuthor(with userId: NSNumber, and blog: Blog, in context: NSManagedObjectContext) -> BlogAuthor { - return context.entity(of: BlogAuthor.self, with: NSPredicate(format: "\(#keyPath(BlogAuthor.userID)) = %@ AND \(#keyPath(BlogAuthor.blog)) = %@", userId, blog)) + private func findBlogAuthor(with userId: NSNumber, and blog: Blog, in context: NSManagedObjectContext) -> BlogAuthor + { + context.entity( + of: BlogAuthor.self, + with: NSPredicate( + format: "\(#keyPath(BlogAuthor.userID)) = %@ AND \(#keyPath(BlogAuthor.blog)) = %@", + userId, + blog + ) + ) } } diff --git a/WordPress/Classes/Services/BlogService.m b/WordPress/Classes/Services/BlogService.m index 323d96c64d27..7b3d522a7586 100644 --- a/WordPress/Classes/Services/BlogService.m +++ b/WordPress/Classes/Services/BlogService.m @@ -344,6 +344,7 @@ - (void)removeBlog:(Blog *)blog DDLogInfo(@" remove", blog.displayURL); [blog.xmlrpcApi invalidateAndCancelTasks]; [self unscheduleBloggingRemindersFor:blog]; + [self evictWordPressClientForBlog:blog]; WPAccount *account = blog.account;