Fetch article contents from Feedly by ids rather than paging through streams up until the last successful sync date. Issues #1453 #1398 #1408 #1429

This commit is contained in:
Kiel Gillard 2020-01-09 16:24:47 +11:00
parent 9a3763f57a
commit 745b5d8cb8
31 changed files with 778 additions and 1908 deletions

View File

@ -689,7 +689,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
database.fetchStarredArticleIDsAsync(webFeedIDs: flattenedWebFeeds().webFeedIDs(), completion: completion)
}
/// Fetch articleIDs for articles that we should have, but dont. These articles are not userDeleted, and they are either (starred) or (unread and newer than the article cutoff date).
/// Fetch articleIDs for articles that we should have, but dont. These articles are not userDeleted, and they are either (starred) or (newer than the article cutoff date).
public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
}

View File

@ -99,7 +99,6 @@
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */; };
9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */; };
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */; };
9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */; };
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */; };
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */; };
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */; };
@ -113,15 +112,14 @@
9E1FF8642368EC2400834C24 /* FeedlySyncAllOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */; };
9E1FF8662368ED7E00834C24 /* TestMarkArticlesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */; };
9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */; };
9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */; };
9E4828F223617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */; };
9E44C90F23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */; };
9E489E8D2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */; };
9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */; };
9E489E93236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */; };
9E5ABE9A236BE6BD00B5DE9F /* feedly-1-initial in Resources */ = {isa = PBXBuildFile; fileRef = 9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */; };
9E5DE60E23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */; };
9E672394236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */; };
9E672396236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */; };
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */; };
9E7299D723505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */; };
9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */; };
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */; };
@ -129,9 +127,8 @@
9E79F7742395C9F00031DB98 /* feedly-add-new-feed in Resources */ = {isa = PBXBuildFile; fileRef = 9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */; };
9E7F88AC235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */; };
9E7F88AE235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */; };
9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */; };
9E84DC472359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */; };
9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */; };
9E85C8E42366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */; };
9E85C8E62366FED600D0F1F7 /* TestGetStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */; };
9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */; };
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */; };
@ -143,6 +140,7 @@
9EA643D3239305680018A28C /* FeedlySearchOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D2239305680018A28C /* FeedlySearchOperation.swift */; };
9EA643D5239306AC0018A28C /* FeedlyFeedsSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */; };
9EA643D923945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */; };
9EAADA1023C93144003A801F /* TestGetEntriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */; };
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; };
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; };
9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */; };
@ -150,7 +148,8 @@
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */; };
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */; };
9EB1D576238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB1D575238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift */; };
9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */; };
9EBD49C023C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */; };
9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */; };
9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */; };
9EC228592362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */; };
9EC2285B23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */; };
@ -173,10 +172,11 @@
9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */; };
9EEAE075235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */; };
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */; };
9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */; };
9EEEF7212355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */; };
9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */; };
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */; };
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */; };
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */; };
9EF35F7A234E830E003AE2AE /* FeedlyCompoundOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */; };
/* End PBXBuildFile section */
@ -322,7 +322,6 @@
9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyTag.swift; sourceTree = "<group>"; };
9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceId.swift; sourceTree = "<group>"; };
9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceIdTests.swift; sourceTree = "<group>"; };
9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperation.swift; sourceTree = "<group>"; };
9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllOperation.swift; sourceTree = "<group>"; };
9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionsOperation.swift; sourceTree = "<group>"; };
9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMirrorCollectionsAsFoldersOperation.swift; sourceTree = "<group>"; };
@ -336,15 +335,14 @@
9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllOperationTests.swift; sourceTree = "<group>"; };
9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMarkArticlesService.swift; sourceTree = "<group>"; };
9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetCollectionsService.swift; sourceTree = "<group>"; };
9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperationTests.swift; sourceTree = "<group>"; };
9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperationTests.swift; sourceTree = "<group>"; };
9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestStreamArticleIdsOperation.swift; sourceTree = "<group>"; };
9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperationTests.swift; sourceTree = "<group>"; };
9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrganiseParsedItemsByFeedOperationTests.swift; sourceTree = "<group>"; };
9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperationTests.swift; sourceTree = "<group>"; };
9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-1-initial"; sourceTree = "<group>"; };
9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFetchIdsForMissingArticlesOperation.swift; sourceTree = "<group>"; };
9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshAccessTokenOperation.swift; sourceTree = "<group>"; };
9E672395236F7E68000BE141 /* OAuthAcessTokenRefreshing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAcessTokenRefreshing.swift; sourceTree = "<group>"; };
9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperation.swift; sourceTree = "<group>"; };
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedToCollectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddFeedToCollectionOperation.swift; sourceTree = "<group>"; };
9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceProviding.swift; sourceTree = "<group>"; };
9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyLogoutOperation.swift; sourceTree = "<group>"; };
@ -352,9 +350,8 @@
9E79F7732395C9EF0031DB98 /* feedly-add-new-feed */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-add-new-feed"; sourceTree = "<group>"; };
9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCreateFeedsForCollectionFoldersOperationTests.swift; sourceTree = "<group>"; };
9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsOperationTests.swift; sourceTree = "<group>"; };
9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncUnreadStatusesOperation.swift; sourceTree = "<group>"; };
9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestUnreadArticleIdsOperation.swift; sourceTree = "<group>"; };
9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCheckpointOperation.swift; sourceTree = "<group>"; };
9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperationTests.swift; sourceTree = "<group>"; };
9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetStreamContentsService.swift; sourceTree = "<group>"; };
9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamContentsService.swift; sourceTree = "<group>"; };
9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesOperation.swift; sourceTree = "<group>"; };
@ -366,6 +363,7 @@
9EA643D2239305680018A28C /* FeedlySearchOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySearchOperation.swift; sourceTree = "<group>"; };
9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedsSearchResponse.swift; sourceTree = "<group>"; };
9EA643D823945CE00018A28C /* FeedlyAddNewFeedOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddNewFeedOperationTests.swift; sourceTree = "<group>"; };
9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetEntriesService.swift; sourceTree = "<group>"; };
9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCollection.swift; sourceTree = "<group>"; };
9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeed.swift; sourceTree = "<group>"; };
9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntry.swift; sourceTree = "<group>"; };
@ -373,7 +371,8 @@
9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCategory.swift; sourceTree = "<group>"; };
9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrigin.swift; sourceTree = "<group>"; };
9EB1D575238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddNewFeedOperation.swift; sourceTree = "<group>"; };
9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperationTests.swift; sourceTree = "<group>"; };
9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyDownloadArticlesOperation.swift; sourceTree = "<group>"; };
9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntryIdentifierProviding.swift; sourceTree = "<group>"; };
9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCheckpointOperationTests.swift; sourceTree = "<group>"; };
9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperationTests.swift; sourceTree = "<group>"; };
9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStreamContentsOperationTests.swift; sourceTree = "<group>"; };
@ -396,10 +395,11 @@
9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsService.swift; sourceTree = "<group>"; };
9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMarkArticlesService.swift; sourceTree = "<group>"; };
9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySendArticleStatusesOperation.swift; sourceTree = "<group>"; };
9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStarredArticlesOperation.swift; sourceTree = "<group>"; };
9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyIngestStarredArticleIdsOperation.swift; sourceTree = "<group>"; };
9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStreamContentsOperation.swift; sourceTree = "<group>"; };
9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamIdsOperation.swift; sourceTree = "<group>"; };
9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStreamIds.swift; sourceTree = "<group>"; };
9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetUpdatedArticleIdsOperation.swift; sourceTree = "<group>"; };
9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCompoundOperation.swift; sourceTree = "<group>"; };
D511EEB5202422BB00712EC3 /* Account_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_debug.xcconfig; sourceTree = "<group>"; };
D511EEB6202422BB00712EC3 /* Account_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_target.xcconfig; sourceTree = "<group>"; };
@ -648,6 +648,7 @@
9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */,
9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */,
9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */,
9EAADA0F23C93144003A801F /* TestGetEntriesService.swift */,
9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */,
9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */,
9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */,
@ -657,13 +658,9 @@
9E489E8C2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift */,
9E489E902360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift */,
9E489E92236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift */,
9E4828F123617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift */,
9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */,
9EC228562362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift */,
9EC228582362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift */,
9EC2285A23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift */,
9E85C8E32366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift */,
9E3CFFFC2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift */,
9E1FF8632368EC2400834C24 /* FeedlySyncAllOperationTests.swift */,
9EC804E2236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift */,
9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */,
@ -718,16 +715,18 @@
9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */,
9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */,
9E85C8E9236700AD00D0F1F7 /* FeedlyGetEntriesOperation.swift */,
9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */,
9E1AF38A2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift */,
9EEEF71E23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift */,
9E84DC482359A73600D6E809 /* FeedlyCheckpointOperation.swift */,
9EF1B10223584B4C000A486A /* FeedlySyncStreamContentsOperation.swift */,
9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift */,
9E84DC462359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift */,
9E44C90E23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift */,
9EEEF7202355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift */,
9E84DC462359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift */,
9EF2602B23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift */,
9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */,
9E672393236F7CA0000BE141 /* FeedlyRefreshAccessTokenOperation.swift */,
9E784EBD237E890600099B1B /* FeedlyLogoutOperation.swift */,
9E5DE60D23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift */,
9EBD49BF23C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
@ -747,6 +746,7 @@
9E1773D22345700E0056A5A8 /* FeedlyLink.swift */,
9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */,
9EA643D4239306AC0018A28C /* FeedlyFeedsSearchResponse.swift */,
9EBD49C123C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift */,
);
path = Models;
sourceTree = "<group>";
@ -1000,12 +1000,12 @@
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
511B9804237CD4270028BCAA /* FeedIdentifier.swift in Sources */,
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */,
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */,
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */,
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */,
9EBD49C223C67784005AD5CD /* FeedlyEntryIdentifierProviding.swift in Sources */,
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */,
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
@ -1015,6 +1015,7 @@
9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */,
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */,
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */,
9E44C90F23C6FF3600CCC286 /* FeedlyIngestStreamArticleIdsOperation.swift in Sources */,
844B297D2106C7EC004020B3 /* WebFeed.swift in Sources */,
3B826DA72385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift in Sources */,
9E964EBA23754B4000A7AF2E /* OAuthAccountAuthorizationOperation.swift in Sources */,
@ -1031,10 +1032,10 @@
9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */,
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */,
84B2D4D02238CD8A00498ADA /* WebFeedMetadata.swift in Sources */,
9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */,
9E84DC472359A23200D6E809 /* FeedlyIngestUnreadArticleIdsOperation.swift in Sources */,
9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */,
9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */,
9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */,
9EEEF7212355277F009E9D80 /* FeedlyIngestStarredArticleIdsOperation.swift in Sources */,
3BC23AB92385ECB100371CBA /* FeedWranglerSubscriptionResult.swift in Sources */,
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
@ -1044,6 +1045,7 @@
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */,
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */,
9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */,
9E5DE60E23C3F4B70064DA30 /* FeedlyFetchIdsForMissingArticlesOperation.swift in Sources */,
3B826DA92385C81C00FC1ADB /* FeedWranglerAPICaller.swift in Sources */,
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */,
51E3EB41229AF61B00645299 /* AccountError.swift in Sources */,
@ -1052,6 +1054,7 @@
552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */,
552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */,
5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */,
9EBD49C023C67602005AD5CD /* FeedlyDownloadArticlesOperation.swift in Sources */,
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */,
9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */,
552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */,
@ -1079,10 +1082,10 @@
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */,
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */,
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */,
9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */,
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
9EF1B10723590D61000A486A /* FeedlyGetStreamIdsOperation.swift in Sources */,
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */,
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */,
3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */,
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
);
@ -1094,9 +1097,8 @@
files = (
9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */,
9E03C122235E62E100FB6D9E /* FeedlyTestSupport.swift in Sources */,
9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */,
9EAADA1023C93144003A801F /* TestGetEntriesService.swift in Sources */,
9E784EC0237E8BE100099B1B /* FeedlyLogoutOperationTests.swift in Sources */,
9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */,
9E03C120235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift in Sources */,
9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */,
9E0260CB236FF99A00D122D3 /* FeedlyRefreshAccessTokenOperationTests.swift in Sources */,
@ -1117,10 +1119,8 @@
51D5875E227F643C00900287 /* AccountFeedbinFolderSyncTest.swift in Sources */,
9EC804E3236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift in Sources */,
9E1FF8682368EE4900834C24 /* TestGetCollectionsService.swift in Sources */,
9E4828F223617F4A00D68691 /* FeedlySetUnreadArticlesOperationTests.swift in Sources */,
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */,
513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */,
9E85C8E42366FE0100D0F1F7 /* FeedlySyncStarredArticlesOperationTests.swift in Sources */,
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */,
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */,
9EC2285B23639A6500766EF8 /* FeedlySyncStreamContentsOperationTests.swift in Sources */,

View File

@ -28,6 +28,7 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
}
struct TestParsedItemsProvider: FeedlyParsedItemProviding {
let parsedItemProviderName = "TestParsedItemsProvider"
var resource: FeedlyResourceId
var parsedEntries: Set<ParsedItem>
}
@ -51,7 +52,6 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
XCTAssertEqual(itemsAndFeedIds, entries)
XCTAssertEqual(resource.id, organise.providerName)
}
func testGroupsOneEntryByFeedId() {
@ -73,7 +73,6 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
XCTAssertEqual(itemsAndFeedIds, entries)
XCTAssertEqual(resource.id, organise.providerName)
}
func testGroupsManyEntriesByFeedId() {
@ -95,6 +94,5 @@ class FeedlyOrganiseParsedItemsByFeedOperationTests: XCTestCase {
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
XCTAssertEqual(itemsAndFeedIds, entries)
XCTAssertEqual(resource.id, organise.providerName)
}
}

View File

@ -1,489 +0,0 @@
//
// FeedlySetStarredArticlesOperationTests.swift
// AccountTests
//
// Created by Kiel Gillard on 25/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
import RSParser
class FeedlySetStarredArticlesOperationTests: XCTestCase {
private var account: Account!
private let support = FeedlyTestSupport()
override func setUp() {
super.setUp()
account = support.makeTestAccount()
}
override func tearDown() {
if let account = account {
support.destroy(account)
}
super.tearDown()
}
// MARK: - Ensuring Unread Status By Id
struct TestStarredArticleProvider: FeedlyStarredEntryIdProviding {
var entryIds: Set<String>
}
func testEmptyArticleIds() {
let testIds = Set<String>()
let provider = TestStarredArticleProvider(entryIds: testIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertTrue(accountArticlesIDs.isEmpty)
XCTAssertEqual(accountArticlesIDs, testIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetOneArticleIdStarred() {
let testIds = Set<String>(["feed/0/article/0"])
let provider = TestStarredArticleProvider(entryIds: testIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetManyArticleIdsStarred() {
let testIds = Set<String>((0..<10_000).map { "feed/0/article/\($0)" })
let provider = TestStarredArticleProvider(entryIds: testIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetSomeArticleIdsUnstarred() {
let initialStarredIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
do {
let provider = TestStarredArticleProvider(entryIds: initialStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
}
let remainingStarredIds = Set(initialStarredIds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { remainingAccountArticlesIDsResult in
do {
let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get()
XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetAllArticleIdsUnstarred() {
let initialStarredIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
do {
let provider = TestStarredArticleProvider(entryIds: initialStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
}
let remainingStarredIds = Set<String>()
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { remainingAccountArticlesIDsResult in
do {
let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get()
XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
// MARK: - Updating Article Unread Status
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
var providerName: String
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
}
func testSetAllArticlesStarred() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
let remainingStarredIds = Set(testItems.compactMap { $0.syncServiceID })
XCTAssertEqual(testItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
let idsOfStarredArticles = Set(try self.account
.fetchArticles(.articleIDs(remainingStarredIds))
.filter { $0.status.boolStatus(forKey: .starred) == true }
.map { $0.articleID })
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetManyArticlesUnread() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
let unreadItems = testItems
.enumerated()
.filter { $0.offset % 2 > 0 }
.map { $0.element }
let remainingStarredIds = Set(unreadItems.compactMap { $0.syncServiceID })
XCTAssertEqual(unreadItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
let idsOfStarredArticles = Set(try self.account
.fetchArticles(.articleIDs(remainingStarredIds))
.filter { $0.status.boolStatus(forKey: .starred) == true }
.map { $0.articleID })
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetOneArticleUnread() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
// Since the test data is completely under the developer's control, not having at least one can be a programmer error.
let remainingStarredIds = Set([testItems.compactMap { $0.syncServiceID }.first!])
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
let idsOfStarredArticles = Set(try self.account
.fetchArticles(.articleIDs(remainingStarredIds))
.filter { $0.status.boolStatus(forKey: .starred) == true }
.map { $0.articleID })
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetNoArticlesRead() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let remainingStarredIds = Set<String>()
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
let idsOfStarredArticles = Set(try self.account
.fetchArticles(.articleIDs(remainingStarredIds))
.filter { $0.status.boolStatus(forKey: .starred) == true }
.map { $0.articleID })
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetAllArticlesAndArticleIdsWithSomeArticlesIngested() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
let someItemsAndFeeds = Dictionary(uniqueKeysWithValues: testItemsAndFeeds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: someItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
let remainingStarredIds = Set(testItems.compactMap { $0.syncServiceID })
XCTAssertEqual(testItems.count, remainingStarredIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
let provider = TestStarredArticleProvider(entryIds: remainingStarredIds)
let setStarred = FeedlySetStarredArticlesOperation(account: account, allStarredEntryIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value })
let someRemainingStarredIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID })
let idsOfStarredArticles = Set(try self.account
.fetchArticles(.articleIDs(someRemainingStarredIdsOfIngestedArticles))
.filter { $0.status.boolStatus(forKey: .starred) == true }
.map { $0.articleID })
XCTAssertEqual(idsOfStarredArticles, someRemainingStarredIdsOfIngestedArticles)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking articles IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
}

View File

@ -1,485 +0,0 @@
//
// FeedlySetUnreadArticlesOperationTests.swift
// AccountTests
//
// Created by Kiel Gillard on 24/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
import RSParser
class FeedlySetUnreadArticlesOperationTests: XCTestCase {
private var account: Account!
private let support = FeedlyTestSupport()
override func setUp() {
super.setUp()
account = support.makeTestAccount()
}
override func tearDown() {
if let account = account {
support.destroy(account)
}
super.tearDown()
}
// MARK: - Ensuring Unread Status By Id
struct TestUnreadArticleIdProvider: FeedlyUnreadEntryIdProviding {
var entryIds: Set<String>
}
func testEmptyArticleIds() {
let testIds = Set<String>()
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertTrue(accountArticlesIDs.isEmpty)
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetOneArticleIdUnread() {
let testIds = Set<String>(["feed/0/article/0"])
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetManyArticleIdsUnread() {
let testIds = Set<String>((0..<10_000).map { "feed/0/article/\($0)" })
let provider = TestUnreadArticleIdProvider(entryIds: testIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetSomeArticleIdsRead() {
let initialUnreadIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
do {
let provider = TestUnreadArticleIdProvider(entryIds: initialUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
}
let remainingUnreadIds = Set(initialUnreadIds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { remainingAccountArticlesIDsResult in
do {
let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get()
XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetAllArticleIdsRead() {
let initialUnreadIds = Set<String>((0..<1000).map { "feed/0/article/\($0)" })
do {
let provider = TestUnreadArticleIdProvider(entryIds: initialUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish Setting Initial Unreads")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
}
let remainingUnreadIds = Set<String>()
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { remainingAccountArticlesIDsResult in
do {
let remainingAccountArticlesIDs = try remainingAccountArticlesIDsResult.get()
XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
waitForExpectations(timeout: 2)
}
// MARK: - Updating Article Unread Status
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
var providerName: String
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
}
func testSetAllArticlesUnread() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
let remainingUnreadIds = Set(testItems.compactMap { $0.syncServiceID })
XCTAssertEqual(testItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
let idsOfUnreadArticles = Set(try self.account
.fetchArticles(.articleIDs(remainingUnreadIds))
.filter { $0.status.boolStatus(forKey: .read) == false }
.map { $0.articleID })
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetManyArticlesUnread() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
let unreadItems = testItems
.enumerated()
.filter { $0.offset % 2 > 0 }
.map { $0.element }
let remainingUnreadIds = Set(unreadItems.compactMap { $0.syncServiceID })
XCTAssertEqual(unreadItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
let idsOfUnreadArticles = Set(try self.account
.fetchArticles(.articleIDs(remainingUnreadIds))
.filter { $0.status.boolStatus(forKey: .read) == false }
.map { $0.articleID })
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
}
func testSetOneArticleUnread() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
// Since the test data is completely under the developer's control, not having at least one can be a programmer error.
let remainingUnreadIds = Set([testItems.compactMap { $0.syncServiceID }.first!])
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
let idsOfUnreadArticles = Set(try self.account
.fetchArticles(.articleIDs(remainingUnreadIds))
.filter { $0.status.boolStatus(forKey: .read) == false }
.map { $0.articleID })
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testSetNoArticlesRead() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let remainingUnreadIds = Set<String>()
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
let idsOfUnreadArticles = Set(try self.account
.fetchArticles(.articleIDs(remainingUnreadIds))
.filter { $0.status.boolStatus(forKey: .read) == false }
.map { $0.articleID })
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
}
func testSetAllArticlesAndArticleIdsWithSomeArticlesIngested() {
let testItemsAndFeeds = support.makeParsedItemTestDataFor(numberOfFeeds: 5, numberOfItemsInFeeds: 100)
let someItemsAndFeeds = Dictionary(uniqueKeysWithValues: testItemsAndFeeds.enumerated().filter { $0.offset % 2 > 0 }.map { $0.element })
do {
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: someItemsAndFeeds)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
update.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(update)
waitForExpectations(timeout: 2)
}
let testItems = Set(testItemsAndFeeds.flatMap { $0.value })
let remainingUnreadIds = Set(testItems.compactMap { $0.syncServiceID })
XCTAssertEqual(testItems.count, remainingUnreadIds.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
let provider = TestUnreadArticleIdProvider(entryIds: remainingUnreadIds)
let setUnread = FeedlySetUnreadArticlesOperation(account: account, allUnreadIdsProvider: provider, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
setUnread.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(setUnread)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetched Articles Ids")
account.fetchUnreadArticleIDs { accountArticlesIDsResult in
do {
let accountArticlesIDs = try accountArticlesIDsResult.get()
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value })
let someRemainingUnreadIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID })
let idsOfUnreadArticles = Set(try self.account
.fetchArticles(.articleIDs(someRemainingUnreadIdsOfIngestedArticles))
.filter { $0.status.boolStatus(forKey: .read) == false }
.map { $0.articleID })
XCTAssertEqual(idsOfUnreadArticles, someRemainingUnreadIdsOfIngestedArticles)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking account articles IDs result: \(error)")
}
}
}
}

View File

@ -57,25 +57,31 @@ class FeedlySyncAllOperationTests: XCTestCase {
getGlobalStreamContents.getStreamContentsExpectation = expectation(description: "Get Contents of global.all")
getGlobalStreamContents.getStreamContentsExpectation?.isInverted = true
let getStarredContents = TestGetStreamContentsService()
getStarredContents.getStreamContentsExpectation = expectation(description: "Get Contents of global.saved")
getStarredContents.getStreamContentsExpectation?.isInverted = true
let getStarredIds = TestGetStreamIdsService()
getStarredIds.getStreamIdsExpectation = expectation(description: "Get Ids of global.saved")
getStarredIds.getStreamIdsExpectation?.isInverted = true
let getEntriesService = TestGetEntriesService()
getEntriesService.getEntriesExpectation = expectation(description: "Get Entries")
getEntriesService.getEntriesExpectation?.isInverted = true
let progress = DownloadProgress(numberOfTasks: 0)
let _ = expectationForCompletion(of: progress)
let container = support.makeTestDatabaseContainer()
let syncAll = FeedlySyncAllOperation(account: account,
credentials: support.accessToken,
lastSuccessfulFetchStartDate: nil,
markArticlesService: markArticlesService,
getUnreadService: getStreamIdsService,
getCollectionsService: getCollectionsService,
getStreamContentsService: getGlobalStreamContents,
getStarredArticlesService: getStarredContents,
database: container.database,
downloadProgress: progress,
log: support.log)
credentials: support.accessToken,
lastSuccessfulFetchStartDate: nil,
markArticlesService: markArticlesService,
getUnreadService: getStreamIdsService,
getCollectionsService: getCollectionsService,
getStreamContentsService: getGlobalStreamContents,
getStarredService: getStarredIds,
getStreamIdsService: getStreamIdsService,
getEntriesService: getEntriesService,
database: container.database,
downloadProgress: progress,
log: support.log)
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
let completionExpectation = expectation(description: "Did Finish")

View File

@ -1,189 +0,0 @@
//
// FeedlySyncStarredArticlesOperationTests.swift
// AccountTests
//
// Created by Kiel Gillard on 28/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class FeedlySyncStarredArticlesOperationTests: XCTestCase {
private var account: Account!
private let support = FeedlyTestSupport()
override func setUp() {
super.setUp()
account = support.makeTestAccount()
}
override func tearDown() {
if let account = account {
support.destroy(account)
}
super.tearDown()
}
func testIngestsOnePageSuccess() {
let service = TestGetStreamContentsService()
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
let items = service.makeMockFeedlyEntryItem()
service.mockResult = .success(FeedlyStream(id: resource.id, updated: nil, continuation: nil, items: items))
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
getStreamContentsExpectation.expectedFulfillmentCount = 1
service.getStreamContentsExpectation = getStreamContentsExpectation
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
XCTAssertEqual(serviceResource.id, resource.id)
XCTAssertNil(serviceNewerThan)
XCTAssertNil(continuation)
XCTAssertNil(serviceUnreadOnly)
}
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
syncStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(syncStarred)
waitForExpectations(timeout: 2)
let expectedArticleIds = Set(items.map { $0.id })
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { starredArticleIdsResult in
do {
let starredArticleIds = try starredArticleIdsResult.get()
let missingIds = expectedArticleIds.subtracting(starredArticleIds)
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as starred.")
// Fetch articles directly because account.fetchArticles(.starred) fetches starred articles for feeds subscribed to.
let expectedArticles = try self.account.fetchArticles(.articleIDs(expectedArticleIds))
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
let starredArticles = try self.account.fetchArticles(.articleIDs(starredArticleIds))
XCTAssertEqual(expectedArticleIds.count, expectedArticles.count)
let missingArticles = expectedArticles.subtracting(starredArticles)
XCTAssertTrue(missingArticles.isEmpty, "These articles should be starred and fetched.")
XCTAssertEqual(expectedArticles, starredArticles)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking starred article IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testIngestsOnePageFailure() {
let service = TestGetStreamContentsService()
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
service.mockResult = .failure(URLError(.timedOut))
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
getStreamContentsExpectation.expectedFulfillmentCount = 1
service.getStreamContentsExpectation = getStreamContentsExpectation
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
XCTAssertEqual(serviceResource.id, resource.id)
XCTAssertNil(serviceNewerThan)
XCTAssertNil(continuation)
XCTAssertNil(serviceUnreadOnly)
}
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
syncStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(syncStarred)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { starredArticleIdsResult in
do {
let starredArticleIds = try starredArticleIdsResult.get()
XCTAssertTrue(starredArticleIds.isEmpty)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking starred article IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testIngestsManyPagesSuccess() {
let service = TestGetPagedStreamContentsService()
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
let continuations = (1...10).map { "\($0)" }
service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 10)
let getStreamContentsExpectation = expectation(description: "Did Get Page of Stream Contents")
getStreamContentsExpectation.expectedFulfillmentCount = 1 + continuations.count
var remainingContinuations = Set(continuations)
let getStreamPageExpectation = expectation(description: "Did Request Page")
getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count
service.getStreamContentsExpectation = getStreamContentsExpectation
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
XCTAssertEqual(serviceResource.id, resource.id)
XCTAssertNil(serviceNewerThan)
XCTAssertNil(serviceUnreadOnly)
if let continuation = continuation {
XCTAssertTrue(remainingContinuations.contains(continuation))
remainingContinuations.remove(continuation)
}
getStreamPageExpectation.fulfill()
}
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, resource: resource, service: service, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
syncStarred.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(syncStarred)
waitForExpectations(timeout: 2)
// Find articles inserted.
let expectedArticleIds = Set(service.pages.values.map { $0.items }.flatMap { $0 }.map { $0.id })
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchStarredArticleIDs { starredArticleIdsResult in
do {
let starredArticleIds = try starredArticleIdsResult.get()
let missingIds = expectedArticleIds.subtracting(starredArticleIds)
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as starred.")
// Fetch articles directly because account.fetchArticles(.starred) fetches starred articles for feeds subscribed to.
let expectedArticles = try self.account.fetchArticles(.articleIDs(expectedArticleIds))
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
let starredArticles = try self.account.fetchArticles(.articleIDs(starredArticleIds))
XCTAssertEqual(expectedArticleIds.count, expectedArticles.count)
let missingArticles = expectedArticles.subtracting(starredArticles)
XCTAssertTrue(missingArticles.isEmpty, "These articles should be starred and fetched.")
XCTAssertEqual(expectedArticles, starredArticles)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking starred article IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
}

View File

@ -1,167 +0,0 @@
//
// FeedlySyncUnreadStatusesOperationTests.swift
// AccountTests
//
// Created by Kiel Gillard on 29/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
class FeedlySyncUnreadStatusesOperationTests: XCTestCase {
private var account: Account!
private let support = FeedlyTestSupport()
override func setUp() {
super.setUp()
account = support.makeTestAccount()
}
override func tearDown() {
if let account = account {
support.destroy(account)
}
super.tearDown()
}
func testIngestsOnePageSuccess() {
let service = TestGetStreamIdsService()
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
let ids = [UUID().uuidString]
service.mockResult = .success(FeedlyStreamIds(continuation: nil, ids: ids))
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Ids")
getStreamIdsExpectation.expectedFulfillmentCount = 1
service.getStreamIdsExpectation = getStreamIdsExpectation
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
XCTAssertEqual(serviceResource.id, resource.id)
XCTAssertNil(serviceNewerThan)
XCTAssertNil(continuation)
XCTAssertEqual(serviceUnreadOnly, true)
}
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
syncUnreads.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(syncUnreads)
waitForExpectations(timeout: 2)
let expectedArticleIds = Set(ids)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchUnreadArticleIDs { unreadArticleIdsResult in
do {
let unreadArticleIds = try unreadArticleIdsResult.get()
let missingIds = expectedArticleIds.subtracting(unreadArticleIds)
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.")
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking unread article IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testIngestsOnePageFailure() {
let service = TestGetStreamIdsService()
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
service.mockResult = .failure(URLError(.timedOut))
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Contents")
getStreamIdsExpectation.expectedFulfillmentCount = 1
service.getStreamIdsExpectation = getStreamIdsExpectation
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
XCTAssertEqual(serviceResource.id, resource.id)
XCTAssertNil(serviceNewerThan)
XCTAssertNil(continuation)
XCTAssertEqual(serviceUnreadOnly, true)
}
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
syncUnreads.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(syncUnreads)
waitForExpectations(timeout: 2)
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchUnreadArticleIDs { unreadArticleIdsResult in
do {
let unreadArticleIds = try unreadArticleIdsResult.get()
XCTAssertTrue(unreadArticleIds.isEmpty)
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking unread article IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
func testIngestsManyPagesSuccess() {
let service = TestGetPagedStreamIdsService()
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
let continuations = (1...10).map { "\($0)" }
service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 1000)
let getStreamIdsExpectation = expectation(description: "Did Get Page of Stream Contents")
getStreamIdsExpectation.expectedFulfillmentCount = 1 + continuations.count
var remainingContinuations = Set(continuations)
let getStreamPageExpectation = expectation(description: "Did Request Page")
getStreamPageExpectation.expectedFulfillmentCount = 1 + continuations.count
service.getStreamIdsExpectation = getStreamIdsExpectation
service.parameterTester = { serviceResource, continuation, serviceNewerThan, serviceUnreadOnly in
XCTAssertEqual(serviceResource.id, resource.id)
XCTAssertNil(serviceNewerThan)
XCTAssertEqual(serviceUnreadOnly, true)
if let continuation = continuation {
XCTAssertTrue(remainingContinuations.contains(continuation))
remainingContinuations.remove(continuation)
}
getStreamPageExpectation.fulfill()
}
let syncUnreads = FeedlySyncUnreadStatusesOperation(account: account, resource: resource, service: service, newerThan: nil, log: support.log)
let completionExpectation = expectation(description: "Did Finish")
syncUnreads.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(syncUnreads)
waitForExpectations(timeout: 2)
// Find statuses inserted.
let expectedArticleIds = Set(service.pages.values.map { $0.ids }.flatMap { $0 })
let fetchIdsExpectation = expectation(description: "Fetch Article Ids")
account.fetchUnreadArticleIDs { unreadArticleIdsResult in
do {
let unreadArticleIds = try unreadArticleIdsResult.get()
let missingIds = expectedArticleIds.subtracting(unreadArticleIds)
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.")
fetchIdsExpectation.fulfill()
} catch {
XCTFail("Error checking unread article IDs: \(error)")
}
}
waitForExpectations(timeout: 2)
}
}

View File

@ -28,14 +28,14 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
}
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
var providerName: String
var parsedItemsByFeedProviderName: String
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
}
func testUpdateAccountWithEmptyItems() throws {
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 0, numberOfItemsInFeeds: 0)
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
@ -59,7 +59,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
func testUpdateAccountWithOneItem() throws {
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
@ -87,7 +87,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
func testUpdateAccountWithManyItems() throws {
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 100, numberOfItemsInFeeds: 100)
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
@ -115,7 +115,7 @@ class FeedlyUpdateAccountFeedsWithItemsOperationTests: XCTestCase {
func testCancelUpdateAccount() throws {
let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
let provider = TestItemsByFeedProvider(parsedItemsByFeedProviderName: resource.id, parsedItemsKeyedByFeedId: testItems)
let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)

View File

@ -0,0 +1,26 @@
//
// TestGetEntriesService.swift
// AccountTests
//
// Created by Kiel Gillard on 11/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
final class TestGetEntriesService: FeedlyGetEntriesService {
var mockResult: Result<[FeedlyEntry], Error>?
var getEntriesExpectation: XCTestExpectation?
func getEntries(for ids: Set<String>, completion: @escaping (Result<[FeedlyEntry], Error>) -> ()) {
guard let result = mockResult else {
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
return
}
DispatchQueue.main.async {
completion(result)
self.getEntriesExpectation?.fulfill()
}
}
}

View File

@ -637,7 +637,7 @@ extension FeedlyAPICaller: FeedlyGetStreamIdsService {
}
queryItems.append(contentsOf: [
URLQueryItem(name: "count", value: "1000"),
URLQueryItem(name: "count", value: "10000"),
URLQueryItem(name: "streamId", value: resource.id),
])

View File

@ -147,10 +147,6 @@ final class FeedlyAccountDelegate: AccountDelegate {
/// So if the user is using another client roughly simultaneously with this app,
/// this app does its part to ensure the articles have a consistent status between both.
///
/// Feedly has no API that allows the app to fetch the identifiers of unread articles only.
/// The only way to identify unread articles is to pull all of the article data,
/// which is effectively equivalent of a full refresh.
///
/// - Parameter account: The account whose articles have a remote status.
/// - Parameter completion: Call on the main queue.
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
@ -160,18 +156,18 @@ final class FeedlyAccountDelegate: AccountDelegate {
let group = DispatchGroup()
let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log)
let ingestUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log)
group.enter()
syncUnread.completionBlock = {
ingestUnread.completionBlock = {
group.leave()
}
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: caller, log: log)
let ingestStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log)
group.enter()
syncStarred.completionBlock = {
ingestStarred.completionBlock = {
group.leave()
}
@ -179,7 +175,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
completion(.success(()))
}
operationQueue.addOperations([syncUnread, syncStarred], waitUntilFinished: false)
operationQueue.addOperations([ingestUnread, ingestStarred], waitUntilFinished: false)
}
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {

View File

@ -0,0 +1,29 @@
//
// FeedlyEntryIdentifierProviding.swift
// Account
//
// Created by Kiel Gillard on 9/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
protocol FeedlyEntryIdentifierProviding: class {
var entryIds: Set<String> { get }
}
final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding {
private (set) var entryIds: Set<String>
init(entryIds: Set<String> = Set()) {
self.entryIds = entryIds
}
func addEntryIds(from provider: FeedlyEntryIdentifierProviding) {
entryIds.formUnion(provider.entryIds)
}
func addEntryIds(in articleIds: [String]) {
entryIds.formUnion(articleIds)
}
}

View File

@ -92,7 +92,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
createFeeds.downloadProgress = downloadProgress
self.operationQueue.addOperation(createFeeds)
let syncUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: syncUnreadIdsService, newerThan: nil, log: log)
let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: syncUnreadIdsService, newerThan: nil, log: log)
syncUnread.addDependency(createFeeds)
syncUnread.downloadProgress = downloadProgress
self.operationQueue.addOperation(syncUnread)

View File

@ -0,0 +1,99 @@
//
// FeedlyDownloadArticlesOperation.swift
// Account
//
// Created by Kiel Gillard on 9/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
class FeedlyDownloadArticlesOperation: FeedlyOperation {
private let account: Account
private let log: OSLog
private let missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding
private let updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding
private let getEntriesService: FeedlyGetEntriesService
private let operationQueue: OperationQueue
private let finishOperation: FeedlyCheckpointOperation
init(account: Account, missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding, updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding, getEntriesService: FeedlyGetEntriesService, log: OSLog) {
self.account = account
self.operationQueue = OperationQueue()
self.operationQueue.isSuspended = true
self.missingArticleEntryIdProvider = missingArticleEntryIdProvider
self.updatedArticleEntryIdProvider = updatedArticleEntryIdProvider
self.getEntriesService = getEntriesService
self.finishOperation = FeedlyCheckpointOperation()
self.log = log
super.init()
self.finishOperation.checkpointDelegate = self
self.operationQueue.addOperation(self.finishOperation)
}
override func cancel() {
os_log(.debug, log: log, "Cancelling %{public}@.", self)
operationQueue.cancelAllOperations()
super.cancel()
didFinish()
}
override func main() {
guard !isCancelled else {
// override of cancel calls didFinish().
return
}
var articleIds = missingArticleEntryIdProvider.entryIds
articleIds.formUnion(updatedArticleEntryIdProvider.entryIds)
os_log(.debug, log: log, "Requesting %{public}i articles.", articleIds.count)
let feedlyAPILimitBatchSize = 1000
for articleIds in Array(articleIds).chunked(into: feedlyAPILimitBatchSize) {
let provider = FeedlyEntryIdentifierProvider(entryIds: Set(articleIds))
let getEntries = FeedlyGetEntriesOperation(account: account, service: getEntriesService, provider: provider, log: log)
getEntries.delegate = self
self.operationQueue.addOperation(getEntries)
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
parsedItemProvider: getEntries,
log: log)
organiseByFeed.delegate = self
organiseByFeed.addDependency(getEntries)
self.operationQueue.addOperation(organiseByFeed)
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
organisedItemsProvider: organiseByFeed,
log: log)
updateAccount.delegate = self
updateAccount.addDependency(organiseByFeed)
self.operationQueue.addOperation(updateAccount)
finishOperation.addDependency(updateAccount)
}
operationQueue.isSuspended = false
}
}
extension FeedlyDownloadArticlesOperation: FeedlyCheckpointOperationDelegate {
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
didFinish()
}
}
extension FeedlyDownloadArticlesOperation: FeedlyOperationDelegate {
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
assert(Thread.isMainThread)
os_log(.debug, log: log, "%{public}@ failed with error: %{public}@.", operation, error as NSError)
cancel()
}
}

View File

@ -0,0 +1,40 @@
//
// FeedlyFetchIdsForMissingArticlesOperation.swift
// Account
//
// Created by Kiel Gillard on 7/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
final class FeedlyFetchIdsForMissingArticlesOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
private let account: Account
private let log: OSLog
private(set) var entryIds = Set<String>()
init(account: Account, log: OSLog) {
self.account = account
self.log = log
}
override func main() {
guard !isCancelled else {
didFinish()
return
}
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
switch result {
case .success(let articleIds):
self.entryIds.formUnion(articleIds)
self.didFinish()
case .failure(let error):
self.didFinish(error)
}
}
}
}

View File

@ -8,15 +8,16 @@
import Foundation
import os.log
import RSParser
/// Single responsibility is to get full entries for the entry identifiers.
final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding {
final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
let account: Account
let service: FeedlyGetEntriesService
let provider: FeedlyEntryIdenifierProviding
let provider: FeedlyEntryIdentifierProviding
let log: OSLog
init(account: Account, service: FeedlyGetEntriesService, provider: FeedlyEntryIdenifierProviding, log: OSLog) {
init(account: Account, service: FeedlyGetEntriesService, provider: FeedlyEntryIdentifierProviding, log: OSLog) {
self.account = account
self.service = service
self.provider = provider
@ -25,6 +26,33 @@ final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding {
private (set) var entries = [FeedlyEntry]()
private var storedParsedEntries: Set<ParsedItem>?
var parsedEntries: Set<ParsedItem> {
if let entries = storedParsedEntries {
return entries
}
let parsed = Set(entries.compactMap {
FeedlyEntryParser(entry: $0).parsedItemRepresentation
})
if parsed.count != entries.count {
let entryIds = Set(entries.map { $0.id })
let parsedIds = Set(parsed.map { $0.uniqueID })
let difference = entryIds.subtracting(parsedIds)
os_log(.debug, log: log, "%{public}@ dropping articles with ids: %{public}@.", self, difference)
}
storedParsedEntries = parsed
return parsed
}
var parsedItemProviderName: String {
return name ?? String(describing: Self.self)
}
override func main() {
guard !isCancelled else {
didFinish()

View File

@ -15,7 +15,7 @@ protocol FeedlyEntryProviding {
}
protocol FeedlyParsedItemProviding {
var resource: FeedlyResourceId { get }
var parsedItemProviderName: String { get }
var parsedEntries: Set<ParsedItem> { get }
}
@ -32,8 +32,8 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid
let resourceProvider: FeedlyResourceProviding
var resource: FeedlyResourceId {
return resourceProvider.resource
var parsedItemProviderName: String {
return resourceProvider.resource.id
}
var entries: [FeedlyEntry] {

View File

@ -9,17 +9,12 @@
import Foundation
import os.log
protocol FeedlyEntryIdenifierProviding: class {
var resource: FeedlyResourceId { get }
var entryIds: Set<String> { get }
}
protocol FeedlyGetStreamIdsOperationDelegate: class {
func feedlyGetStreamIdsOperation(_ operation: FeedlyGetStreamIdsOperation, didGet streamIds: FeedlyStreamIds)
}
/// Single responsibility is to get the stream ids from Feedly.
final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdenifierProviding, FeedlyUnreadEntryIdProviding {
final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
var entryIds: Set<String> {
guard let ids = streamIds?.ids else {

View File

@ -0,0 +1,83 @@
//
// FeedlyGetUpdatedArticleIdsOperation.swift
// Account
//
// Created by Kiel Gillard on 11/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
/// Single responsibility is to identify articles that have changed since a particular date.
///
/// Typically, it pages through the article ids of the global.all stream.
/// When all the article ids are collected, it is the responsibility of another operation to download them when appropriate.
class FeedlyGetUpdatedArticleIdsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
private let account: Account
private let resource: FeedlyResourceId
private let service: FeedlyGetStreamIdsService
private let newerThan: Date?
private let log: OSLog
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
self.account = account
self.resource = resource
self.service = service
self.newerThan = newerThan
self.log = log
}
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
let all = FeedlyCategoryResourceId.Global.all(for: credentials.username)
self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log)
}
var entryIds: Set<String> {
return storedUpdatedArticleIds
}
private var storedUpdatedArticleIds = Set<String>()
override func main() {
guard !isCancelled else {
didFinish()
return
}
getStreamIds(nil)
}
private func getStreamIds(_ continuation: String?) {
guard let date = newerThan else {
os_log(.debug, log: log, "No date provided so everything must be new (nothing is updated).")
didFinish()
return
}
service.getStreamIds(for: resource, continuation: continuation, newerThan: date, unreadOnly: nil, completion: didGetStreamIds(_:))
}
private func didGetStreamIds(_ result: Result<FeedlyStreamIds, Error>) {
guard !isCancelled else {
didFinish()
return
}
switch result {
case .success(let streamIds):
storedUpdatedArticleIds.formUnion(streamIds.ids)
guard let continuation = streamIds.continuation else {
os_log(.debug, log: log, "%{public}i articles updated since last successful sync start date.", storedUpdatedArticleIds.count)
didFinish()
return
}
getStreamIds(continuation)
case .failure(let error):
didFinish(error)
}
}
}

View File

@ -0,0 +1,130 @@
//
// FeedlyIngestStarredArticleIdsOperation.swift
// Account
//
// Created by Kiel Gillard on 15/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
/// Single responsibility is to clone locally the remote starred article state.
///
/// Typically, it pages through the article ids of the global.saved stream.
/// When all the article ids are collected, a status is created for each.
/// The article ids previously marked as starred but not collected become unstarred.
/// So this operation has side effects *for the entire account* it operates on.
final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation {
private let account: Account
private let resource: FeedlyResourceId
private let service: FeedlyGetStreamIdsService
private let entryIdsProvider: FeedlyEntryIdentifierProvider
private let log: OSLog
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
let resource = FeedlyTagResourceId.Global.saved(for: credentials.username)
self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log)
}
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
self.account = account
self.resource = resource
self.service = service
self.entryIdsProvider = FeedlyEntryIdentifierProvider()
self.log = log
}
override func main() {
guard !isCancelled else {
didFinish()
return
}
getStreamIds(nil)
}
private func getStreamIds(_ continuation: String?) {
service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil, completion: didGetStreamIds(_:))
}
private func didGetStreamIds(_ result: Result<FeedlyStreamIds, Error>) {
guard !isCancelled else {
didFinish()
return
}
switch result {
case .success(let streamIds):
entryIdsProvider.addEntryIds(in: streamIds.ids)
guard let continuation = streamIds.continuation else {
updateStarredStatuses()
return
}
getStreamIds(continuation)
case .failure(let error):
didFinish(error)
}
}
private func updateStarredStatuses() {
guard !isCancelled else {
didFinish()
return
}
account.fetchStarredArticleIDs { result in
switch result {
case .success(let localStarredArticleIDs):
self.processStarredArticleIDs(localStarredArticleIDs)
case .failure(let error):
self.didFinish(error)
}
}
}
func processStarredArticleIDs(_ localStarredArticleIDs: Set<String>) {
guard !isCancelled else {
didFinish()
return
}
let remoteStarredArticleIDs = entryIdsProvider.entryIds
let group = DispatchGroup()
final class StarredStatusResults {
var markAsStarredError: Error?
var markAsUnstarredError: Error?
}
let results = StarredStatusResults()
group.enter()
account.markAsStarred(remoteStarredArticleIDs) { error in
results.markAsStarredError = error
group.leave()
}
let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { error in
results.markAsUnstarredError = error
group.leave()
}
group.notify(queue: .main) {
let markingError = results.markAsStarredError ?? results.markAsUnstarredError
guard let error = markingError else {
self.didFinish()
return
}
self.didFinish(error)
}
}
}

View File

@ -0,0 +1,75 @@
//
// FeedlyIngestStreamArticleIdsOperation.swift
// Account
//
// Created by Kiel Gillard on 9/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
/// Single responsibility is to ensure a status exists for every article id the user might be interested in.
///
/// Typically, it pages through the article ids of the global.all stream.
/// As the article ids are collected, a default read status is created for each.
/// So this operation has side effects *for the entire account* it operates on.
class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation {
private let account: Account
private let resource: FeedlyResourceId
private let service: FeedlyGetStreamIdsService
private let log: OSLog
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, log: OSLog) {
self.account = account
self.resource = resource
self.service = service
self.log = log
}
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, log: OSLog) {
let all = FeedlyCategoryResourceId.Global.all(for: credentials.username)
self.init(account: account, resource: all, service: service, log: log)
}
override func main() {
guard !isCancelled else {
didFinish()
return
}
getStreamIds(nil)
}
private func getStreamIds(_ continuation: String?) {
service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil, completion: didGetStreamIds(_:))
}
private func didGetStreamIds(_ result: Result<FeedlyStreamIds, Error>) {
guard !isCancelled else {
didFinish()
return
}
switch result {
case .success(let streamIds):
account.createStatusesIfNeeded(articleIDs: Set(streamIds.ids)) { databaseError in
if let error = databaseError {
self.didFinish(error)
return
}
guard let continuation = streamIds.continuation else {
os_log(.debug, log: self.log, "Reached end of stream for %@", self.resource.id)
self.didFinish()
return
}
self.getStreamIds(continuation)
}
case .failure(let error):
didFinish(error)
}
}
}

View File

@ -0,0 +1,130 @@
//
// FeedlyIngestUnreadArticleIdsOperation.swift
// Account
//
// Created by Kiel Gillard on 18/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
import RSParser
/// Single responsibility is to clone locally the remote unread article state.
///
/// Typically, it pages through the unread article ids of the global.all stream.
/// When all the unread article ids are collected, a status is created for each.
/// The article ids previously marked as unread but not collected become read.
/// So this operation has side effects *for the entire account* it operates on.
final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation {
private let account: Account
private let resource: FeedlyResourceId
private let service: FeedlyGetStreamIdsService
private let entryIdsProvider: FeedlyEntryIdentifierProvider
private let log: OSLog
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
let resource = FeedlyCategoryResourceId.Global.all(for: credentials.username)
self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log)
}
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
self.account = account
self.resource = resource
self.service = service
self.entryIdsProvider = FeedlyEntryIdentifierProvider()
self.log = log
}
override func main() {
guard !isCancelled else {
didFinish()
return
}
getStreamIds(nil)
}
private func getStreamIds(_ continuation: String?) {
service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: true, completion: didGetStreamIds(_:))
}
private func didGetStreamIds(_ result: Result<FeedlyStreamIds, Error>) {
guard !isCancelled else {
didFinish()
return
}
switch result {
case .success(let streamIds):
entryIdsProvider.addEntryIds(in: streamIds.ids)
guard let continuation = streamIds.continuation else {
updateUnreadStatuses()
return
}
getStreamIds(continuation)
case .failure(let error):
didFinish(error)
}
}
private func updateUnreadStatuses() {
guard !isCancelled else {
didFinish()
return
}
account.fetchUnreadArticleIDs { result in
switch result {
case .success(let localUnreadArticleIDs):
self.processUnreadArticleIDs(localUnreadArticleIDs)
case .failure(let error):
self.didFinish(error)
}
}
}
private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set<String>) {
guard !isCancelled else {
didFinish()
return
}
let remoteUnreadArticleIDs = entryIdsProvider.entryIds
let group = DispatchGroup()
final class ReadStatusResults {
var markAsUnreadError: Error?
var markAsReadError: Error?
}
let results = ReadStatusResults()
group.enter()
account.markAsUnread(remoteUnreadArticleIDs) { error in
results.markAsUnreadError = error
group.leave()
}
let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs)
group.enter()
account.markAsRead(articleIDsToMarkRead) { error in
results.markAsReadError = error
group.leave()
}
group.notify(queue: .main) {
let markingError = results.markAsReadError ?? results.markAsUnreadError
guard let error = markingError else {
self.didFinish()
return
}
self.didFinish(error)
}
}
}

View File

@ -11,7 +11,7 @@ import RSParser
import os.log
protocol FeedlyParsedItemsByFeedProviding {
var providerName: String { get }
var parsedItemsByFeedProviderName: String { get }
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>] { get }
}
@ -21,15 +21,15 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar
private let parsedItemProvider: FeedlyParsedItemProviding
private let log: OSLog
var parsedItemsByFeedProviderName: String {
return name ?? String(describing: Self.self)
}
var parsedItemsKeyedByFeedId: [String : Set<ParsedItem>] {
assert(Thread.isMainThread) // Needs to be on main thread because Feed is a main-thread-only model type.
return itemsKeyedByFeedId
}
var providerName: String {
return parsedItemProvider.resource.id
}
private var itemsKeyedByFeedId = [String: Set<ParsedItem>]()
init(account: Account, parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) {
@ -61,7 +61,7 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar
guard !isCancelled else { return }
}
os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.resource.id)
os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.parsedItemProviderName)
itemsKeyedByFeedId = dict
}

View File

@ -1,92 +0,0 @@
//
// FeedlySetStarredArticlesOperation.swift
// Account
//
// Created by Kiel Gillard on 14/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
protocol FeedlyStarredEntryIdProviding {
var entryIds: Set<String> { get }
}
/// Single responsibility is to associate a starred status for ingested and remote
/// articles identfied by the provided identifiers *for the entire account.*
final class FeedlySetStarredArticlesOperation: FeedlyOperation {
private let account: Account
private let allStarredEntryIdsProvider: FeedlyStarredEntryIdProviding
private let log: OSLog
init(account: Account, allStarredEntryIdsProvider: FeedlyStarredEntryIdProviding, log: OSLog) {
self.account = account
self.allStarredEntryIdsProvider = allStarredEntryIdsProvider
self.log = log
}
override func main() {
guard !isCancelled else {
didFinish()
return
}
account.fetchStarredArticleIDs { result in
switch result {
case .success(let localStarredArticleIDs):
self.processStarredArticleIDs(localStarredArticleIDs)
case .failure(let error):
self.didFinish(error)
}
}
}
}
private extension FeedlySetStarredArticlesOperation {
func processStarredArticleIDs(_ localStarredArticleIDs: Set<String>) {
guard !isCancelled else {
didFinish()
return
}
let remoteStarredArticleIDs = allStarredEntryIdsProvider.entryIds
guard !remoteStarredArticleIDs.isEmpty else {
didFinish()
return
}
let group = DispatchGroup()
final class StarredStatusResults {
var markAsStarredError: Error?
var markAsUnstarredError: Error?
}
let results = StarredStatusResults()
group.enter()
account.markAsStarred(remoteStarredArticleIDs) { error in
results.markAsStarredError = error
group.leave()
}
let deltaUnstarredArticleIDs = localStarredArticleIDs.subtracting(remoteStarredArticleIDs)
group.enter()
account.markAsUnstarred(deltaUnstarredArticleIDs) { error in
results.markAsUnstarredError = error
group.leave()
}
group.notify(queue: .main) {
let markingError = results.markAsStarredError ?? results.markAsUnstarredError
guard let error = markingError else {
self.didFinish()
return
}
self.didFinish(error)
}
}
}

View File

@ -1,92 +0,0 @@
//
// FeedlySetUnreadArticlesOperation.swift
// Account
//
// Created by Kiel Gillard on 25/9/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
protocol FeedlyUnreadEntryIdProviding {
var entryIds: Set<String> { get }
}
/// Single responsibility is to associate a read status for ingested and remote articles
/// where the provided article identifers identify the unread articles *for the entire account.*
final class FeedlySetUnreadArticlesOperation: FeedlyOperation {
private let account: Account
private let allUnreadIdsProvider: FeedlyUnreadEntryIdProviding
private let log: OSLog
init(account: Account, allUnreadIdsProvider: FeedlyUnreadEntryIdProviding, log: OSLog) {
self.account = account
self.allUnreadIdsProvider = allUnreadIdsProvider
self.log = log
}
override func main() {
guard !isCancelled else {
didFinish()
return
}
account.fetchUnreadArticleIDs { result in
switch result {
case .success(let localUnreadArticleIDs):
self.processUnreadArticleIDs(localUnreadArticleIDs)
case .failure(let error):
self.didFinish(error)
}
}
}
}
private extension FeedlySetUnreadArticlesOperation {
private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set<String>) {
guard !isCancelled else {
didFinish()
return
}
let remoteUnreadArticleIDs = allUnreadIdsProvider.entryIds
guard !remoteUnreadArticleIDs.isEmpty else {
didFinish()
return
}
let group = DispatchGroup()
final class ReadStatusResults {
var markAsUnreadError: Error?
var markAsReadError: Error?
}
let results = ReadStatusResults()
group.enter()
account.markAsUnread(remoteUnreadArticleIDs) { error in
results.markAsUnreadError = error
group.leave()
}
let articleIDsToMarkRead = localUnreadArticleIDs.subtracting(remoteUnreadArticleIDs)
group.enter()
account.markAsRead(articleIDsToMarkRead) { error in
results.markAsReadError = error
group.leave()
}
group.notify(queue: .main) {
let markingError = results.markAsReadError ?? results.markAsUnreadError
guard let error = markingError else {
self.didFinish()
return
}
self.didFinish(error)
}
}
}

View File

@ -19,7 +19,19 @@ final class FeedlySyncAllOperation: FeedlyOperation {
var syncCompletionHandler: ((Result<Void, Error>) -> ())?
init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredArticlesService: FeedlyGetStreamContentsService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) {
/// These requests to Feedly determine which articles to download:
/// 1. The set of all article ids we might need or show.
/// 2. The set of all unread article ids we might need or show (a subset of 1).
/// 3. The set of all article ids changed since the last sync (a subset of 1).
/// 4. The set of all starred article ids.
///
/// On the response for 1, create statuses for each article id.
/// On the response for 2, create unread statuses for each article id and mark as read those no longer in the response.
/// On the response for 4, create starred statuses for each article id and mark as unstarred those no longer in the response.
///
/// Download articles for statuses at the union of those statuses without its corresponding article and those included in 3 (changed since last successful sync).
///
init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) {
self.syncUUID = UUID()
self.log = log
self.operationQueue = OperationQueue()
@ -54,48 +66,66 @@ final class FeedlySyncAllOperation: FeedlyOperation {
createFeedsOperation.addDependency(mirrorCollectionsAsFolders)
self.operationQueue.addOperation(createFeedsOperation)
let getAllArticleIds = FeedlyIngestStreamArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, log: log)
getAllArticleIds.delegate = self
getAllArticleIds.downloadProgress = downloadProgress
getAllArticleIds.addDependency(createFeedsOperation)
self.operationQueue.addOperation(getAllArticleIds)
// Get each page of unread article ids in the global.all stream for the last 31 days (nil = Feedly API default).
let getUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: getUnreadService, newerThan: nil, log: log)
let getUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: getUnreadService, newerThan: nil, log: log)
getUnread.delegate = self
getUnread.addDependency(createFeedsOperation)
getUnread.addDependency(getAllArticleIds)
getUnread.downloadProgress = downloadProgress
self.operationQueue.addOperation(getUnread)
// Get each page of the global.all stream until we get either the content from the last sync or the last 31 days.
let getStreamContents = FeedlySyncStreamContentsOperation(account: account, credentials: credentials, service: getStreamContentsService, newerThan: lastSuccessfulFetchStartDate, log: log)
getStreamContents.delegate = self
getStreamContents.downloadProgress = downloadProgress
getStreamContents.addDependency(getUnread)
self.operationQueue.addOperation(getStreamContents)
// Get each page of the article ids which have been update since the last successful fetch start date.
// If the date is nil, this operation provides an empty set (everything is new, nothing is updated).
let getUpdated = FeedlyGetUpdatedArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, newerThan: lastSuccessfulFetchStartDate, log: log)
getUpdated.delegate = self
getUpdated.downloadProgress = downloadProgress
getUpdated.addDependency(createFeedsOperation)
self.operationQueue.addOperation(getUpdated)
// Get each and every starred article.
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: getStarredArticlesService, log: log)
syncStarred.delegate = self
syncStarred.downloadProgress = downloadProgress
syncStarred.addDependency(createFeedsOperation)
self.operationQueue.addOperation(syncStarred)
// Get each page of the article ids for starred articles.
let getStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: getStarredService, newerThan: nil, log: log)
getStarred.delegate = self
getStarred.downloadProgress = downloadProgress
getStarred.addDependency(createFeedsOperation)
self.operationQueue.addOperation(getStarred)
// Now all the possible article ids we need have a status, fetch the article ids for missing articles.
let getMissingIds = FeedlyFetchIdsForMissingArticlesOperation(account: account, log: log)
getMissingIds.delegate = self
getMissingIds.downloadProgress = downloadProgress
getMissingIds.addDependency(getAllArticleIds)
getMissingIds.addDependency(getUnread)
getMissingIds.addDependency(getStarred)
getMissingIds.addDependency(getUpdated)
self.operationQueue.addOperation(getMissingIds)
// Download all the missing and updated articles
let downloadMissingArticles = FeedlyDownloadArticlesOperation(account: account,
missingArticleEntryIdProvider: getMissingIds,
updatedArticleEntryIdProvider: getUpdated,
getEntriesService: getEntriesService,
log: log)
downloadMissingArticles.delegate = self
downloadMissingArticles.downloadProgress = downloadProgress
downloadMissingArticles.addDependency(getMissingIds)
downloadMissingArticles.addDependency(getUpdated)
self.operationQueue.addOperation(downloadMissingArticles)
// Once this operation's dependencies, their dependencies etc finish, we can finish.
let finishOperation = FeedlyCheckpointOperation()
finishOperation.checkpointDelegate = self
finishOperation.downloadProgress = downloadProgress
finishOperation.addDependency(getStreamContents)
finishOperation.addDependency(syncStarred)
finishOperation.addDependency(downloadMissingArticles)
self.operationQueue.addOperation(finishOperation)
}
convenience init(account: Account, credentials: Credentials, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) {
let newerThan: Date? = {
if let date = lastSuccessfulFetchStartDate {
return date
} else {
return Calendar.current.date(byAdding: .day, value: -31, to: Date())
}
}()
self.init(account: account, credentials: credentials, lastSuccessfulFetchStartDate: newerThan, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredArticlesService: caller, database: database, downloadProgress: downloadProgress, log: log)
self.init(account: account, credentials: credentials, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log)
}
override func cancel() {

View File

@ -1,150 +0,0 @@
//
// FeedlySyncStarredArticlesOperation.swift
// Account
//
// Created by Kiel Gillard on 15/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
import RSParser
final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate {
private let account: Account
private let operationQueue: OperationQueue
private let service: FeedlyGetStreamContentsService
private let log: OSLog
private let setStatuses: FeedlySetStarredArticlesOperation
private let finishOperation: FeedlyCheckpointOperation
/// Buffers every starred/saved entry from every page.
private class StarredEntryProvider: FeedlyEntryProviding, FeedlyStarredEntryIdProviding, FeedlyParsedItemProviding {
var resource: FeedlyResourceId
private(set) var parsedEntries = Set<ParsedItem>()
private(set) var entries = [FeedlyEntry]()
init(resource: FeedlyResourceId) {
self.resource = resource
}
func addEntries(from provider: FeedlyEntryProviding & FeedlyParsedItemProviding) {
entries.append(contentsOf: provider.entries)
parsedEntries.formUnion(provider.parsedEntries)
}
var entryIds: Set<String> {
return Set(entries.map { $0.id })
}
}
private let entryProvider: StarredEntryProvider
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamContentsService, log: OSLog) {
let saved = FeedlyTagResourceId.Global.saved(for: credentials.username)
self.init(account: account, resource: saved, service: service, log: log)
}
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, log: OSLog) {
self.account = account
self.service = service
self.operationQueue = OperationQueue()
self.operationQueue.isSuspended = true
self.finishOperation = FeedlyCheckpointOperation()
self.log = log
let provider = StarredEntryProvider(resource: resource)
self.entryProvider = provider
self.setStatuses = FeedlySetStarredArticlesOperation(account: account,
allStarredEntryIdsProvider: provider,
log: log)
super.init()
let getFirstPage = FeedlyGetStreamContentsOperation(account: account,
resource: resource,
service: service,
newerThan: nil,
log: log)
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
parsedItemProvider: provider,
log: log)
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
organisedItemsProvider: organiseByFeed,
log: log)
getFirstPage.delegate = self
getFirstPage.streamDelegate = self
setStatuses.addDependency(getFirstPage)
setStatuses.delegate = self
organiseByFeed.addDependency(setStatuses)
organiseByFeed.delegate = self
updateAccount.addDependency(organiseByFeed)
updateAccount.delegate = self
finishOperation.checkpointDelegate = self
finishOperation.addDependency(updateAccount)
let operations = [getFirstPage, setStatuses, organiseByFeed, updateAccount, finishOperation]
operationQueue.addOperations(operations, waitUntilFinished: false)
}
override func cancel() {
os_log(.debug, log: log, "Canceling sync starred articles")
operationQueue.cancelAllOperations()
super.cancel()
didFinish()
}
override func main() {
guard !isCancelled else {
// override of cancel calls didFinish().
return
}
operationQueue.isSuspended = false
}
func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) {
guard !isCancelled else {
os_log(.debug, log: log, "Cancelled starred stream contents for %@", stream.id)
return
}
entryProvider.addEntries(from: operation)
os_log(.debug, log: log, "Collecting %i items from %@", stream.items.count, stream.id)
guard let continuation = stream.continuation else {
return
}
let nextPageOperation = FeedlyGetStreamContentsOperation(account: operation.account,
resource: operation.resource,
service: operation.service,
continuation: continuation,
newerThan: operation.newerThan,
log: log)
nextPageOperation.delegate = self
nextPageOperation.streamDelegate = self
setStatuses.addDependency(nextPageOperation)
operationQueue.addOperation(nextPageOperation)
}
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
didFinish()
}
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
os_log(.debug, log: log, "%{public}@ failing and cancelling other operations because %{public}@.", operation, error.localizedDescription)
operationQueue.cancelAllOperations()
didFinish(error)
}
}

View File

@ -1,131 +0,0 @@
//
// FeedlySyncUnreadStatusesOperation.swift
// Account
//
// Created by Kiel Gillard on 18/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
import RSParser
/// Makes one or more requests to get the complete set of unread article ids to update the status of those articles *for the entire account.*
final class FeedlySyncUnreadStatusesOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamIdsOperationDelegate, FeedlyCheckpointOperationDelegate {
private let account: Account
private let resource: FeedlyResourceId
private let operationQueue: OperationQueue
private let service: FeedlyGetStreamIdsService
private let log: OSLog
/// Buffers every unread article id from every page of the resource's stream.
private class UnreadEntryIdsProvider: FeedlyUnreadEntryIdProviding {
let resource: FeedlyResourceId
private(set) var entryIds = Set<String>()
init(resource: FeedlyResourceId) {
self.resource = resource
}
func addEntryIds(from provider: FeedlyEntryIdenifierProviding) {
entryIds.formUnion(provider.entryIds)
}
}
private let unreadEntryIdsProvider: UnreadEntryIdsProvider
private let setStatuses: FeedlySetUnreadArticlesOperation
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
let resource = FeedlyCategoryResourceId.Global.all(for: credentials.username)
self.init(account: account, resource: resource, service: service, newerThan: newerThan, log: log)
}
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
self.account = account
self.resource = resource
self.service = service
self.operationQueue = OperationQueue()
self.operationQueue.isSuspended = true
self.log = log
let provider = UnreadEntryIdsProvider(resource: resource)
self.unreadEntryIdsProvider = provider
self.setStatuses = FeedlySetUnreadArticlesOperation(account: account,
allUnreadIdsProvider: unreadEntryIdsProvider,
log: log)
super.init()
let getFirstPageOfUnreadIds = FeedlyGetStreamIdsOperation(account: account,
resource: resource,
service: service,
newerThan: nil,
unreadOnly: true,
log: log)
getFirstPageOfUnreadIds.delegate = self
getFirstPageOfUnreadIds.streamIdsDelegate = self
setStatuses.addDependency(getFirstPageOfUnreadIds)
setStatuses.delegate = self
let finishOperation = FeedlyCheckpointOperation()
finishOperation.checkpointDelegate = self
finishOperation.addDependency(setStatuses)
let operations = [getFirstPageOfUnreadIds, setStatuses, finishOperation]
operationQueue.addOperations(operations, waitUntilFinished: false)
}
override func cancel() {
os_log(.debug, log: log, "Canceling sync unread statuses")
operationQueue.cancelAllOperations()
super.cancel()
didFinish()
}
override func main() {
guard !isCancelled else {
// override of cancel calls didFinish().
return
}
operationQueue.isSuspended = false
}
func feedlyGetStreamIdsOperation(_ operation: FeedlyGetStreamIdsOperation, didGet streamIds: FeedlyStreamIds) {
guard !isCancelled else {
os_log(.debug, log: log, "Cancelled unread stream ids.")
return
}
os_log(.debug, log: log, "Collecting %i unread article ids from %@", streamIds.ids.count, resource.id)
unreadEntryIdsProvider.addEntryIds(from: operation)
guard let continuation = streamIds.continuation else {
return
}
let nextPageOperation = FeedlyGetStreamIdsOperation(account: operation.account,
resource: operation.resource,
service: operation.service,
continuation: continuation,
newerThan: operation.newerThan,
unreadOnly: operation.unreadOnly,
log: log)
nextPageOperation.delegate = self
nextPageOperation.streamIdsDelegate = self
setStatuses.addDependency(nextPageOperation)
operationQueue.addOperation(nextPageOperation)
}
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
didFinish()
}
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
operationQueue.cancelAllOperations()
didFinish(error)
}
}

View File

@ -37,7 +37,7 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation {
return
}
os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", webFeedIDsAndItems.count, self.organisedItemsProvider.providerName)
os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", webFeedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName)
self.didFinish()
}
}

View File

@ -176,7 +176,7 @@ public final class ArticlesDatabase {
articlesTable.fetchStarredArticleIDsAsync(webFeedIDs, completion)
}
/// Fetch articleIDs for articles that we should have, but dont. These articles are not userDeleted, and they are either (starred) or (unread and newer than the article cutoff date).
/// Fetch articleIDs for articles that we should have, but dont. These articles are not userDeleted, and they are either (starred) or (newer than the article cutoff date).
public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) {
articlesTable.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
}