diff --git a/WordPress/Classes/Services/ReaderTopicService.h b/WordPress/Classes/Services/ReaderTopicService.h index 4c6bcf69df14..4da69970d967 100644 --- a/WordPress/Classes/Services/ReaderTopicService.h +++ b/WordPress/Classes/Services/ReaderTopicService.h @@ -241,6 +241,7 @@ extern NSString * const ReaderTopicFreshlyPressedPathCommponent; @end @interface ReaderTopicService (Tests) +- (void)mergeFollowedSites:(NSArray *)sites withSuccess:(void (^)(void))success; - (void)mergeMenuTopics:(NSArray *)topics withSuccess:(void (^)(void))success; - (NSString *)formatTitle:(NSString *)str; @end diff --git a/WordPress/Classes/Services/ReaderTopicService.m b/WordPress/Classes/Services/ReaderTopicService.m index 77afbcaa5626..8f68abb99fba 100644 --- a/WordPress/Classes/Services/ReaderTopicService.m +++ b/WordPress/Classes/Services/ReaderTopicService.m @@ -51,14 +51,7 @@ - (void)fetchFollowedSitesWithSuccess:(void(^)(void))success failure:(void(^)(NS { ReaderTopicServiceRemote *service = [[ReaderTopicServiceRemote alloc] initWithWordPressComRestApi:[self apiForRequest]]; [service fetchFollowedSitesWithSuccess:^(NSArray *sites) { - for (RemoteReaderSiteInfo *siteInfo in sites) { - [self siteTopicForRemoteSiteInfo:siteInfo]; - } - [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{ - if (success) { - success(); - } - }]; + [self mergeFollowedSites:sites withSuccess:success]; } failure:^(NSError *error) { if (failure) { failure(error); @@ -925,6 +918,39 @@ - (NSString *)formatTitle:(NSString *)str return [title capitalizedStringWithLocale:[NSLocale currentLocale]]; } +/** +Saves the specified `ReaderSiteTopics`. Any `ReaderSiteTopics` not included in the passed +array are marked as being unfollowed in Core Data. + +@param topics An array of `ReaderSiteTopics` to save. +*/ +- (void)mergeFollowedSites:(NSArray *)sites withSuccess:(void (^)(void))success +{ + [self.managedObjectContext performBlock:^{ + NSArray *currentSiteTopics = [self allSiteTopics]; + NSMutableArray *remoteFeedIds = [NSMutableArray array]; + + for (RemoteReaderSiteInfo *siteInfo in sites) { + [remoteFeedIds addObject:siteInfo.feedID]; + [self siteTopicForRemoteSiteInfo:siteInfo]; + } + + for (ReaderSiteTopic *siteTopic in currentSiteTopics) { + // If a site fetched from Core Data isn't included in the list of sites + // fetched from remote, that means it's no longer being followed. + if (![remoteFeedIds containsObject:siteTopic.feedID]) { + siteTopic.following = NO; + } + } + + [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{ + if (success) { + success(); + } + }]; + }]; +} + /** Saves the specified `ReaderAbstractTopics`. Any `ReaderAbstractTopics` not included in the passed array are removed from Core Data. diff --git a/WordPress/WordPressTest/ReaderTopicServiceTest.swift b/WordPress/WordPressTest/ReaderTopicServiceTest.swift index 11a9ff54b43c..0c0a3af611bc 100644 --- a/WordPress/WordPressTest/ReaderTopicServiceTest.swift +++ b/WordPress/WordPressTest/ReaderTopicServiceTest.swift @@ -95,6 +95,25 @@ final class ReaderTopicSwiftTest: XCTestCase { } } + func remoteSiteInfoForTests() -> [RemoteReaderSiteInfo] { + let foo = RemoteReaderSiteInfo() + foo.feedID = 1 + foo.isFollowing = true + foo.postsEndpoint = "/sites/foo" + + let bar = RemoteReaderSiteInfo() + bar.feedID = 2 + bar.isFollowing = true + bar.postsEndpoint = "/sites/bar" + + let baz = RemoteReaderSiteInfo() + baz.feedID = 3 + baz.isFollowing = true + baz.postsEndpoint = "/sites/baz" + + return [foo, bar, baz] + } + func remoteTopicsForTests() -> [RemoteReaderTopic] { let foo = RemoteReaderTopic() foo.topicID = 1 @@ -138,6 +157,48 @@ final class ReaderTopicSwiftTest: XCTestCase { // MARK: Tests + /** + Ensure that followed sites a user unfollows from are set to unfollowed in core data when merging + results from the REST API. + */ + func testUnfollowedSiteIsUnfollowedDuringSync() { + // Arrange: Setup + let remoteSites = remoteSiteInfoForTests() + let service = ReaderTopicService(managedObjectContext: context!) + let foo = remoteSites.first as RemoteReaderSiteInfo? + + // Act: Save sites + var expect = expectation(description: "sites saved expectation") + service.mergeFollowedSites(remoteSites, withSuccess: { () -> Void in + expect.fulfill() + }) + waitForExpectations(timeout: expectationTimeout, handler: nil) + + // Assert: Sites exist in the context + let request = NSFetchRequest(entityName: ReaderSiteTopic.classNameWithoutNamespaces()) + request.predicate = NSPredicate(format: "following = YES") + var count = try! context!.count(for: request) + XCTAssertEqual(count, remoteSites.count, "Number of sites in context did not match expectations") + + // Act: Merge new set of sites + expect = expectation(description: "sites saved expectation") + service.mergeFollowedSites([foo!], withSuccess: { () -> Void in + expect.fulfill() + }) + waitForExpectations(timeout: expectationTimeout, handler: nil) + + // Assert: Unfollowed sites were unfollowed when merged + count = try! context!.count(for: request) + XCTAssertEqual(count, 1, "Number of sites in context did not match expectations") + do { + let results = try context!.fetch(request) + let site = results.first as! ReaderSiteTopic + XCTAssertEqual(site.feedID, foo?.feedID, "The site returned was not the one expected.") + } catch let error as NSError { + XCTAssertNil(error, "Error executing fetch request.") + } + } + /** Ensure that topics a user unsubscribes from are removed from core data when merging results from the REST API.