Merge pull request #1144 from kielgillard/master
Syncs articles in pages, decouples article syncing from status syncin…
This commit is contained in:
commit
219e5751a1
|
@ -76,6 +76,10 @@
|
|||
84EAC4822148CC6300F154AB /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84EAC4812148CC6300F154AB /* RSDatabase.framework */; };
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */; };
|
||||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */; };
|
||||
9E03C11C235D921400FB6D9E /* FeedlyOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */; };
|
||||
9E03C11E235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */; };
|
||||
9E03C120235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E03C11F235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift */; };
|
||||
9E03C122235E62E100FB6D9E /* FeedlyTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E03C121235E62E100FB6D9E /* FeedlyTestSupport.swift */; };
|
||||
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */; };
|
||||
9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D22345700E0056A5A8 /* FeedlyLink.swift */; };
|
||||
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D4234570E30056A5A8 /* FeedlyEntryParser.swift */; };
|
||||
|
@ -83,29 +87,38 @@
|
|||
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 /* FeedlySyncStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154C233370D800F4944C /* FeedlySyncStrategy.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 */; };
|
||||
9E1D15532334304B00F4944C /* FeedlyGetStreamOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15522334304B00F4944C /* FeedlyGetStreamOperation.swift */; };
|
||||
9E1D15532334304B00F4944C /* FeedlyGetStreamContentsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15522334304B00F4944C /* FeedlyGetStreamContentsOperation.swift */; };
|
||||
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D1554233431A600F4944C /* FeedlyOperation.swift */; };
|
||||
9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */; };
|
||||
9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */; };
|
||||
9E1D155D233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */; };
|
||||
9E1FF8602368216B00834C24 /* TestGetStreamIdsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF85F2368216B00834C24 /* TestGetStreamIdsService.swift */; };
|
||||
9E1FF8622368219B00834C24 /* TestGetPagedStreamIdsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
9E510D6E234F16A8002E6F1A /* FeedlyAddFeedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */; };
|
||||
9E713653233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */; };
|
||||
9E5ABE9A236BE6BD00B5DE9F /* feedly-1-initial in Resources */ = {isa = PBXBuildFile; fileRef = 9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */; };
|
||||
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */; };
|
||||
9E7299D723505E9600DAEFB7 /* FeedlyAddFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D623505E9600DAEFB7 /* FeedlyAddFeedOperation.swift */; };
|
||||
9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */; };
|
||||
9E7F15072341E96700F860D1 /* AccountFeedlySyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7F15062341E96700F860D1 /* AccountFeedlySyncTest.swift */; };
|
||||
9E7F150A2341EF5A00F860D1 /* feedly_collections_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15092341EF5A00F860D1 /* feedly_collections_initial.json */; };
|
||||
9E7F150D2341F32000F860D1 /* macintosh_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F150C2341F32000F860D1 /* macintosh_initial.json */; };
|
||||
9E7F15112341F39A00F860D1 /* uncategorized_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15102341F39A00F860D1 /* uncategorized_initial.json */; };
|
||||
9E7F15132341F3D900F860D1 /* programming_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15122341F3D900F860D1 /* programming_initial.json */; };
|
||||
9E7F15152341F42000F860D1 /* weblogs_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15142341F42000F860D1 /* weblogs_initial.json */; };
|
||||
9E7F15172341F48900F860D1 /* mustread_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E7F15162341F48900F860D1 /* mustread_initial.json */; };
|
||||
9E832B1E2343467900D83249 /* feedly_collections_addcollection.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E832B1D2343467900D83249 /* feedly_collections_addcollection.json */; };
|
||||
9E832B202343476A00D83249 /* newcollection_addcollection.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E832B1F2343476A00D83249 /* newcollection_addcollection.json */; };
|
||||
9E832B23234416B400D83249 /* feedly_collections_addfeed.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E832B22234416B400D83249 /* feedly_collections_addfeed.json */; };
|
||||
9E832B25234416FF00D83249 /* mustread_addfeed.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E832B24234416FF00D83249 /* mustread_addfeed.json */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */; };
|
||||
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */; };
|
||||
9EAEC60C2332FE830085D7C9 /* FeedlyCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */; };
|
||||
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */; };
|
||||
|
@ -113,14 +126,33 @@
|
|||
9EAEC626233318400085D7C9 /* FeedlyStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC625233318400085D7C9 /* FeedlyStream.swift */; };
|
||||
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */; };
|
||||
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */; };
|
||||
9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.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 */; };
|
||||
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */; };
|
||||
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */; };
|
||||
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */; };
|
||||
9EC804E3236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC804E2236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift */; };
|
||||
9EC804E5236C1A7F0057CFCB /* feedly-2-changestatuses in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804E4236C1A7F0057CFCB /* feedly-2-changestatuses */; };
|
||||
9EC804E7236C1BA60057CFCB /* feedly-3-changestatusesagain in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804E6236C1BA60057CFCB /* feedly-3-changestatusesagain */; };
|
||||
9EC804E9236C1CBF0057CFCB /* feedly-4-addfeedsandfolders in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804E8236C1CBE0057CFCB /* feedly-4-addfeedsandfolders */; };
|
||||
9EC804EB236C1DFB0057CFCB /* feedly-5-removefeedsandfolders in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804EA236C1DFA0057CFCB /* feedly-5-removefeedsandfolders */; };
|
||||
9EC804ED236C206A0057CFCB /* feedly_collections_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804EC236C206A0057CFCB /* feedly_collections_initial.json */; };
|
||||
9EC804EF236C20DD0057CFCB /* feedly_macintosh_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804EE236C20DD0057CFCB /* feedly_macintosh_initial.json */; };
|
||||
9EC804F2236C21320057CFCB /* feedly_unreads_1000.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804F0236C21320057CFCB /* feedly_unreads_1000.json */; };
|
||||
9EC804F3236C21320057CFCB /* feedly_unreads_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EC804F1236C21320057CFCB /* feedly_unreads_initial.json */; };
|
||||
9ECC9A85234DC16E009B5144 /* FeedlyAccountDelegateError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */; };
|
||||
9EE4CCFA234F106600FBAE4B /* FeedlyFeedContainerValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */; };
|
||||
9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE06D235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift */; };
|
||||
9EEAE071235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEAE070235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift */; };
|
||||
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 */; };
|
||||
9EEEF75223567CA6009E9D80 /* saved_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 9EEEF75123567CA6009E9D80 /* saved_initial.json */; };
|
||||
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 */; };
|
||||
9EF35F7A234E830E003AE2AE /* FeedlyCompoundOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -243,6 +275,10 @@
|
|||
84D09622217418DC00D77525 /* FeedbinTagging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTagging.swift; sourceTree = "<group>"; };
|
||||
84EAC4812148CC6300F154AB /* RSDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleFetcher.swift; sourceTree = "<group>"; };
|
||||
9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionsOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E03C11F235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyMirrorCollectionsAsFoldersOperationTests.swift; sourceTree = "<group>"; };
|
||||
9E03C121235E62E100FB6D9E /* FeedlyTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyTestSupport.swift; sourceTree = "<group>"; };
|
||||
9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyCreateFeedsForCollectionFoldersOperation.swift; sourceTree = "<group>"; };
|
||||
9E1773D22345700E0056A5A8 /* FeedlyLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyLink.swift; sourceTree = "<group>"; };
|
||||
9E1773D4234570E30056A5A8 /* FeedlyEntryParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyEntryParser.swift; sourceTree = "<group>"; };
|
||||
|
@ -250,29 +286,38 @@
|
|||
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 /* FeedlySyncStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStrategy.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>"; };
|
||||
9E1D15522334304B00F4944C /* FeedlyGetStreamOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D15522334304B00F4944C /* FeedlyGetStreamContentsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D1554233431A600F4944C /* FeedlyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRequestStreamsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyOrganiseParsedItemsByFeedOperation.swift; sourceTree = "<group>"; };
|
||||
9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyUpdateAccountFeedsWithItemsOperation.swift; sourceTree = "<group>"; };
|
||||
9E1FF85F2368216B00834C24 /* TestGetStreamIdsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetStreamIdsService.swift; sourceTree = "<group>"; };
|
||||
9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGetPagedStreamIdsService.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddFeedRequest.swift; sourceTree = "<group>"; };
|
||||
9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshStreamEntriesStatusOperation.swift; sourceTree = "<group>"; };
|
||||
9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-1-initial"; sourceTree = "<group>"; };
|
||||
9E713652233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetUnreadArticlesOperation.swift; sourceTree = "<group>"; };
|
||||
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAddFeedOperation.swift; sourceTree = "<group>"; };
|
||||
9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceProviding.swift; sourceTree = "<group>"; };
|
||||
9E7F15062341E96700F860D1 /* AccountFeedlySyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedlySyncTest.swift; sourceTree = "<group>"; };
|
||||
9E7F15092341EF5A00F860D1 /* feedly_collections_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_collections_initial.json; sourceTree = "<group>"; };
|
||||
9E7F150C2341F32000F860D1 /* macintosh_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = macintosh_initial.json; sourceTree = "<group>"; };
|
||||
9E7F15102341F39A00F860D1 /* uncategorized_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = uncategorized_initial.json; sourceTree = "<group>"; };
|
||||
9E7F15122341F3D900F860D1 /* programming_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = programming_initial.json; sourceTree = "<group>"; };
|
||||
9E7F15142341F42000F860D1 /* weblogs_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weblogs_initial.json; sourceTree = "<group>"; };
|
||||
9E7F15162341F48900F860D1 /* mustread_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mustread_initial.json; sourceTree = "<group>"; };
|
||||
9E832B1D2343467900D83249 /* feedly_collections_addcollection.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_collections_addcollection.json; sourceTree = "<group>"; };
|
||||
9E832B1F2343476A00D83249 /* newcollection_addcollection.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = newcollection_addcollection.json; sourceTree = "<group>"; };
|
||||
9E832B22234416B400D83249 /* feedly_collections_addfeed.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_collections_addfeed.json; sourceTree = "<group>"; };
|
||||
9E832B24234416FF00D83249 /* mustread_addfeed.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = mustread_addfeed.json; 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>"; };
|
||||
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>"; };
|
||||
9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetEntriesService.swift; sourceTree = "<group>"; };
|
||||
9EA3133A231E368100268BA0 /* FeedlyAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegate.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>"; };
|
||||
|
@ -280,14 +325,33 @@
|
|||
9EAEC625233318400085D7C9 /* FeedlyStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyStream.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
9EC228542362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySetStarredArticlesOperationTests.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>"; };
|
||||
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAPICaller.swift; sourceTree = "<group>"; };
|
||||
9EC688EB232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FeedlyAccountDelegate+OAuth.swift"; sourceTree = "<group>"; };
|
||||
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthAuthorizationCodeGranting.swift; sourceTree = "<group>"; };
|
||||
9EC804E2236C18AB0057CFCB /* FeedlySyncAllMockResponseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncAllMockResponseProvider.swift; sourceTree = "<group>"; };
|
||||
9EC804E4236C1A7F0057CFCB /* feedly-2-changestatuses */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-2-changestatuses"; sourceTree = "<group>"; };
|
||||
9EC804E6236C1BA60057CFCB /* feedly-3-changestatusesagain */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-3-changestatusesagain"; sourceTree = "<group>"; };
|
||||
9EC804E8236C1CBE0057CFCB /* feedly-4-addfeedsandfolders */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-4-addfeedsandfolders"; sourceTree = "<group>"; };
|
||||
9EC804EA236C1DFA0057CFCB /* feedly-5-removefeedsandfolders */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "feedly-5-removefeedsandfolders"; sourceTree = "<group>"; };
|
||||
9EC804EC236C206A0057CFCB /* feedly_collections_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_collections_initial.json; sourceTree = "<group>"; };
|
||||
9EC804EE236C20DD0057CFCB /* feedly_macintosh_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_macintosh_initial.json; sourceTree = "<group>"; };
|
||||
9EC804F0236C21320057CFCB /* feedly_unreads_1000.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_unreads_1000.json; sourceTree = "<group>"; };
|
||||
9EC804F1236C21320057CFCB /* feedly_unreads_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = feedly_unreads_initial.json; sourceTree = "<group>"; };
|
||||
9ECC9A84234DC16E009B5144 /* FeedlyAccountDelegateError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyAccountDelegateError.swift; sourceTree = "<group>"; };
|
||||
9EE4CCF9234F106600FBAE4B /* FeedlyFeedContainerValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyFeedContainerValidator.swift; sourceTree = "<group>"; };
|
||||
9EEAE06D235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetCollectionsService.swift; sourceTree = "<group>"; };
|
||||
9EEAE070235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyGetStreamContentsService.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
9EEEF75123567CA6009E9D80 /* saved_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = saved_initial.json; 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>"; };
|
||||
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>"; };
|
||||
|
@ -355,6 +419,10 @@
|
|||
51D58756227F62E300900287 /* JSON */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9EC804F0236C21320057CFCB /* feedly_unreads_1000.json */,
|
||||
9EC804F1236C21320057CFCB /* feedly_unreads_initial.json */,
|
||||
9EC804EE236C20DD0057CFCB /* feedly_macintosh_initial.json */,
|
||||
9EC804EC236C206A0057CFCB /* feedly_collections_initial.json */,
|
||||
5133230D2281089500C30F19 /* icons.json */,
|
||||
5133230B2281088A00C30F19 /* subscriptions_add.json */,
|
||||
513323092281082F00C30F19 /* subscriptions_initial.json */,
|
||||
|
@ -506,47 +574,40 @@
|
|||
9E7F15082341E97100F860D1 /* Feedly */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E7F15062341E96700F860D1 /* AccountFeedlySyncTest.swift */,
|
||||
9E03C121235E62E100FB6D9E /* FeedlyTestSupport.swift */,
|
||||
9E85C8E52366FED600D0F1F7 /* TestGetStreamContentsService.swift */,
|
||||
9E1FF85F2368216B00834C24 /* TestGetStreamIdsService.swift */,
|
||||
9E85C8E72366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift */,
|
||||
9E1FF8612368219B00834C24 /* TestGetPagedStreamIdsService.swift */,
|
||||
9E1FF8672368EE4900834C24 /* TestGetCollectionsService.swift */,
|
||||
9E1FF8652368ED7E00834C24 /* TestMarkArticlesService.swift */,
|
||||
9E03C11B235D921400FB6D9E /* FeedlyOperationTests.swift */,
|
||||
9E03C11D235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift */,
|
||||
9E03C11F235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift */,
|
||||
9E7F88AB235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift */,
|
||||
9E7F88AD235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift */,
|
||||
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 */,
|
||||
9E7F150B2341F2A700F860D1 /* Initial */,
|
||||
9E832B1A234344DA00D83249 /* AddCollection */,
|
||||
9E832B21234416B400D83249 /* AddFeed */,
|
||||
9E5ABE99236BE6BC00B5DE9F /* feedly-1-initial */,
|
||||
9EC804E4236C1A7F0057CFCB /* feedly-2-changestatuses */,
|
||||
9EC804E6236C1BA60057CFCB /* feedly-3-changestatusesagain */,
|
||||
9EC804E8236C1CBE0057CFCB /* feedly-4-addfeedsandfolders */,
|
||||
9EC804EA236C1DFA0057CFCB /* feedly-5-removefeedsandfolders */,
|
||||
);
|
||||
path = Feedly;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E7F150B2341F2A700F860D1 /* Initial */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9EEEF75123567CA6009E9D80 /* saved_initial.json */,
|
||||
9E7F15092341EF5A00F860D1 /* feedly_collections_initial.json */,
|
||||
9E7F150C2341F32000F860D1 /* macintosh_initial.json */,
|
||||
9E7F15162341F48900F860D1 /* mustread_initial.json */,
|
||||
9E7F15122341F3D900F860D1 /* programming_initial.json */,
|
||||
9E7F15102341F39A00F860D1 /* uncategorized_initial.json */,
|
||||
9E7F15142341F42000F860D1 /* weblogs_initial.json */,
|
||||
);
|
||||
path = Initial;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E832B1A234344DA00D83249 /* AddCollection */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E832B1D2343467900D83249 /* feedly_collections_addcollection.json */,
|
||||
9E832B1F2343476A00D83249 /* newcollection_addcollection.json */,
|
||||
);
|
||||
path = AddCollection;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E832B21234416B400D83249 /* AddFeed */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E832B22234416B400D83249 /* feedly_collections_addfeed.json */,
|
||||
9E832B24234416FF00D83249 /* mustread_addfeed.json */,
|
||||
);
|
||||
path = AddFeed;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EA31339231E368100268BA0 /* Feedly */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -557,33 +618,39 @@
|
|||
9EC688ED232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift */,
|
||||
9EC688E9232B973C00A8D0A2 /* FeedlyAPICaller.swift */,
|
||||
9E510D6D234F16A8002E6F1A /* FeedlyAddFeedRequest.swift */,
|
||||
9E1D1554233431A600F4944C /* FeedlyOperation.swift */,
|
||||
9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */,
|
||||
9E7299D8235062A200DAEFB7 /* FeedlyResourceProviding.swift */,
|
||||
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedOperation.swift */,
|
||||
9EEAE06F235D003400E3FEE4 /* Services */,
|
||||
9EBC31B32338AC2E002A567B /* Models */,
|
||||
9EBC31B22338AC0F002A567B /* Refresh */,
|
||||
9EBC31B22338AC0F002A567B /* Operations */,
|
||||
);
|
||||
path = Feedly;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
9EBC31B22338AC0F002A567B /* Refresh */ = {
|
||||
9EBC31B22338AC0F002A567B /* Operations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E1D154C233370D800F4944C /* FeedlySyncStrategy.swift */,
|
||||
9E1D1554233431A600F4944C /* FeedlyOperation.swift */,
|
||||
9EF35F79234E830E003AE2AE /* FeedlyCompoundOperation.swift */,
|
||||
9E7299D623505E9600DAEFB7 /* FeedlyAddFeedOperation.swift */,
|
||||
9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */,
|
||||
9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */,
|
||||
9E12B01F2334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift */,
|
||||
9E1D15562334355900F4944C /* FeedlyRequestStreamsOperation.swift */,
|
||||
9E1D15522334304B00F4944C /* FeedlyGetStreamOperation.swift */,
|
||||
9E1D15522334304B00F4944C /* FeedlyGetStreamContentsOperation.swift */,
|
||||
9EF1B10623590D61000A486A /* FeedlyGetStreamIdsOperation.swift */,
|
||||
9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */,
|
||||
9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */,
|
||||
9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */,
|
||||
9EEEF7202355277F009E9D80 /* FeedlySyncStarredArticlesOperation.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 */,
|
||||
9E1D154C233370D800F4944C /* FeedlySyncAllOperation.swift */,
|
||||
);
|
||||
path = Refresh;
|
||||
path = Operations;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EBC31B32338AC2E002A567B /* Models */ = {
|
||||
|
@ -595,6 +662,7 @@
|
|||
9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */,
|
||||
9E1773D4234570E30056A5A8 /* FeedlyEntryParser.swift */,
|
||||
9EAEC625233318400085D7C9 /* FeedlyStream.swift */,
|
||||
9EF1B10823590E93000A486A /* FeedlyStreamIds.swift */,
|
||||
9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */,
|
||||
9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */,
|
||||
9E1773D22345700E0056A5A8 /* FeedlyLink.swift */,
|
||||
|
@ -603,6 +671,18 @@
|
|||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EEAE06F235D003400E3FEE4 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9EEAE06D235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift */,
|
||||
9EEAE070235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift */,
|
||||
9E85C8EC2367020700D0F1F7 /* FeedlyGetEntriesService.swift */,
|
||||
9EEAE072235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift */,
|
||||
9EEAE074235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D511EEB4202422BB00712EC3 /* xcconfig */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -679,11 +759,11 @@
|
|||
848934F51F62484F00CEBD24 = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
LastSwiftMigration = 0900;
|
||||
ProvisioningStyle = Manual;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
848934FE1F62484F00CEBD24 = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
ProvisioningStyle = Manual;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -765,25 +845,23 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9EC804ED236C206A0057CFCB /* feedly_collections_initial.json in Resources */,
|
||||
9E5ABE9A236BE6BD00B5DE9F /* feedly-1-initial in Resources */,
|
||||
5165D71822821C2400D9D53D /* taggings_initial.json in Resources */,
|
||||
5133230E2281089500C30F19 /* icons.json in Resources */,
|
||||
9EC804F3236C21320057CFCB /* feedly_unreads_initial.json in Resources */,
|
||||
51D5875B227F630B00900287 /* tags_add.json in Resources */,
|
||||
9EC804E9236C1CBF0057CFCB /* feedly-4-addfeedsandfolders in Resources */,
|
||||
9EC804EB236C1DFB0057CFCB /* feedly-5-removefeedsandfolders in Resources */,
|
||||
5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */,
|
||||
9E832B202343476A00D83249 /* newcollection_addcollection.json in Resources */,
|
||||
9E832B1E2343467900D83249 /* feedly_collections_addcollection.json in Resources */,
|
||||
9E7F15152341F42000F860D1 /* weblogs_initial.json in Resources */,
|
||||
51D5875C227F630B00900287 /* tags_initial.json in Resources */,
|
||||
9EC804E5236C1A7F0057CFCB /* feedly-2-changestatuses in Resources */,
|
||||
51D5875A227F630B00900287 /* tags_delete.json in Resources */,
|
||||
9EC804E7236C1BA60057CFCB /* feedly-3-changestatusesagain in Resources */,
|
||||
9EC804EF236C20DD0057CFCB /* feedly_macintosh_initial.json in Resources */,
|
||||
5165D71722821C2400D9D53D /* taggings_add.json in Resources */,
|
||||
9EEEF75223567CA6009E9D80 /* saved_initial.json in Resources */,
|
||||
9E832B23234416B400D83249 /* feedly_collections_addfeed.json in Resources */,
|
||||
5165D71622821C2400D9D53D /* taggings_delete.json in Resources */,
|
||||
9E7F15112341F39A00F860D1 /* uncategorized_initial.json in Resources */,
|
||||
9E7F15132341F3D900F860D1 /* programming_initial.json in Resources */,
|
||||
9E832B25234416FF00D83249 /* mustread_addfeed.json in Resources */,
|
||||
9E7F150A2341EF5A00F860D1 /* feedly_collections_initial.json in Resources */,
|
||||
9E7F15172341F48900F860D1 /* mustread_initial.json in Resources */,
|
||||
9E7F150D2341F32000F860D1 /* macintosh_initial.json in Resources */,
|
||||
9EC804F2236C21320057CFCB /* feedly_unreads_1000.json in Resources */,
|
||||
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -821,6 +899,7 @@
|
|||
552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */,
|
||||
84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */,
|
||||
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */,
|
||||
9EEAE071235D019B00E3FEE4 /* FeedlyGetStreamContentsService.swift in Sources */,
|
||||
9E7299D9235062A200DAEFB7 /* FeedlyResourceProviding.swift in Sources */,
|
||||
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */,
|
||||
8469F81C1F6DD15E0084783E /* Account.swift in Sources */,
|
||||
|
@ -830,12 +909,12 @@
|
|||
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */,
|
||||
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
|
||||
846E77451F6EF9B900A165E2 /* Container.swift in Sources */,
|
||||
9E1D15532334304B00F4944C /* FeedlyGetStreamOperation.swift in Sources */,
|
||||
9E1D15532334304B00F4944C /* FeedlyGetStreamContentsOperation.swift in Sources */,
|
||||
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */,
|
||||
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */,
|
||||
9EAEC62A23331EE70085D7C9 /* FeedlyOrigin.swift in Sources */,
|
||||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
|
||||
9E713653233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift in Sources */,
|
||||
9E713653233AD63E00765C84 /* FeedlySetUnreadArticlesOperation.swift in Sources */,
|
||||
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
|
||||
510BD113232C3E9D002692E4 /* FeedMetadataFile.swift in Sources */,
|
||||
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
|
||||
|
@ -843,17 +922,25 @@
|
|||
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
|
||||
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */,
|
||||
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
|
||||
9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */,
|
||||
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */,
|
||||
9E1D154D233370D800F4944C /* FeedlySyncStrategy.swift in Sources */,
|
||||
9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */,
|
||||
9E84DC492359A73600D6E809 /* FeedlyCheckpointOperation.swift in Sources */,
|
||||
9E85C8EB236700E600D0F1F7 /* FeedlyGetEntriesOperation.swift in Sources */,
|
||||
9E1D154D233370D800F4944C /* FeedlySyncAllOperation.swift in Sources */,
|
||||
844B297D2106C7EC004020B3 /* Feed.swift in Sources */,
|
||||
9E1D15572334355900F4944C /* FeedlyRequestStreamsOperation.swift in Sources */,
|
||||
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */,
|
||||
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */,
|
||||
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */,
|
||||
9E7299D723505E9600DAEFB7 /* FeedlyAddFeedOperation.swift in Sources */,
|
||||
9EEAE075235D01C400E3FEE4 /* FeedlyMarkArticlesService.swift in Sources */,
|
||||
9EF1B10323584B4C000A486A /* FeedlySyncStreamContentsOperation.swift in Sources */,
|
||||
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */,
|
||||
84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */,
|
||||
9E84DC472359A23200D6E809 /* FeedlySyncUnreadStatusesOperation.swift in Sources */,
|
||||
9EAEC624233315F60085D7C9 /* FeedlyEntry.swift in Sources */,
|
||||
9EEAE073235D01AE00E3FEE4 /* FeedlyGetStreamIdsService.swift in Sources */,
|
||||
9EEEF7212355277F009E9D80 /* FeedlySyncStarredArticlesOperation.swift in Sources */,
|
||||
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
|
||||
84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */,
|
||||
|
@ -882,6 +969,7 @@
|
|||
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */,
|
||||
9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */,
|
||||
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */,
|
||||
9EF1B10923590E93000A486A /* FeedlyStreamIds.swift in Sources */,
|
||||
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
||||
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
|
||||
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */,
|
||||
|
@ -894,6 +982,7 @@
|
|||
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 */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -902,14 +991,37 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9E7F15072341E96700F860D1 /* AccountFeedlySyncTest.swift in Sources */,
|
||||
9EC228572362C7F900766EF8 /* FeedlyCheckpointOperationTests.swift in Sources */,
|
||||
9E03C122235E62E100FB6D9E /* FeedlyTestSupport.swift in Sources */,
|
||||
9E3CFFFD2368202000BA7365 /* FeedlySyncUnreadStatusesOperationTests.swift in Sources */,
|
||||
9EC228552362C17F00766EF8 /* FeedlySetStarredArticlesOperationTests.swift in Sources */,
|
||||
9E03C120235E62A500FB6D9E /* FeedlyMirrorCollectionsAsFoldersOperationTests.swift in Sources */,
|
||||
9E489E912360ED30004372EE /* FeedlyOrganiseParsedItemsByFeedOperationTests.swift in Sources */,
|
||||
9E1FF8622368219B00834C24 /* TestGetPagedStreamIdsService.swift in Sources */,
|
||||
9E7F88AC235EDDC2009AB9DF /* FeedlyCreateFeedsForCollectionFoldersOperationTests.swift in Sources */,
|
||||
9E03C11E235D976500FB6D9E /* FeedlyGetCollectionsOperationTests.swift in Sources */,
|
||||
9E85C8E62366FED600D0F1F7 /* TestGetStreamContentsService.swift in Sources */,
|
||||
9E1FF8662368ED7E00834C24 /* TestMarkArticlesService.swift in Sources */,
|
||||
9E03C11C235D921400FB6D9E /* FeedlyOperationTests.swift in Sources */,
|
||||
9E1FF8642368EC2400834C24 /* FeedlySyncAllOperationTests.swift in Sources */,
|
||||
9E1FF8602368216B00834C24 /* TestGetStreamIdsService.swift in Sources */,
|
||||
9E85C8E82366FF4200D0F1F7 /* TestGetPagedStreamContentsService.swift in Sources */,
|
||||
9EC228592362D0EA00766EF8 /* FeedlySendArticleStatusesOperationTests.swift in Sources */,
|
||||
5165D7122282080C00D9D53D /* AccountFeedbinFolderContentsSyncTest.swift in Sources */,
|
||||
9E489E93236101FC004372EE /* FeedlyUpdateAccountFeedsWithItemsOperationTests.swift in Sources */,
|
||||
9E489E8D2360652C004372EE /* FeedlyGetStreamIdsOperationTests.swift in Sources */,
|
||||
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 */,
|
||||
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */,
|
||||
9E7F88AE235FBB11009AB9DF /* FeedlyGetStreamContentsOperationTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -1,324 +0,0 @@
|
|||
//
|
||||
// AccountFeedlySyncTest.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 30/9/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import Articles
|
||||
|
||||
class AccountFeedlySyncTest: XCTestCase {
|
||||
|
||||
private let testTransport = TestTransport()
|
||||
private var account: Account!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
account = TestAccountManager.shared.createAccount(type: .feedly, transport: testTransport)
|
||||
|
||||
do {
|
||||
let username = UUID().uuidString
|
||||
let credentials = Credentials(type: .oauthAccessToken, username: username, secret: "test")
|
||||
try account.storeCredentials(credentials)
|
||||
} catch {
|
||||
XCTFail("Unable to register mock credentials because \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Clean up
|
||||
do {
|
||||
try account.removeCredentials(type: .oauthAccessToken)
|
||||
} catch {
|
||||
XCTFail("Unable to clean up mock credentials because \(error)")
|
||||
}
|
||||
|
||||
TestAccountManager.shared.deleteAccount(account)
|
||||
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: Initial Sync
|
||||
|
||||
func testInitialSync() {
|
||||
XCTAssertTrue(account.idToFeedDictionary.isEmpty, "Expected to be testing a fresh account without any existing feeds.")
|
||||
XCTAssertTrue((account.folders ?? Set()).isEmpty, "Expected to be testing a fresh account without any existing folders.")
|
||||
|
||||
set(testFiles: .initial, with: testTransport)
|
||||
|
||||
// Test initial folders for collections and feeds for collection feeds.
|
||||
let initialExpection = self.expectation(description: "Initial feeds")
|
||||
account.refreshAll() { _ in
|
||||
initialExpection.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "macintosh_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "programming_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "uncategorized_initial")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "weblogs_initial")
|
||||
}
|
||||
|
||||
// MARK: Add Collection
|
||||
|
||||
func testAddsFoldersForCollections() {
|
||||
prepareBaseline(.initial)
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_initial")
|
||||
|
||||
set(testFiles: .addCollection, with: testTransport)
|
||||
|
||||
let addCollectionExpectation = self.expectation(description: "Adds NewCollection")
|
||||
account.refreshAll() { _ in
|
||||
addCollectionExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addcollection")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "newcollection_addcollection")
|
||||
}
|
||||
|
||||
// MARK: Add Feed
|
||||
|
||||
func testAddsFeeds() {
|
||||
prepareBaseline(.addCollection)
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addcollection")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_initial")
|
||||
|
||||
set(testFiles: .addFeed, with: testTransport)
|
||||
|
||||
let addFeedExpectation = self.expectation(description: "Add Feed To Must Read (hey, that rhymes!)")
|
||||
account.refreshAll() { _ in
|
||||
addFeedExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addfeed")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_addfeed")
|
||||
}
|
||||
|
||||
// MARK: Remove Feed
|
||||
|
||||
func testRemovesFeeds() {
|
||||
prepareBaseline(.addFeed)
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addfeed")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_addfeed")
|
||||
|
||||
set(testFiles: .removeFeed, with: testTransport)
|
||||
|
||||
let removeFeedExpectation = self.expectation(description: "Remove Feed from Must Read")
|
||||
account.refreshAll() { _ in
|
||||
removeFeedExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addcollection")
|
||||
checkArticles(againstItemsInStreamInJSONNamed: "mustread_initial")
|
||||
}
|
||||
|
||||
func testRemoveCollection() {
|
||||
prepareBaseline(.addFeed)
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_addfeed")
|
||||
|
||||
set(testFiles: .removeCollection, with: testTransport)
|
||||
|
||||
let removeCollectionExpectation = self.expectation(description: "Remove Collection")
|
||||
account.refreshAll() { _ in
|
||||
removeCollectionExpectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
|
||||
checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed: "feedly_collections_initial")
|
||||
}
|
||||
|
||||
// MARK: Utility
|
||||
|
||||
func prepareBaseline(_ testFiles: TestFiles) {
|
||||
XCTAssertTrue(account.idToFeedDictionary.isEmpty, "Expected to be testing a fresh accout.")
|
||||
|
||||
set(testFiles: testFiles, with: testTransport)
|
||||
|
||||
// Test initial folders for collections and feeds for collection feeds.
|
||||
let preparationExpectation = self.expectation(description: "Prepare Account")
|
||||
account.refreshAll() { _ in
|
||||
preparationExpectation.fulfill()
|
||||
}
|
||||
// If there's a failure here, then an operation hasn't completed.
|
||||
// Check that test files have responses for all the requests this might make.
|
||||
waitForExpectations(timeout: 5)
|
||||
}
|
||||
|
||||
func checkFoldersAndFeeds(againstCollectionsAndFeedsInJSONNamed name: String) {
|
||||
let collections = testJSON(named: name) as! [[String:Any]]
|
||||
let collectionNames = Set(collections.map { $0["label"] as! String })
|
||||
let collectionIds = Set(collections.map { $0["id"] as! String })
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.name })
|
||||
let folderIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let missingNames = collectionNames.subtracting(folderNames)
|
||||
let missingIds = collectionIds.subtracting(folderIds)
|
||||
|
||||
XCTAssertEqual(folders.count, collections.count, "Mismatch between collections and folders.")
|
||||
XCTAssertTrue(missingNames.isEmpty, "Collections with these names did not have a corresponding folder with the same name.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids did not have a corresponding folder with the same id.")
|
||||
|
||||
for collection in collections {
|
||||
checkSingleFolderAndFeeds(againstOneCollectionAndFeedsInJSONPayload: collection)
|
||||
}
|
||||
}
|
||||
|
||||
func checkSingleFolderAndFeeds(againstOneCollectionAndFeedsInJSONNamed name: String) {
|
||||
let collection = testJSON(named: name) as! [String:Any]
|
||||
checkSingleFolderAndFeeds(againstOneCollectionAndFeedsInJSONPayload: collection)
|
||||
}
|
||||
|
||||
func checkSingleFolderAndFeeds(againstOneCollectionAndFeedsInJSONPayload collection: [String: Any]) {
|
||||
let label = collection["label"] as! String
|
||||
guard let folder = account.existingFolder(with: label) else {
|
||||
// due to a previous test failure?
|
||||
XCTFail("Could not find the \"\(label)\" folder.")
|
||||
return
|
||||
}
|
||||
let collectionFeeds = collection["feeds"] as! [[String: Any]]
|
||||
let folderFeeds = folder.topLevelFeeds
|
||||
|
||||
XCTAssertEqual(collectionFeeds.count, folderFeeds.count)
|
||||
|
||||
let collectionFeedIds = Set(collectionFeeds.map { $0["id"] as! String })
|
||||
let folderFeedIds = Set(folderFeeds.map { $0.feedID })
|
||||
let missingFeedIds = collectionFeedIds.subtracting(folderFeedIds)
|
||||
|
||||
XCTAssertTrue(missingFeedIds.isEmpty, "Feeds with these ids were not found in the \"\(label)\" folder.")
|
||||
}
|
||||
|
||||
func checkArticles(againstItemsInStreamInJSONNamed name: String) {
|
||||
let stream = testJSON(named: name) as! [String:Any]
|
||||
checkArticles(againstItemsInStreamInJSONPayload: stream)
|
||||
}
|
||||
|
||||
func checkArticles(againstItemsInStreamInJSONPayload stream: [String: Any]) {
|
||||
|
||||
struct ArticleItem {
|
||||
var id: String
|
||||
var feedId: String
|
||||
var content: String
|
||||
var JSON: [String: Any]
|
||||
var unread: Bool
|
||||
|
||||
/// Convoluted external URL logic "documented" here:
|
||||
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
|
||||
var externalUrl: String? {
|
||||
return ((JSON["canonical"] as? [[String: Any]]) ?? (JSON["alternate"] as? [[String: Any]]))?.compactMap { link -> String? in
|
||||
let href = link["href"] as? String
|
||||
if let type = link["type"] as? String {
|
||||
if type == "text/html" {
|
||||
return href
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return href
|
||||
}.first
|
||||
}
|
||||
|
||||
init(item: [String: Any]) {
|
||||
self.JSON = item
|
||||
self.id = item["id"] as! String
|
||||
|
||||
let origin = item["origin"] as! [String: Any]
|
||||
self.feedId = origin["streamId"] as! String
|
||||
|
||||
let content = item["content"] as? [String: Any]
|
||||
let summary = item["summary"] as? [String: Any]
|
||||
self.content = ((content ?? summary)?["content"] as? String) ?? ""
|
||||
|
||||
self.unread = item["unread"] as! Bool
|
||||
}
|
||||
}
|
||||
|
||||
let items = stream["items"] as! [[String: Any]]
|
||||
let articleItems = items.map { ArticleItem(item: $0) }
|
||||
let itemIds = Set(articleItems.map { $0.id })
|
||||
|
||||
let articles = account.fetchArticles(.articleIDs(itemIds))
|
||||
let articleIds = Set(articles.map { $0.articleID })
|
||||
|
||||
let missing = itemIds.subtracting(articleIds)
|
||||
|
||||
XCTAssertEqual(items.count, articles.count)
|
||||
XCTAssertTrue(missing.isEmpty, "Items with these ids did not have a corresponding article with the same id.")
|
||||
|
||||
for article in articles {
|
||||
for item in articleItems where item.id == article.articleID {
|
||||
|
||||
XCTAssertEqual(article.uniqueID, item.id)
|
||||
XCTAssertEqual(article.contentHTML, item.content)
|
||||
XCTAssertEqual(article.feedID, item.feedId)
|
||||
XCTAssertEqual(article.externalURL, item.externalUrl)
|
||||
// XCTAssertEqual(article.status.boolStatus(forKey: .read), item.unread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testJSON(named: String) -> Any {
|
||||
let bundle = Bundle(for: TestTransport.self)
|
||||
let url = bundle.url(forResource: named, withExtension: "json")!
|
||||
let data = try! Data(contentsOf: url)
|
||||
let json = try! JSONSerialization.jsonObject(with: data)
|
||||
return json
|
||||
}
|
||||
|
||||
enum TestFiles {
|
||||
case initial
|
||||
case addCollection
|
||||
case addFeed
|
||||
case removeFeed
|
||||
case removeCollection
|
||||
}
|
||||
|
||||
func set(testFiles: TestFiles, with transport: TestTransport) {
|
||||
// TestTransport blacklists certain query items to make mocking responses easier.
|
||||
let collectionsEndpoint = "/v3/collections"
|
||||
switch testFiles {
|
||||
case .initial:
|
||||
let dict = [
|
||||
"/global.saved": "saved_initial.json",
|
||||
collectionsEndpoint: "feedly_collections_initial.json",
|
||||
"/5ca4d61d-e55d-4999-a8d1-c3b9d8789815": "macintosh_initial.json",
|
||||
"/global.must": "mustread_initial.json",
|
||||
"/885f2e01-d314-4e63-abac-17dcb063f5b5": "programming_initial.json",
|
||||
"/66132046-6f14-488d-b590-8e93422723c8": "uncategorized_initial.json",
|
||||
"/e31b3fcb-27f6-4f3e-b96c-53902586e366": "weblogs_initial.json",
|
||||
]
|
||||
transport.testFiles = dict
|
||||
|
||||
case .addCollection:
|
||||
set(testFiles: .initial, with: transport)
|
||||
|
||||
var dict = transport.testFiles
|
||||
dict[collectionsEndpoint] = "feedly_collections_addcollection.json"
|
||||
dict["/fc09f383-5a9a-4daa-a575-3efc1733b173"] = "newcollection_addcollection.json"
|
||||
transport.testFiles = dict
|
||||
|
||||
case .addFeed:
|
||||
set(testFiles: .addCollection, with: transport)
|
||||
|
||||
var dict = transport.testFiles
|
||||
dict[collectionsEndpoint] = "feedly_collections_addfeed.json"
|
||||
dict["/global.must"] = "mustread_addfeed.json"
|
||||
transport.testFiles = dict
|
||||
|
||||
case .removeFeed:
|
||||
set(testFiles: .addCollection, with: transport)
|
||||
|
||||
case .removeCollection:
|
||||
set(testFiles: .initial, with: transport)
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// FeedlyCheckpointOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 25/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyCheckpointOperationTests: XCTestCase {
|
||||
|
||||
class TestDelegate: FeedlyCheckpointOperationDelegate {
|
||||
|
||||
var didReachCheckpointExpectation: XCTestExpectation?
|
||||
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
|
||||
didReachCheckpointExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func testCallback() {
|
||||
let delegate = TestDelegate()
|
||||
delegate.didReachCheckpointExpectation = expectation(description: "Did Reach Checkpoint")
|
||||
|
||||
let operation = FeedlyCheckpointOperation()
|
||||
operation.checkpointDelegate = delegate
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
operation.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(operation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testCancellation() {
|
||||
let didReachCheckpointExpectation = expectation(description: "Did Reach Checkpoint")
|
||||
didReachCheckpointExpectation.isInverted = true
|
||||
|
||||
let delegate = TestDelegate()
|
||||
delegate.didReachCheckpointExpectation = didReachCheckpointExpectation
|
||||
|
||||
let operation = FeedlyCheckpointOperation()
|
||||
operation.checkpointDelegate = delegate
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
operation.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(operation)
|
||||
|
||||
operation.cancel()
|
||||
|
||||
waitForExpectations(timeout: 1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// FeedlyCreateFeedsForCollectionFoldersOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 22/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyCreateFeedsForCollectionFoldersOperationTests: 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()
|
||||
}
|
||||
|
||||
class FeedsAndFoldersProvider: FeedlyFeedsAndFoldersProviding {
|
||||
var feedsAndFolders = [([FeedlyFeed], Folder)]()
|
||||
}
|
||||
|
||||
func testAddFeeds() {
|
||||
let feedsForFolderOne = [
|
||||
FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil),
|
||||
FeedlyFeed(feedId: "feed/2", id: "feed/2", title: "Feed Two", updated: nil, website: nil)
|
||||
]
|
||||
|
||||
let feedsForFolderTwo = [
|
||||
FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil),
|
||||
FeedlyFeed(feedId: "feed/3", id: "feed/3", title: "Feed Three", updated: nil, website: nil),
|
||||
]
|
||||
|
||||
let folderOne: (name: String, id: String) = ("FolderOne", "folder/1")
|
||||
let folderTwo: (name: String, id: String) = ("FolderTwo", "folder/2")
|
||||
let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)]
|
||||
|
||||
let provider = FeedsAndFoldersProvider()
|
||||
provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in
|
||||
let accountFolder = account.ensureFolder(with: folder.name)!
|
||||
accountFolder.externalID = folder.id
|
||||
return (feeds, accountFolder)
|
||||
}
|
||||
|
||||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
createFeeds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(account.flattenedFeeds().isEmpty, "Expected empty account.")
|
||||
|
||||
OperationQueue.main.addOperation(createFeeds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let feedIds = Set([feedsForFolderOne, feedsForFolderTwo]
|
||||
.flatMap { $0 }
|
||||
.map { $0.feedId })
|
||||
|
||||
let feedTitles = Set([feedsForFolderOne, feedsForFolderTwo]
|
||||
.flatMap { $0 }
|
||||
.map { $0.title })
|
||||
|
||||
let accountFeeds = account.flattenedFeeds()
|
||||
let ingestedIds = Set(accountFeeds.map { $0.feedID })
|
||||
let ingestedTitles = Set(accountFeeds.map { $0.nameForDisplay })
|
||||
|
||||
let missingIds = feedIds.subtracting(ingestedIds)
|
||||
let missingTitles = feedTitles.subtracting(ingestedTitles)
|
||||
|
||||
XCTAssertTrue(missingIds.isEmpty, "Failed to ingest feeds with these ids.")
|
||||
XCTAssertTrue(missingTitles.isEmpty, "Failed to ingest feeds with these titles.")
|
||||
|
||||
let expectedFolderAndFeedIds = namesAndFeeds
|
||||
.sorted { $0.0.id < $1.0.id }
|
||||
.map { folder, feeds -> [String: [String]] in
|
||||
return [folder.id: feeds.map { $0.feedId }.sorted(by: <)]
|
||||
}
|
||||
|
||||
let ingestedFolderAndFeedIds = (account.folders ?? Set())
|
||||
.sorted { $0.externalID! < $1.externalID! }
|
||||
.compactMap { folder -> [String: [String]]? in
|
||||
return [folder.externalID!: folder.topLevelFeeds.map { $0.feedID }.sorted(by: <)]
|
||||
}
|
||||
|
||||
XCTAssertEqual(expectedFolderAndFeedIds, ingestedFolderAndFeedIds, "Did not ingest feeds in their corresponding folders.")
|
||||
}
|
||||
|
||||
func testRemoveFeeds() {
|
||||
let folderOne: (name: String, id: String) = ("FolderOne", "folder/1")
|
||||
let folderTwo: (name: String, id: String) = ("FolderTwo", "folder/2")
|
||||
let feedToRemove = FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil)
|
||||
|
||||
var feedsForFolderOne = [
|
||||
feedToRemove,
|
||||
FeedlyFeed(feedId: "feed/2", id: "feed/2", title: "Feed Two", updated: nil, website: nil)
|
||||
]
|
||||
|
||||
var feedsForFolderTwo = [
|
||||
feedToRemove,
|
||||
FeedlyFeed(feedId: "feed/3", id: "feed/3", title: "Feed Three", updated: nil, website: nil),
|
||||
]
|
||||
|
||||
// Add initial content.
|
||||
do {
|
||||
let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)]
|
||||
|
||||
let provider = FeedsAndFoldersProvider()
|
||||
provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in
|
||||
let accountFolder = account.ensureFolder(with: folder.name)!
|
||||
accountFolder.externalID = folder.id
|
||||
return (feeds, accountFolder)
|
||||
}
|
||||
|
||||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
createFeeds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(account.flattenedFeeds().isEmpty, "Expected empty account.")
|
||||
|
||||
OperationQueue.main.addOperation(createFeeds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
feedsForFolderOne.removeAll { $0.id == feedToRemove.id }
|
||||
feedsForFolderTwo.removeAll { $0.id == feedToRemove.id }
|
||||
let namesAndFeeds = [(folderOne, feedsForFolderOne), (folderTwo, feedsForFolderTwo)]
|
||||
|
||||
let provider = FeedsAndFoldersProvider()
|
||||
provider.feedsAndFolders = namesAndFeeds.map { (folder, feeds) in
|
||||
let accountFolder = account.ensureFolder(with: folder.name)!
|
||||
accountFolder.externalID = folder.id
|
||||
return (feeds, accountFolder)
|
||||
}
|
||||
|
||||
let removeFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
removeFeeds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(removeFeeds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let feedIds = Set([feedsForFolderOne, feedsForFolderTwo]
|
||||
.flatMap { $0 }
|
||||
.map { $0.feedId })
|
||||
|
||||
let feedTitles = Set([feedsForFolderOne, feedsForFolderTwo]
|
||||
.flatMap { $0 }
|
||||
.map { $0.title })
|
||||
|
||||
let accountFeeds = account.flattenedFeeds()
|
||||
let ingestedIds = Set(accountFeeds.map { $0.feedID })
|
||||
let ingestedTitles = Set(accountFeeds.map { $0.nameForDisplay })
|
||||
|
||||
XCTAssertEqual(ingestedIds.count, feedIds.count)
|
||||
XCTAssertEqual(ingestedTitles.count, feedTitles.count)
|
||||
|
||||
let missingIds = feedIds.subtracting(ingestedIds)
|
||||
let missingTitles = feedTitles.subtracting(ingestedTitles)
|
||||
|
||||
XCTAssertTrue(missingIds.isEmpty, "Failed to ingest feeds with these ids.")
|
||||
XCTAssertTrue(missingTitles.isEmpty, "Failed to ingest feeds with these titles.")
|
||||
|
||||
let expectedFolderAndFeedIds = namesAndFeeds
|
||||
.sorted { $0.0.id < $1.0.id }
|
||||
.map { folder, feeds -> [String: [String]] in
|
||||
return [folder.id: feeds.map { $0.feedId }.sorted(by: <)]
|
||||
}
|
||||
|
||||
let ingestedFolderAndFeedIds = (account.folders ?? Set())
|
||||
.sorted { $0.externalID! < $1.externalID! }
|
||||
.compactMap { folder -> [String: [String]]? in
|
||||
return [folder.externalID!: folder.topLevelFeeds.map { $0.feedID }.sorted(by: <)]
|
||||
}
|
||||
|
||||
XCTAssertEqual(expectedFolderAndFeedIds, ingestedFolderAndFeedIds, "Did not ingest feeds to their corresponding folders.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// FeedlyGetCollectionsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 21/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import os.log
|
||||
|
||||
class FeedlyGetCollectionsOperationTests: XCTestCase {
|
||||
|
||||
func testGetCollections() {
|
||||
let support = FeedlyTestSupport()
|
||||
let (transport, caller) = support.makeMockNetworkStack()
|
||||
let jsonName = "feedly_collections_initial"
|
||||
transport.testFiles["/v3/collections"] = "\(jsonName).json"
|
||||
|
||||
let getCollections = FeedlyGetCollectionsOperation(service: caller, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getCollections.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getCollections)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let collections = support.testJSON(named: jsonName) as! [[String:Any]]
|
||||
let labelsInJSON = Set(collections.map { $0["label"] as! String })
|
||||
let idsInJSON = Set(collections.map { $0["id"] as! String })
|
||||
|
||||
let labels = Set(getCollections.collections.map { $0.label })
|
||||
let ids = Set(getCollections.collections.map { $0.id })
|
||||
|
||||
let missingLabels = labelsInJSON.subtracting(labels)
|
||||
let missingIds = idsInJSON.subtracting(ids)
|
||||
|
||||
XCTAssertEqual(getCollections.collections.count, collections.count, "Mismatch between collections provided by operation and test JSON collections.")
|
||||
XCTAssertTrue(missingLabels.isEmpty, "Collections with these labels did not have a corresponding \(FeedlyCollection.self) value with the same name.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids did not have a corresponding \(FeedlyCollection.self) with the same id.")
|
||||
|
||||
for collection in collections {
|
||||
let collectionId = collection["id"] as! String
|
||||
let collectionFeeds = collection["feeds"] as! [[String: Any]]
|
||||
let collectionFeedIds = Set(collectionFeeds.map { $0["id"] as! String })
|
||||
|
||||
for operationCollection in getCollections.collections where operationCollection.id == collectionId {
|
||||
let feedIds = Set(operationCollection.feeds.map { $0.id })
|
||||
let missingIds = collectionFeedIds.subtracting(feedIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "Feeds with these ids were not found in the \"\(operationCollection.label)\" \(FeedlyCollection.self).")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testGetCollectionsError() {
|
||||
|
||||
class TestDelegate: FeedlyOperationDelegate {
|
||||
var errorExpectation: XCTestExpectation?
|
||||
var error: Error?
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
self.error = error
|
||||
errorExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
let delegate = TestDelegate()
|
||||
delegate.errorExpectation = expectation(description: "Did Fail With Expected Error")
|
||||
|
||||
let support = FeedlyTestSupport()
|
||||
let service = TestGetCollectionsService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
|
||||
let getCollections = FeedlyGetCollectionsOperation(service: service, log: support.log)
|
||||
getCollections.delegate = delegate
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getCollections.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getCollections)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertNotNil(delegate.error)
|
||||
XCTAssertTrue(getCollections.collections.isEmpty, "Collections should be empty.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// FeedlyGetStreamContentsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 23/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyGetStreamContentsOperationTests: 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 testGetStreamContentsFailure() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: service, continuation: nil, newerThan: nil, unreadOnly: nil)
|
||||
|
||||
service.mockResult = .failure(URLError(.fileDoesNotExist))
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertNil(getStreamContents.stream)
|
||||
}
|
||||
|
||||
func testValuesPassingForGetStreamContents() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let continuation: String? = "abcdefg"
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 86)
|
||||
let unreadOnly: Bool? = true
|
||||
|
||||
let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: service, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly)
|
||||
|
||||
let mockStream = FeedlyStream(id: "stream/1", updated: nil, continuation: nil, items: [])
|
||||
service.mockResult = .success(mockStream)
|
||||
service.getStreamContentsExpectation = expectation(description: "Did Call Service")
|
||||
service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in
|
||||
// Verify these values given to the opeartion are passed to the service.
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertEqual(serviceContinuation, continuation)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertEqual(serviceUnreadOnly, unreadOnly)
|
||||
}
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
guard let stream = getStreamContents.stream else {
|
||||
XCTFail("\(FeedlyGetStreamContentsOperation.self) did not store the stream.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(stream.id, mockStream.id)
|
||||
XCTAssertEqual(stream.updated, mockStream.updated)
|
||||
XCTAssertEqual(stream.continuation, mockStream.continuation)
|
||||
|
||||
let streamIds = stream.items.map { $0.id }
|
||||
let mockStreamIds = mockStream.items.map { $0.id }
|
||||
XCTAssertEqual(streamIds, mockStreamIds)
|
||||
}
|
||||
|
||||
func testGetStreamContentsFromJSON() {
|
||||
let support = FeedlyTestSupport()
|
||||
let (transport, caller) = support.makeMockNetworkStack()
|
||||
let jsonName = "feedly_macintosh_initial"
|
||||
transport.testFiles["/v3/streams/contents"] = "\(jsonName).json"
|
||||
|
||||
let resource = FeedlyCategoryResourceId(id: "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815")
|
||||
let getStreamContents = FeedlyGetStreamContentsOperation(account: account, resource: resource, service: caller, continuation: nil, newerThan: nil, unreadOnly: nil)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
// verify entry providing and parsed item providing
|
||||
guard let stream = getStreamContents.stream else {
|
||||
return XCTFail("Expected to have stream.")
|
||||
}
|
||||
|
||||
let streamJSON = support.testJSON(named: jsonName) as! [String:Any]
|
||||
|
||||
let id = streamJSON["id"] as! String
|
||||
XCTAssertEqual(stream.id, id)
|
||||
|
||||
let milliseconds = streamJSON["updated"] as! Double
|
||||
let updated = Date(timeIntervalSince1970: TimeInterval(milliseconds / 1000))
|
||||
XCTAssertEqual(stream.updated, updated)
|
||||
|
||||
let continuation = streamJSON["continuation"] as! String
|
||||
XCTAssertEqual(stream.continuation, continuation)
|
||||
|
||||
support.check(getStreamContents.entries, correspondToStreamItemsIn: streamJSON)
|
||||
support.check(stream.items, correspondToStreamItemsIn: streamJSON)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// FeedlyGetStreamIdsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 23/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyGetStreamIdsOperationTests: 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 testGetStreamIdsFailure() {
|
||||
let service = TestGetStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: service, continuation: nil, newerThan: nil, unreadOnly: nil)
|
||||
|
||||
service.mockResult = .failure(URLError(.fileDoesNotExist))
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamIds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamIds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertNil(getStreamIds.streamIds)
|
||||
}
|
||||
|
||||
func testValuesPassingForGetStreamIds() {
|
||||
let service = TestGetStreamIdsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
|
||||
let continuation: String? = "gfdsa"
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 1000)
|
||||
let unreadOnly: Bool? = false
|
||||
|
||||
let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: service, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly)
|
||||
|
||||
let mockStreamIds = FeedlyStreamIds(continuation: "1234", ids: ["item/1", "item/2", "item/3"])
|
||||
service.mockResult = .success(mockStreamIds)
|
||||
service.getStreamIdsExpectation = expectation(description: "Did Call Service")
|
||||
service.parameterTester = { serviceResource, serviceContinuation, serviceNewerThan, serviceUnreadOnly in
|
||||
// Verify these values given to the opeartion are passed to the service.
|
||||
XCTAssertEqual(serviceResource.id, resource.id)
|
||||
XCTAssertEqual(serviceContinuation, continuation)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertEqual(serviceUnreadOnly, unreadOnly)
|
||||
}
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamIds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamIds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
guard let streamIds = getStreamIds.streamIds else {
|
||||
XCTFail("\(FeedlyGetStreamIdsOperation.self) did not store the stream.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(streamIds.continuation, mockStreamIds.continuation)
|
||||
XCTAssertEqual(streamIds.ids, mockStreamIds.ids)
|
||||
}
|
||||
|
||||
func testGetStreamIdsFromJSON() {
|
||||
let support = FeedlyTestSupport()
|
||||
let (transport, caller) = support.makeMockNetworkStack()
|
||||
let jsonName = "feedly_unreads_1000"
|
||||
transport.testFiles["/v3/streams/ids"] = "\(jsonName).json"
|
||||
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let getStreamIds = FeedlyGetStreamIdsOperation(account: account, resource: resource, service: caller, continuation: nil, newerThan: nil, unreadOnly: nil)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
getStreamIds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(getStreamIds)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
guard let streamIds = getStreamIds.streamIds else {
|
||||
return XCTFail("Expected to have a stream of identifiers.")
|
||||
}
|
||||
|
||||
let streamIdsJSON = support.testJSON(named: jsonName) as! [String:Any]
|
||||
|
||||
let continuation = streamIdsJSON["continuation"] as! String
|
||||
XCTAssertEqual(streamIds.continuation, continuation)
|
||||
XCTAssertEqual(streamIds.ids, streamIdsJSON["ids"] as! [String])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
//
|
||||
// FeedlyMirrorCollectionsAsFoldersOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 22/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyMirrorCollectionsAsFoldersOperationTests: 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()
|
||||
}
|
||||
|
||||
class CollectionsProvider: FeedlyCollectionProviding {
|
||||
var collections = [
|
||||
FeedlyCollection(feeds: [], label: "One", id: "collections/1"),
|
||||
FeedlyCollection(feeds: [], label: "Two", id: "collections/2")
|
||||
]
|
||||
}
|
||||
|
||||
func testAddsFolders() {
|
||||
let provider = CollectionsProvider()
|
||||
let mirrorOperation = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
mirrorOperation.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(mirrorOperation.collectionsAndFolders.isEmpty)
|
||||
XCTAssertTrue(mirrorOperation.feedsAndFolders.isEmpty)
|
||||
|
||||
OperationQueue.main.addOperation(mirrorOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.nameForDisplay })
|
||||
let folderExternalIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let collectionLabels = Set(provider.collections.map { $0.label })
|
||||
let collectionIds = Set(provider.collections.map { $0.id })
|
||||
|
||||
let missingNames = collectionLabels.subtracting(folderNames)
|
||||
let missingIds = collectionIds.subtracting(folderExternalIds)
|
||||
|
||||
XCTAssertTrue(missingNames.isEmpty, "Collections with these labels have no corresponding folder.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids have no corresponding folder.")
|
||||
XCTAssertEqual(mirrorOperation.collectionsAndFolders.count, provider.collections.count, "Mismatch between collections and folders.")
|
||||
}
|
||||
|
||||
func testRemovesFolders() {
|
||||
let provider = CollectionsProvider()
|
||||
|
||||
do {
|
||||
let addFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
addFolders.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(addFolders)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
// Now that the folders are added, remove them all.
|
||||
provider.collections = []
|
||||
|
||||
let removeFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
removeFolders.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(removeFolders)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.nameForDisplay })
|
||||
let folderExternalIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let collectionLabels = Set(provider.collections.map { $0.label })
|
||||
let collectionIds = Set(provider.collections.map { $0.id })
|
||||
|
||||
let remainingNames = folderNames.subtracting(collectionLabels)
|
||||
let remainingIds = folderExternalIds.subtracting(collectionIds)
|
||||
|
||||
XCTAssertTrue(remainingNames.isEmpty, "Folders with these names remain with no corresponding collection.")
|
||||
XCTAssertTrue(remainingIds.isEmpty, "Folders with these ids remain with no corresponding collection.")
|
||||
|
||||
XCTAssertTrue(removeFolders.collectionsAndFolders.isEmpty)
|
||||
XCTAssertTrue(removeFolders.feedsAndFolders.isEmpty)
|
||||
}
|
||||
|
||||
class CollectionsAndFeedsProvider: FeedlyCollectionProviding {
|
||||
var feedsForCollectionOne = [
|
||||
FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil),
|
||||
FeedlyFeed(feedId: "feed/2", id: "feed/2", title: "Feed Two", updated: nil, website: nil)
|
||||
]
|
||||
|
||||
var feedsForCollectionTwo = [
|
||||
FeedlyFeed(feedId: "feed/1", id: "feed/1", title: "Feed One", updated: nil, website: nil),
|
||||
FeedlyFeed(feedId: "feed/3", id: "feed/3", title: "Feed Three", updated: nil, website: nil),
|
||||
]
|
||||
|
||||
var collections: [FeedlyCollection] {
|
||||
return [
|
||||
FeedlyCollection(feeds: feedsForCollectionOne, label: "One", id: "collections/1"),
|
||||
FeedlyCollection(feeds: feedsForCollectionTwo, label: "Two", id: "collections/2")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
func testFeedMappedToFolders() {
|
||||
let provider = CollectionsAndFeedsProvider()
|
||||
let mirrorOperation = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
mirrorOperation.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(mirrorOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.nameForDisplay })
|
||||
let folderExternalIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let collectionLabels = Set(provider.collections.map { $0.label })
|
||||
let collectionIds = Set(provider.collections.map { $0.id })
|
||||
|
||||
let missingNames = collectionLabels.subtracting(folderNames)
|
||||
let missingIds = collectionIds.subtracting(folderExternalIds)
|
||||
|
||||
XCTAssertTrue(missingNames.isEmpty, "Collections with these labels have no corresponding folder.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids have no corresponding folder.")
|
||||
|
||||
let collectionIdsAndFeedIds = provider.collections.map { collection -> [String:[String]] in
|
||||
return [collection.id: collection.feeds.map { $0.id }.sorted(by: <)]
|
||||
}
|
||||
|
||||
let folderIdsAndFeedIds = mirrorOperation.feedsAndFolders.compactMap { feeds, folder -> [String:[String]]? in
|
||||
guard let id = folder.externalID else {
|
||||
return nil
|
||||
}
|
||||
return [id: feeds.map { $0.id }.sorted(by: <)]
|
||||
}
|
||||
|
||||
XCTAssertEqual(collectionIdsAndFeedIds, folderIdsAndFeedIds, "Did not map folders to feeds correctly.")
|
||||
}
|
||||
|
||||
func testRemovingFolderRemovesFeeds() {
|
||||
do {
|
||||
let provider = CollectionsAndFeedsProvider()
|
||||
let addFoldersAndFeeds = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
|
||||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addFoldersAndFeeds, log: support.log)
|
||||
createFeeds.addDependency(addFoldersAndFeeds)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
createFeeds.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperations([addFoldersAndFeeds, createFeeds], waitUntilFinished: false)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertFalse(account.flattenedFeeds().isEmpty, "Expected account to have feeds.")
|
||||
}
|
||||
|
||||
// Now that the folders are added, remove them all.
|
||||
let provider = CollectionsProvider()
|
||||
provider.collections = []
|
||||
|
||||
let removeFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: provider, log: support.log)
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
removeFolders.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(removeFolders)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let feeds = account.flattenedFeeds()
|
||||
|
||||
XCTAssertTrue(feeds.isEmpty)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
//
|
||||
// FeedlyOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 21/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyOperationTests: XCTestCase {
|
||||
|
||||
enum TestOperationError: Error, Equatable {
|
||||
case mockError
|
||||
case anotherMockError
|
||||
}
|
||||
|
||||
final class TestOperation: FeedlyOperation {
|
||||
var didCallMainExpectation: XCTestExpectation?
|
||||
var mockError: Error?
|
||||
|
||||
override func main() {
|
||||
// Should always call on main thread.
|
||||
XCTAssertTrue(Thread.isMainThread)
|
||||
|
||||
didCallMainExpectation?.fulfill()
|
||||
|
||||
if let error = mockError {
|
||||
didFinish(error)
|
||||
} else {
|
||||
didFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class TestDelegate: FeedlyOperationDelegate {
|
||||
|
||||
var error: Error?
|
||||
var didFailExpectation: XCTestExpectation?
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
didFailExpectation?.fulfill()
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
func testDoesCallMain() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
|
||||
OperationQueue.main.addOperation(testOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testDoesFail() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
testOperation.mockError = TestOperationError.mockError
|
||||
|
||||
let delegate = TestDelegate()
|
||||
delegate.didFailExpectation = expectation(description: "Operation Failed As Expected")
|
||||
|
||||
testOperation.delegate = delegate
|
||||
|
||||
OperationQueue.main.addOperation(testOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
if let error = delegate.error as? TestOperationError {
|
||||
XCTAssertEqual(error, TestOperationError.mockError)
|
||||
} else {
|
||||
XCTFail("Expected \(TestOperationError.self) but got \(String(describing: delegate.error)).")
|
||||
}
|
||||
}
|
||||
|
||||
func testOperationFlags() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
|
||||
let completionExpectation = expectation(description: "Operation Completed")
|
||||
testOperation.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(testOperation.isReady)
|
||||
XCTAssertFalse(testOperation.isFinished)
|
||||
XCTAssertFalse(testOperation.isExecuting)
|
||||
XCTAssertFalse(testOperation.isCancelled)
|
||||
|
||||
OperationQueue.main.addOperation(testOperation)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertTrue(testOperation.isReady)
|
||||
XCTAssertTrue(testOperation.isFinished)
|
||||
XCTAssertFalse(testOperation.isExecuting)
|
||||
XCTAssertFalse(testOperation.isCancelled)
|
||||
}
|
||||
|
||||
func testOperationCancellationFlags() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
|
||||
let completionExpectation = expectation(description: "Operation Completed")
|
||||
testOperation.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
XCTAssertTrue(testOperation.isReady)
|
||||
XCTAssertFalse(testOperation.isFinished)
|
||||
XCTAssertFalse(testOperation.isExecuting)
|
||||
XCTAssertFalse(testOperation.isCancelled)
|
||||
|
||||
OperationQueue.main.addOperation(testOperation)
|
||||
|
||||
testOperation.cancel()
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertTrue(testOperation.isReady)
|
||||
XCTAssertTrue(testOperation.isFinished)
|
||||
XCTAssertFalse(testOperation.isExecuting)
|
||||
XCTAssertTrue(testOperation.isCancelled)
|
||||
}
|
||||
|
||||
func testDependency() {
|
||||
let testOperation = TestOperation()
|
||||
testOperation.didCallMainExpectation = expectation(description: "Did Call Main")
|
||||
|
||||
let dependencyExpectation = expectation(description: "Did Call Dependency")
|
||||
let blockOperation = BlockOperation {
|
||||
dependencyExpectation.fulfill()
|
||||
}
|
||||
|
||||
blockOperation.addDependency(testOperation)
|
||||
|
||||
XCTAssertTrue(blockOperation.dependencies.contains(testOperation))
|
||||
|
||||
OperationQueue.main.addOperations([testOperation, blockOperation], waitUntilFinished: false)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// FeedlyOrganiseParsedItemsByFeedOperationTests.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 FeedlyOrganiseParsedItemsByFeedOperationTests: 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()
|
||||
}
|
||||
|
||||
struct TestParsedItemsProvider: FeedlyParsedItemProviding {
|
||||
var resource: FeedlyResourceId
|
||||
var parsedEntries: Set<ParsedItem>
|
||||
}
|
||||
|
||||
func testNoEntries() {
|
||||
let entries = support.makeParsedItemTestDataFor(numberOfFeeds: 0, numberOfItemsInFeeds: 0)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let parsedEntries = Set(entries.values.flatMap { $0 })
|
||||
let provider = TestParsedItemsProvider(resource: resource, parsedEntries: parsedEntries)
|
||||
|
||||
let organise = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
organise.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(organise)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
|
||||
func testGroupsOneEntryByFeedId() {
|
||||
let entries = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let parsedEntries = Set(entries.values.flatMap { $0 })
|
||||
let provider = TestParsedItemsProvider(resource: resource, parsedEntries: parsedEntries)
|
||||
|
||||
let organise = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
organise.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(organise)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
|
||||
func testGroupsManyEntriesByFeedId() {
|
||||
let entries = support.makeParsedItemTestDataFor(numberOfFeeds: 100, numberOfItemsInFeeds: 100)
|
||||
let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
|
||||
let parsedEntries = Set(entries.values.flatMap { $0 })
|
||||
let provider = TestParsedItemsProvider(resource: resource, parsedEntries: parsedEntries)
|
||||
|
||||
let organise = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
organise.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(organise)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let itemsAndFeedIds = organise.parsedItemsKeyedByFeedId
|
||||
XCTAssertEqual(itemsAndFeedIds, entries)
|
||||
XCTAssertEqual(resource.id, organise.providerName)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,401 @@
|
|||
//
|
||||
// FeedlySendArticleStatusesOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 25/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import SyncDatabase
|
||||
import Articles
|
||||
|
||||
class FeedlySendArticleStatusesOperationTests: XCTestCase {
|
||||
|
||||
private var account: Account!
|
||||
private let support = FeedlyTestSupport()
|
||||
private var container: FeedlyTestSupport.TestDatabaseContainer!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
account = support.makeTestAccount()
|
||||
container = support.makeTestDatabaseContainer()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
container = nil
|
||||
if let account = account {
|
||||
support.destroy(account)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSendEmpty() {
|
||||
let service = TestMarkArticlesService()
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testSendUnreadSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .read, flag: false) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .unread)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendUnreadFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .read, flag: false) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .unread)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
|
||||
func testSendReadSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .read, flag: true) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .read)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendReadFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .read, flag: true) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .read)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
|
||||
func testSendStarredSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .starred, flag: true) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .saved)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendStarredFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .starred, flag: true) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .saved)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
|
||||
func testSendUnstarredSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .starred, flag: false) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .unsaved)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendUnstarredFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let statuses = articleIds.map { SyncStatus(articleID: $0, key: .starred, flag: false) }
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
XCTAssertEqual(serviceArticleIds, articleIds)
|
||||
XCTAssertEqual(action, .unsaved)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
|
||||
func testSendAllSuccess() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let keys = [ArticleStatus.Key.read, .starred]
|
||||
let flags = [true, false]
|
||||
let statuses = articleIds.map { articleId -> SyncStatus in
|
||||
let key = keys.randomElement()!
|
||||
let flag = flags.randomElement()!
|
||||
let status = SyncStatus(articleID: articleId, key: key, flag: flag)
|
||||
return status
|
||||
}
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .success(())
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
let syncStatuses: [SyncStatus]
|
||||
switch action {
|
||||
case .read:
|
||||
syncStatuses = statuses.filter { $0.key == .read && $0.flag == true }
|
||||
case .unread:
|
||||
syncStatuses = statuses.filter { $0.key == .read && $0.flag == false }
|
||||
case .saved:
|
||||
syncStatuses = statuses.filter { $0.key == .starred && $0.flag == true }
|
||||
case .unsaved:
|
||||
syncStatuses = statuses.filter { $0.key == .starred && $0.flag == false }
|
||||
}
|
||||
let expectedArticleIds = Set(syncStatuses.map { $0.articleID })
|
||||
XCTAssertEqual(serviceArticleIds, expectedArticleIds)
|
||||
}
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
XCTAssertEqual(container.database.selectPendingCount(), 0)
|
||||
}
|
||||
|
||||
func testSendAllFailure() {
|
||||
let articleIds = Set((0..<100).map { "feed/0/article/\($0)" })
|
||||
let keys = [ArticleStatus.Key.read, .starred]
|
||||
let flags = [true, false]
|
||||
let statuses = articleIds.map { articleId -> SyncStatus in
|
||||
let key = keys.randomElement()!
|
||||
let flag = flags.randomElement()!
|
||||
let status = SyncStatus(articleID: articleId, key: key, flag: flag)
|
||||
return status
|
||||
}
|
||||
|
||||
let insertExpectation = expectation(description: "Inserted Statuses")
|
||||
container.database.insertStatuses(statuses) {
|
||||
insertExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let service = TestMarkArticlesService()
|
||||
service.mockResult = .failure(URLError(.timedOut))
|
||||
service.parameterTester = { serviceArticleIds, action in
|
||||
let syncStatuses: [SyncStatus]
|
||||
switch action {
|
||||
case .read:
|
||||
syncStatuses = statuses.filter { $0.key == .read && $0.flag == true }
|
||||
case .unread:
|
||||
syncStatuses = statuses.filter { $0.key == .read && $0.flag == false }
|
||||
case .saved:
|
||||
syncStatuses = statuses.filter { $0.key == .starred && $0.flag == true }
|
||||
case .unsaved:
|
||||
syncStatuses = statuses.filter { $0.key == .starred && $0.flag == false }
|
||||
}
|
||||
let expectedArticleIds = Set(syncStatuses.map { $0.articleID })
|
||||
XCTAssertEqual(serviceArticleIds, expectedArticleIds)
|
||||
}
|
||||
|
||||
let send = FeedlySendArticleStatusesOperation(database: container.database, service: service, log: support.log)
|
||||
|
||||
let didFinishExpectation = expectation(description: "Did Finish")
|
||||
send.completionBlock = {
|
||||
didFinishExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(send)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertEqual(container.database.selectPendingCount(), statuses.count)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,398 @@
|
|||
//
|
||||
// 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 accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertTrue(accountArticlesIDs.isEmpty)
|
||||
XCTAssertEqual(accountArticlesIDs, testIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
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 remainingAccountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds)
|
||||
}
|
||||
|
||||
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 remainingAccountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingStarredIds)
|
||||
}
|
||||
|
||||
// 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 accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingStarredIds))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, remainingStarredIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchStarredArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingStarredIds)
|
||||
|
||||
let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value })
|
||||
let someRemainingStarredIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID })
|
||||
let idsOfStarredArticles = Set(account
|
||||
.fetchArticles(.articleIDs(someRemainingStarredIdsOfIngestedArticles))
|
||||
.filter { $0.status.boolStatus(forKey: .starred) == true }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfStarredArticles, someRemainingStarredIdsOfIngestedArticles)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,398 @@
|
|||
//
|
||||
// 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 accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertTrue(accountArticlesIDs.isEmpty)
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs.count, testIds.count)
|
||||
}
|
||||
|
||||
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 remainingAccountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds)
|
||||
}
|
||||
|
||||
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 remainingAccountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(remainingAccountArticlesIDs, remainingUnreadIds)
|
||||
}
|
||||
|
||||
// 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 accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(remainingUnreadIds))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, remainingUnreadIds)
|
||||
}
|
||||
|
||||
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 accountArticlesIDs = account.fetchUnreadArticleIDs()
|
||||
XCTAssertEqual(accountArticlesIDs, remainingUnreadIds)
|
||||
|
||||
let someTestItems = Set(someItemsAndFeeds.flatMap { $0.value })
|
||||
let someRemainingUnreadIdsOfIngestedArticles = Set(someTestItems.compactMap { $0.syncServiceID })
|
||||
let idsOfUnreadArticles = Set(account
|
||||
.fetchArticles(.articleIDs(someRemainingUnreadIdsOfIngestedArticles))
|
||||
.filter { $0.status.boolStatus(forKey: .read) == false }
|
||||
.map { $0.articleID })
|
||||
|
||||
XCTAssertEqual(idsOfUnreadArticles, someRemainingUnreadIdsOfIngestedArticles)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// FeedlySyncAllMockResponseProvider.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 1/11/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class FeedlyMockResponseProvider: TestTransportMockResponseProviding {
|
||||
|
||||
let subdirectory: String
|
||||
|
||||
init(findingMocksIn subdirectory: String) {
|
||||
self.subdirectory = subdirectory
|
||||
}
|
||||
|
||||
func mockResponseFileUrl(for components: URLComponents) -> URL? {
|
||||
let bundle = Bundle(for: FeedlyMockResponseProvider.self)
|
||||
|
||||
// Match request for collections to build a list of folders.
|
||||
if components.path.contains("v3/collections") {
|
||||
return bundle.url(forResource: "collections", withExtension: "json", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
guard let queryItems = components.queryItems else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match requests for starred articles from global.saved.
|
||||
if components.path.contains("streams/contents") &&
|
||||
queryItems.contains(where: { ($0.value ?? "").contains("global.saved") }) {
|
||||
return bundle.url(forResource: "starred", withExtension: "json", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
let continuation = queryItems.first(where: { $0.name.contains("continuation") })?.value
|
||||
|
||||
// Match requests for unread article ids.
|
||||
if components.path.contains("streams/ids") && queryItems.contains(where: { $0.name.contains("unreadOnly") }) {
|
||||
|
||||
// if there is a continuation, return the page for it
|
||||
if let continuation = continuation, let data = continuation.data(using: .utf8) {
|
||||
let base64 = data.base64EncodedString() // at least base64 can be used as a path component.
|
||||
return bundle.url(forResource: "unreadIds@\(base64)", withExtension: "json", subdirectory: subdirectory)
|
||||
|
||||
} else {
|
||||
// return first page
|
||||
return bundle.url(forResource: "unreadIds", withExtension: "json", subdirectory: subdirectory)
|
||||
}
|
||||
}
|
||||
|
||||
// Match requests for the contents of global.all.
|
||||
if components.path.contains("streams/contents") &&
|
||||
queryItems.contains(where: { ($0.value ?? "").contains("global.all") }){
|
||||
|
||||
// if there is a continuation, return the page for it
|
||||
if let continuation = continuation, let data = continuation.data(using: .utf8) {
|
||||
let base64 = data.base64EncodedString() // at least base64 can be used as a path component.
|
||||
return bundle.url(forResource: "global.all@\(base64)", withExtension: "json", subdirectory: subdirectory)
|
||||
|
||||
} else {
|
||||
// return first page
|
||||
return bundle.url(forResource: "global.all", withExtension: "json", subdirectory: subdirectory)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
//
|
||||
// FeedlySyncAllOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 30/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
import RSWeb
|
||||
|
||||
class FeedlySyncAllOperationTests: 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 testCancel() {
|
||||
let markArticlesService = TestMarkArticlesService()
|
||||
markArticlesService.didMarkExpectation = expectation(description: "Set Article Statuses")
|
||||
markArticlesService.didMarkExpectation?.isInverted = true
|
||||
|
||||
let getStreamIdsService = TestGetStreamIdsService()
|
||||
getStreamIdsService.getStreamIdsExpectation = expectation(description: "Get Unread Article Identifiers")
|
||||
getStreamIdsService.getStreamIdsExpectation?.isInverted = true
|
||||
|
||||
let getCollectionsService = TestGetCollectionsService()
|
||||
getCollectionsService.getCollectionsExpectation = expectation(description: "Get User's Collections")
|
||||
getCollectionsService.getCollectionsExpectation?.isInverted = true
|
||||
|
||||
let getGlobalStreamContents = TestGetStreamContentsService()
|
||||
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 container = support.makeTestDatabaseContainer()
|
||||
let syncAll = FeedlySyncAllOperation(account: account,
|
||||
credentials: support.credentials,
|
||||
lastSuccessfulFetchStartDate: nil,
|
||||
markArticlesService: markArticlesService,
|
||||
getUnreadService: getStreamIdsService,
|
||||
getCollectionsService: getCollectionsService,
|
||||
getStreamContentsService: getGlobalStreamContents,
|
||||
getStarredArticlesService: getStarredContents,
|
||||
database: container.database,
|
||||
log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncAll.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
let syncCompletionExpectation = expectation(description: "Did Finish Sync")
|
||||
syncAll.syncCompletionHandler = { result in
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Expected failure.")
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
syncCompletionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncAll)
|
||||
|
||||
syncAll.cancel()
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
XCTAssertNil(syncAll.syncCompletionHandler, "Expected completion handler to be destroyed after completion.")
|
||||
}
|
||||
|
||||
private var transport = TestTransport()
|
||||
lazy var caller: FeedlyAPICaller = {
|
||||
let caller = FeedlyAPICaller(transport: transport, api: .sandbox)
|
||||
caller.credentials = support.credentials
|
||||
return caller
|
||||
}()
|
||||
|
||||
func testSyncing() {
|
||||
performInitialSync()
|
||||
verifyInitialSync()
|
||||
|
||||
performChangeStatuses()
|
||||
verifyChangeStatuses()
|
||||
|
||||
performChangeStatusesAgain()
|
||||
verifyChangeStatusesAgain()
|
||||
|
||||
performAddFeedsAndFolders()
|
||||
verifyAddFeedsAndFolders()
|
||||
}
|
||||
|
||||
// MARK: 1 - Initial Sync
|
||||
|
||||
private func loadMockData(inSubdirectoryNamed subdirectory: String) {
|
||||
let provider = FeedlyMockResponseProvider(findingMocksIn: subdirectory)
|
||||
transport.mockResponseFileUrlProvider = provider
|
||||
|
||||
// lastSuccessfulFetchStartDate does not matter for the test, content will always be the same.
|
||||
// It is tested in `FeedlyGetStreamContentsOperationTests`.
|
||||
let syncAll = FeedlySyncAllOperation(account: account,
|
||||
credentials: support.credentials,
|
||||
caller: caller,
|
||||
database: databaseContainer.database,
|
||||
lastSuccessfulFetchStartDate: nil,
|
||||
log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncAll.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncAll)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func performInitialSync() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-1-initial")
|
||||
}
|
||||
|
||||
func verifyInitialSync() {
|
||||
let subdirectory = "feedly-1-initial"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all@MTZkOTdkZWQ1NzM6NTE2OjUzYjgyNmEy", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOTRhOTNhZTQ6MzExOjUzYjgyNmEy", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: 2 - Change Statuses
|
||||
|
||||
func performChangeStatuses() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-2-changestatuses")
|
||||
}
|
||||
|
||||
func verifyChangeStatuses() {
|
||||
let subdirectory = "feedly-2-changestatuses"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOTJkNjIwM2Q6MTEzYjpkNDUwNjA3MQ==", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: 3 - Change Statuses Again
|
||||
|
||||
func performChangeStatusesAgain() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-3-changestatusesagain")
|
||||
}
|
||||
|
||||
func verifyChangeStatusesAgain() {
|
||||
let subdirectory = "feedly-3-changestatusesagain"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOGRlMjVmM2M6M2YyOmQ0NTA2MDcx", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: 4 - Add Feeds and Folders
|
||||
|
||||
func performAddFeedsAndFolders() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-4-addfeedsandfolders")
|
||||
}
|
||||
|
||||
func verifyAddFeedsAndFolders() {
|
||||
let subdirectory = "feedly-4-addfeedsandfolders"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOTE3YTRlMzQ6YWZjOmQ0NTA2MDcx", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: 5 - Remove Feeds and Folders
|
||||
|
||||
func performRemoveFeedsAndFolders() {
|
||||
loadMockData(inSubdirectoryNamed: "feedly-5-removefeedsandfolders")
|
||||
}
|
||||
|
||||
func verifyRemoveFeedsAndFolders() {
|
||||
let subdirectory = "feedly-5-removefeedsandfolders"
|
||||
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "collections", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "global.all", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory)
|
||||
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds@MTZkOGRlMjVmM2M6M2YxOmQ0NTA2MDcx", subdirectory: subdirectory)
|
||||
support.checkStarredStatuses(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "starred", subdirectory: subdirectory)
|
||||
}
|
||||
|
||||
// MARK: Downloading Test Data
|
||||
|
||||
var lastSuccessfulFetchStartDate: Date?
|
||||
lazy var databaseContainer: FeedlyTestSupport.TestDatabaseContainer = {
|
||||
return support.makeTestDatabaseContainer()
|
||||
}()
|
||||
|
||||
func downloadTestData() {
|
||||
let caller = FeedlyAPICaller(transport: URLSession.webserviceTransport(), api: .sandbox)
|
||||
let credentials = Credentials(type: .oauthAccessToken, username: "<#USERNAME#>", secret: "<#SECRET#>")
|
||||
caller.credentials = credentials
|
||||
|
||||
let syncAll = FeedlySyncAllOperation(account: account, credentials: credentials, caller: caller, database: databaseContainer.database, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, log: support.log)
|
||||
|
||||
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncAll.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
lastSuccessfulFetchStartDate = Date()
|
||||
|
||||
OperationQueue.main.addOperation(syncAll)
|
||||
|
||||
waitForExpectations(timeout: 60)
|
||||
}
|
||||
|
||||
// Prefix with "test" to manually run this particular function, e.g.: func test_getTestData()
|
||||
func getTestData() {
|
||||
// Add a breakpoint on the `print` statements and start a proxy server on your Mac.
|
||||
// 1. In Feedly sandbox, perform the actions implied by the string in the print statement.
|
||||
// 2. In the proxy server app, such as Charles, clear requests and responses and filter by "sandbox".
|
||||
// 3. In Xcode, hit continue in the Debugger so the test requests the data.
|
||||
// 4. Save the responses captured by the proxy.
|
||||
print("Prepare for initial sync.")
|
||||
downloadTestData()
|
||||
|
||||
assert(lastSuccessfulFetchStartDate != nil)
|
||||
|
||||
print("Read/unread, star and unstar some articles.")
|
||||
downloadTestData()
|
||||
|
||||
print("Read/unread, star and unstar some articles again.")
|
||||
downloadTestData()
|
||||
|
||||
print("Add Feeds and Folders.")
|
||||
downloadTestData()
|
||||
|
||||
print("Rename and Remove Feeds and Folders.")
|
||||
downloadTestData()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
//
|
||||
// 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 starredArticleIds = account.fetchStarredArticleIDs()
|
||||
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 = account.fetchArticles(.articleIDs(expectedArticleIds))
|
||||
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
|
||||
|
||||
let starredArticles = 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)
|
||||
}
|
||||
|
||||
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 starredArticleIds = account.fetchStarredArticleIDs()
|
||||
XCTAssertTrue(starredArticleIds.isEmpty)
|
||||
}
|
||||
|
||||
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 starredArticleIds = account.fetchStarredArticleIDs()
|
||||
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 = account.fetchArticles(.articleIDs(expectedArticleIds))
|
||||
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
|
||||
|
||||
let starredArticles = 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
//
|
||||
// FeedlySyncStreamContentsOperationTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 26/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlySyncStreamContentsOperationTests: 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 newerThan: Date? = Date(timeIntervalSinceReferenceDate: 0)
|
||||
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)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
}
|
||||
|
||||
let syncStreamContents = FeedlySyncStreamContentsOperation(account: account, resource: resource, service: service, newerThan: newerThan, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let expectedArticleIds = Set(items.map { $0.id })
|
||||
let expectedArticles = account.fetchArticles(.articleIDs(expectedArticleIds))
|
||||
XCTAssertEqual(expectedArticles.count, expectedArticleIds.count, "Did not fetch all the articles.")
|
||||
}
|
||||
|
||||
func testIngestsOnePageFailure() {
|
||||
let service = TestGetStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 0)
|
||||
|
||||
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)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertNil(continuation)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
}
|
||||
|
||||
let syncStreamContents = FeedlySyncStreamContentsOperation(account: account, resource: resource, service: service, newerThan: newerThan, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
}
|
||||
|
||||
func testIngestsManyPagesSuccess() {
|
||||
let service = TestGetPagedStreamContentsService()
|
||||
let resource = FeedlyCategoryResourceId(id: "user/1234/category/5678")
|
||||
let newerThan: Date? = Date(timeIntervalSinceReferenceDate: 0)
|
||||
|
||||
let continuations = (1...10).map { "\($0)" }
|
||||
service.addAtLeastOnePage(for: resource, continuations: continuations, numberOfEntriesPerPage: 1000)
|
||||
|
||||
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)
|
||||
XCTAssertEqual(serviceNewerThan, newerThan)
|
||||
XCTAssertNil(serviceUnreadOnly)
|
||||
|
||||
if let continuation = continuation {
|
||||
XCTAssertTrue(remainingContinuations.contains(continuation))
|
||||
remainingContinuations.remove(continuation)
|
||||
}
|
||||
|
||||
getStreamPageExpectation.fulfill()
|
||||
}
|
||||
|
||||
let syncStreamContents = FeedlySyncStreamContentsOperation(account: account, resource: resource, service: service, newerThan: newerThan, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
syncStreamContents.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(syncStreamContents)
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
|
||||
// Find articles inserted.
|
||||
let articleIds = Set(service.pages.values.map { $0.items }.flatMap { $0 }.map { $0.id })
|
||||
let articles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertEqual(articleIds.count, articles.count)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// 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 unreadArticleIds = account.fetchUnreadArticleIDs()
|
||||
let missingIds = expectedArticleIds.subtracting(unreadArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.")
|
||||
}
|
||||
|
||||
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 unreadArticleIds = account.fetchUnreadArticleIDs()
|
||||
XCTAssertTrue(unreadArticleIds.isEmpty)
|
||||
}
|
||||
|
||||
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 unreadArticleIds = account.fetchUnreadArticleIDs()
|
||||
let missingIds = expectedArticleIds.subtracting(unreadArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty, "These article ids were not marked as unread.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
//
|
||||
// FeedlyTestSupport.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 22/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import RSParser
|
||||
@testable import Account
|
||||
import os.log
|
||||
import SyncDatabase
|
||||
|
||||
struct FeedlyTestSupport {
|
||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "FeedlyTests")
|
||||
var credentials = Credentials(type: .oauthAccessToken, username: "Test", secret: "t3st")
|
||||
var transport = TestTransport()
|
||||
|
||||
func makeMockNetworkStack() -> (TestTransport, FeedlyAPICaller) {
|
||||
let caller = FeedlyAPICaller(transport: transport, api: .sandbox)
|
||||
caller.credentials = credentials
|
||||
return (transport, caller)
|
||||
}
|
||||
|
||||
func makeTestAccount() -> Account {
|
||||
let manager = TestAccountManager()
|
||||
let account = manager.createAccount(type: .feedly, transport: transport)
|
||||
do {
|
||||
try account.storeCredentials(credentials)
|
||||
} catch {
|
||||
XCTFail("Unable to register mock credentials because \(error)")
|
||||
}
|
||||
return account
|
||||
}
|
||||
|
||||
func makeTestDatabaseContainer() -> TestDatabaseContainer {
|
||||
return TestDatabaseContainer()
|
||||
}
|
||||
|
||||
class TestDatabaseContainer {
|
||||
private let path: String
|
||||
private(set) var database: SyncDatabase!
|
||||
|
||||
init() {
|
||||
let dataFolder = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
path = dataFolder.appendingPathComponent("\(UUID().uuidString)-Sync.sqlite3").path
|
||||
database = SyncDatabase(databaseFilePath: path)
|
||||
}
|
||||
|
||||
deinit {
|
||||
// We should close the database before removing the database.
|
||||
database = nil
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: path)
|
||||
print("Removed database at \(path)")
|
||||
} catch {
|
||||
print("Unable to remove database owned by \(self) because \(error).")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func destroy(_ testAccount: Account) {
|
||||
do {
|
||||
try testAccount.removeCredentials(type: .oauthAccessToken)
|
||||
} catch {
|
||||
XCTFail("Unable to clean up mock credentials because \(error)")
|
||||
}
|
||||
|
||||
let manager = TestAccountManager()
|
||||
manager.deleteAccount(testAccount)
|
||||
}
|
||||
|
||||
func testJSON(named: String, subdirectory: String? = nil) -> Any {
|
||||
let bundle = Bundle(for: TestTransport.self)
|
||||
let url = bundle.url(forResource: named, withExtension: "json", subdirectory: subdirectory)!
|
||||
let data = try! Data(contentsOf: url)
|
||||
let json = try! JSONSerialization.jsonObject(with: data)
|
||||
return json
|
||||
}
|
||||
|
||||
func checkFoldersAndFeeds(in account: Account, againstCollectionsAndFeedsInJSONNamed name: String, subdirectory: String? = nil) {
|
||||
let collections = testJSON(named: name, subdirectory: subdirectory) as! [[String:Any]]
|
||||
let collectionNames = Set(collections.map { $0["label"] as! String })
|
||||
let collectionIds = Set(collections.map { $0["id"] as! String })
|
||||
|
||||
let folders = account.folders ?? Set()
|
||||
let folderNames = Set(folders.compactMap { $0.name })
|
||||
let folderIds = Set(folders.compactMap { $0.externalID })
|
||||
|
||||
let missingNames = collectionNames.subtracting(folderNames)
|
||||
let missingIds = collectionIds.subtracting(folderIds)
|
||||
|
||||
XCTAssertEqual(folders.count, collections.count, "Mismatch between collections and folders.")
|
||||
XCTAssertTrue(missingNames.isEmpty, "Collections with these names did not have a corresponding folder with the same name.")
|
||||
XCTAssertTrue(missingIds.isEmpty, "Collections with these ids did not have a corresponding folder with the same id.")
|
||||
|
||||
for collection in collections {
|
||||
checkSingleFolderAndFeeds(in: account, againstOneCollectionAndFeedsInJSONPayload: collection)
|
||||
}
|
||||
}
|
||||
|
||||
func checkSingleFolderAndFeeds(in account: Account, againstOneCollectionAndFeedsInJSONNamed name: String) {
|
||||
let collection = testJSON(named: name) as! [String:Any]
|
||||
checkSingleFolderAndFeeds(in: account, againstOneCollectionAndFeedsInJSONPayload: collection)
|
||||
}
|
||||
|
||||
func checkSingleFolderAndFeeds(in account: Account, againstOneCollectionAndFeedsInJSONPayload collection: [String: Any]) {
|
||||
let label = collection["label"] as! String
|
||||
guard let folder = account.existingFolder(with: label) else {
|
||||
// due to a previous test failure?
|
||||
XCTFail("Could not find the \"\(label)\" folder.")
|
||||
return
|
||||
}
|
||||
let collectionFeeds = collection["feeds"] as! [[String: Any]]
|
||||
let folderFeeds = folder.topLevelFeeds
|
||||
|
||||
XCTAssertEqual(collectionFeeds.count, folderFeeds.count)
|
||||
|
||||
let collectionFeedIds = Set(collectionFeeds.map { $0["id"] as! String })
|
||||
let folderFeedIds = Set(folderFeeds.map { $0.feedID })
|
||||
let missingFeedIds = collectionFeedIds.subtracting(folderFeedIds)
|
||||
|
||||
XCTAssertTrue(missingFeedIds.isEmpty, "Feeds with these ids were not found in the \"\(label)\" folder.")
|
||||
}
|
||||
|
||||
func checkArticles(in account: Account, againstItemsInStreamInJSONNamed name: String, subdirectory: String? = nil) {
|
||||
let stream = testJSON(named: name, subdirectory: subdirectory) as! [String:Any]
|
||||
checkArticles(in: account, againstItemsInStreamInJSONPayload: stream)
|
||||
}
|
||||
|
||||
func checkArticles(in account: Account, againstItemsInStreamInJSONPayload stream: [String: Any]) {
|
||||
checkArticles(in: account, correspondToStreamItemsIn: stream)
|
||||
}
|
||||
|
||||
private struct ArticleItem {
|
||||
var id: String
|
||||
var feedId: String
|
||||
var content: String
|
||||
var JSON: [String: Any]
|
||||
var unread: Bool
|
||||
|
||||
/// Convoluted external URL logic "documented" here:
|
||||
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
|
||||
var externalUrl: String? {
|
||||
return ((JSON["canonical"] as? [[String: Any]]) ?? (JSON["alternate"] as? [[String: Any]]))?.compactMap { link -> String? in
|
||||
let href = link["href"] as? String
|
||||
if let type = link["type"] as? String {
|
||||
if type == "text/html" {
|
||||
return href
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return href
|
||||
}.first
|
||||
}
|
||||
|
||||
init(item: [String: Any]) {
|
||||
self.JSON = item
|
||||
self.id = item["id"] as! String
|
||||
|
||||
let origin = item["origin"] as! [String: Any]
|
||||
self.feedId = origin["streamId"] as! String
|
||||
|
||||
let content = item["content"] as? [String: Any]
|
||||
let summary = item["summary"] as? [String: Any]
|
||||
self.content = ((content ?? summary)?["content"] as? String) ?? ""
|
||||
|
||||
self.unread = item["unread"] as! Bool
|
||||
}
|
||||
}
|
||||
|
||||
/// Awkwardly titled to make it clear the JSON given is from a stream response.
|
||||
func checkArticles(in testAccount: Account, correspondToStreamItemsIn stream: [String: Any]) {
|
||||
|
||||
let items = stream["items"] as! [[String: Any]]
|
||||
let articleItems = items.map { ArticleItem(item: $0) }
|
||||
let itemIds = Set(articleItems.map { $0.id })
|
||||
|
||||
let articles = testAccount.fetchArticles(.articleIDs(itemIds))
|
||||
let articleIds = Set(articles.map { $0.articleID })
|
||||
|
||||
let missing = itemIds.subtracting(articleIds)
|
||||
|
||||
XCTAssertEqual(items.count, articles.count)
|
||||
XCTAssertTrue(missing.isEmpty, "Items with these ids did not have a corresponding article with the same id.")
|
||||
|
||||
for article in articles {
|
||||
for item in articleItems where item.id == article.articleID {
|
||||
XCTAssertEqual(article.uniqueID, item.id)
|
||||
XCTAssertEqual(article.contentHTML, item.content)
|
||||
XCTAssertEqual(article.feedID, item.feedId)
|
||||
XCTAssertEqual(article.externalURL, item.externalUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkUnreadStatuses(in account: Account, againstIdsInStreamInJSONNamed name: String, subdirectory: String? = nil) {
|
||||
let streamIds = testJSON(named: name, subdirectory: subdirectory) as! [String:Any]
|
||||
checkUnreadStatuses(in: account, correspondToIdsInJSONPayload: streamIds)
|
||||
}
|
||||
|
||||
func checkUnreadStatuses(in testAccount: Account, correspondToIdsInJSONPayload streamIds: [String: Any]) {
|
||||
let ids = Set(streamIds["ids"] as! [String])
|
||||
let articleIds = testAccount.fetchUnreadArticleIDs()
|
||||
// Unread statuses can be paged from Feedly.
|
||||
// Instead of joining test data, the best we can do is
|
||||
// make sure that these ids are marked as unread (a subset of the total).
|
||||
XCTAssertTrue(ids.isSubset(of: articleIds), "Some articles in `ids` are not marked as unread.")
|
||||
}
|
||||
|
||||
func checkStarredStatuses(in account: Account, againstItemsInStreamInJSONNamed name: String, subdirectory: String? = nil) {
|
||||
let streamIds = testJSON(named: name, subdirectory: subdirectory) as! [String:Any]
|
||||
checkStarredStatuses(in: account, correspondToStreamItemsIn: streamIds)
|
||||
}
|
||||
|
||||
func checkStarredStatuses(in testAccount: Account, correspondToStreamItemsIn stream: [String: Any]) {
|
||||
let items = stream["items"] as! [[String: Any]]
|
||||
let ids = Set(items.map { $0["id"] as! String })
|
||||
let articleIds = testAccount.fetchStarredArticleIDs()
|
||||
// Starred articles can be paged from Feedly.
|
||||
// Instead of joining test data, the best we can do is
|
||||
// make sure that these articles are marked as starred (a subset of the total).
|
||||
XCTAssertTrue(ids.isSubset(of: articleIds), "Some articles in `ids` are not marked as starred.")
|
||||
}
|
||||
|
||||
func check(_ entries: [FeedlyEntry], correspondToStreamItemsIn stream: [String: Any]) {
|
||||
|
||||
let items = stream["items"] as! [[String: Any]]
|
||||
let itemIds = Set(items.map { $0["id"] as! String })
|
||||
|
||||
let articleIds = Set(entries.map { $0.id })
|
||||
|
||||
let missing = itemIds.subtracting(articleIds)
|
||||
|
||||
XCTAssertEqual(items.count, entries.count)
|
||||
XCTAssertTrue(missing.isEmpty, "Failed to create \(FeedlyEntry.self) values from objects in the JSON with these ids.")
|
||||
}
|
||||
|
||||
func makeParsedItemTestDataFor(numberOfFeeds: Int, numberOfItemsInFeeds: Int) -> [String: Set<ParsedItem>] {
|
||||
let ids = (0..<numberOfFeeds).map { "feed/\($0)" }
|
||||
let feedIdsAndItemCounts = ids.map { ($0, numberOfItemsInFeeds) }
|
||||
|
||||
let entries = feedIdsAndItemCounts.map { (feedId, count) -> (String, [Int]) in
|
||||
return (feedId, (0..<count).map { $0 })
|
||||
|
||||
}.map { pair -> (String, Set<ParsedItem>) in
|
||||
let items = pair.1.map { index -> ParsedItem in
|
||||
ParsedItem(syncServiceID: "\(pair.0)/articles/\(index)",
|
||||
uniqueID: UUID().uuidString,
|
||||
feedURL: pair.0,
|
||||
url: "http://localhost/",
|
||||
externalURL: "http://localhost/\(pair.0)/articles/\(index).html",
|
||||
title: "Title\(index)",
|
||||
contentHTML: "Content \(index) HTML.",
|
||||
contentText: "Content \(index) Text",
|
||||
summary: nil,
|
||||
imageURL: nil,
|
||||
bannerImageURL: nil,
|
||||
datePublished: nil,
|
||||
dateModified: nil,
|
||||
authors: nil,
|
||||
tags: nil,
|
||||
attachments: nil)
|
||||
}
|
||||
return (pair.0, Set(items))
|
||||
}.reduce([String: Set<ParsedItem>](minimumCapacity: feedIdsAndItemCounts.count)) { (dict, pair) in
|
||||
var mutant = dict
|
||||
mutant[pair.0] = pair.1
|
||||
return mutant
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// FeedlyUpdateAccountFeedsWithItemsOperationTests.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 FeedlyUpdateAccountFeedsWithItemsOperationTests: 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()
|
||||
}
|
||||
|
||||
struct TestItemsByFeedProvider: FeedlyParsedItemsByFeedProviding {
|
||||
var providerName: String
|
||||
var parsedItemsKeyedByFeedId: [String: Set<ParsedItem>]
|
||||
}
|
||||
|
||||
func testUpdateAccountWithEmptyItems() {
|
||||
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 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 entries = testItems.flatMap { $0.value }
|
||||
let articleIds = Set(entries.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let accountArticles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertTrue(accountArticles.isEmpty)
|
||||
}
|
||||
|
||||
func testUpdateAccountWithOneItem() {
|
||||
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 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 entries = testItems.flatMap { $0.value }
|
||||
let articleIds = Set(entries.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let accountArticles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertTrue(accountArticles.count == entries.count)
|
||||
|
||||
let accountArticleIds = Set(accountArticles.map { $0.articleID })
|
||||
let missingIds = articleIds.subtracting(accountArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty)
|
||||
}
|
||||
|
||||
func testUpdateAccountWithManyItems() {
|
||||
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 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: 10) // 10,000 articles takes ~ three seconds for me.
|
||||
|
||||
let entries = testItems.flatMap { $0.value }
|
||||
let articleIds = Set(entries.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let accountArticles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertTrue(accountArticles.count == entries.count)
|
||||
|
||||
let accountArticleIds = Set(accountArticles.map { $0.articleID })
|
||||
let missingIds = articleIds.subtracting(accountArticleIds)
|
||||
XCTAssertTrue(missingIds.isEmpty)
|
||||
}
|
||||
|
||||
func testCancelUpdateAccount() {
|
||||
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 update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
|
||||
|
||||
let completionExpectation = expectation(description: "Did Finish")
|
||||
update.completionBlock = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(update)
|
||||
|
||||
update.cancel()
|
||||
|
||||
waitForExpectations(timeout: 2)
|
||||
|
||||
let entries = testItems.flatMap { $0.value }
|
||||
let articleIds = Set(entries.compactMap { $0.syncServiceID })
|
||||
XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
|
||||
|
||||
let accountArticles = account.fetchArticles(.articleIDs(articleIds))
|
||||
XCTAssertTrue(accountArticles.isEmpty)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","items":[]}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// TestGetCollectionsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 30/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetCollectionsService: FeedlyGetCollectionsService {
|
||||
var mockResult: Result<[FeedlyCollection], Error>?
|
||||
var getCollectionsExpectation: XCTestExpectation?
|
||||
|
||||
func getCollections(completionHandler: @escaping (Result<[FeedlyCollection], 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 {
|
||||
completionHandler(result)
|
||||
self.getCollectionsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// TestGetPagedStreamContentsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 28/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetPagedStreamContentsService: FeedlyGetStreamContentsService {
|
||||
|
||||
var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
|
||||
var getStreamContentsExpectation: XCTestExpectation?
|
||||
var pages = [String: FeedlyStream]()
|
||||
|
||||
func addAtLeastOnePage(for resource: FeedlyResourceId, continuations: [String], numberOfEntriesPerPage count: Int) {
|
||||
pages = [String: FeedlyStream](minimumCapacity: continuations.count + 1)
|
||||
|
||||
// A continuation is an identifier for the next page.
|
||||
// The first page has a nil identifier.
|
||||
// The last page has no next page, so the next continuation value for that page is nil.
|
||||
// Therefore, each page needs to know the identifier of the next page.
|
||||
for index in -1..<continuations.count {
|
||||
let nextIndex = index + 1
|
||||
let continuation: String? = nextIndex < continuations.count ? continuations[nextIndex] : nil
|
||||
let page = makeStreamContents(for: resource, continuation: continuation, between: 0..<count)
|
||||
let key = TestGetPagedStreamContentsService.getPagingKey(for: resource, continuation: index < 0 ? nil : continuations[index])
|
||||
pages[key] = page
|
||||
}
|
||||
}
|
||||
|
||||
private func makeStreamContents(for resource: FeedlyResourceId, continuation: String?, between range: Range<Int>) -> FeedlyStream {
|
||||
let entries = range.map { index -> FeedlyEntry in
|
||||
let content = FeedlyEntry.Content(content: "Content \(index)",
|
||||
direction: .leftToRight)
|
||||
|
||||
let origin = FeedlyOrigin(title: "Origin \(index)",
|
||||
streamId: resource.id,
|
||||
htmlUrl: "http://localhost/feedly/origin/\(index)")
|
||||
|
||||
return FeedlyEntry(id: "/articles/\(index)",
|
||||
title: "Article \(index)",
|
||||
content: content,
|
||||
summary: content,
|
||||
author: nil,
|
||||
published: Date(),
|
||||
updated: nil,
|
||||
origin: origin,
|
||||
canonical: nil,
|
||||
alternate: nil,
|
||||
unread: true,
|
||||
tags: nil,
|
||||
categories: nil,
|
||||
enclosure: nil)
|
||||
}
|
||||
|
||||
let stream = FeedlyStream(id: resource.id, updated: nil, continuation: continuation, items: entries)
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
static func getPagingKey(for stream: FeedlyResourceId, continuation: String?) -> String {
|
||||
return "\(stream.id)@\(continuation ?? "")"
|
||||
}
|
||||
|
||||
func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
let key = TestGetPagedStreamContentsService.getPagingKey(for: resource, continuation: continuation)
|
||||
guard let page = pages[key] else {
|
||||
XCTFail("Missing page for \(resource.id) and continuation \(String(describing: continuation)). Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(resource, continuation, newerThan, unreadOnly)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(.success(page))
|
||||
self.getStreamContentsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// TestGetPagedStreamIdsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 29/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetPagedStreamIdsService: FeedlyGetStreamIdsService {
|
||||
|
||||
var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
|
||||
var getStreamIdsExpectation: XCTestExpectation?
|
||||
var pages = [String: FeedlyStreamIds]()
|
||||
|
||||
func addAtLeastOnePage(for resource: FeedlyResourceId, continuations: [String], numberOfEntriesPerPage count: Int) {
|
||||
pages = [String: FeedlyStreamIds](minimumCapacity: continuations.count + 1)
|
||||
|
||||
// A continuation is an identifier for the next page.
|
||||
// The first page has a nil identifier.
|
||||
// The last page has no next page, so the next continuation value for that page is nil.
|
||||
// Therefore, each page needs to know the identifier of the next page.
|
||||
for index in -1..<continuations.count {
|
||||
let nextIndex = index + 1
|
||||
let continuation: String? = nextIndex < continuations.count ? continuations[nextIndex] : nil
|
||||
let page = makeStreamIds(for: resource, continuation: continuation, between: 0..<count)
|
||||
let key = TestGetPagedStreamIdsService.getPagingKey(for: resource, continuation: index < 0 ? nil : continuations[index])
|
||||
pages[key] = page
|
||||
}
|
||||
}
|
||||
|
||||
private func makeStreamIds(for resource: FeedlyResourceId, continuation: String?, between range: Range<Int>) -> FeedlyStreamIds {
|
||||
let entryIds = range.map { _ in UUID().uuidString }
|
||||
let stream = FeedlyStreamIds(continuation: continuation, ids: entryIds)
|
||||
return stream
|
||||
}
|
||||
|
||||
static func getPagingKey(for stream: FeedlyResourceId, continuation: String?) -> String {
|
||||
return "\(stream.id)@\(continuation ?? "")"
|
||||
}
|
||||
|
||||
func getStreamIds(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStreamIds, Error>) -> ()) {
|
||||
let key = TestGetPagedStreamIdsService.getPagingKey(for: resource, continuation: continuation)
|
||||
guard let page = pages[key] else {
|
||||
XCTFail("Missing page for \(resource.id) and continuation \(String(describing: continuation)). Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(resource, continuation, newerThan, unreadOnly)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(.success(page))
|
||||
self.getStreamIdsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// TestGetStreamContentsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 28/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetStreamContentsService: FeedlyGetStreamContentsService {
|
||||
|
||||
var mockResult: Result<FeedlyStream, Error>?
|
||||
var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
|
||||
var getStreamContentsExpectation: XCTestExpectation?
|
||||
|
||||
func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
guard let result = mockResult else {
|
||||
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(resource, continuation, newerThan, unreadOnly)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(result)
|
||||
self.getStreamContentsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func makeMockFeedlyEntryItem() -> [FeedlyEntry] {
|
||||
let origin = FeedlyOrigin(title: "XCTest@localhost", streamId: "user/12345/category/67890", htmlUrl: "http://localhost/nnw/xctest")
|
||||
let content = FeedlyEntry.Content(content: "In the beginning...", direction: .leftToRight)
|
||||
let items = [FeedlyEntry(id: "feeds/0/article/0",
|
||||
title: "RSS Reader Ingests Man",
|
||||
content: content,
|
||||
summary: content,
|
||||
author: nil,
|
||||
published: Date(),
|
||||
updated: nil,
|
||||
origin: origin,
|
||||
canonical: nil,
|
||||
alternate: nil,
|
||||
unread: true,
|
||||
tags: nil,
|
||||
categories: nil,
|
||||
enclosure: nil)]
|
||||
return items
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// TestGetStreamIdsService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 29/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
final class TestGetStreamIdsService: FeedlyGetStreamIdsService {
|
||||
|
||||
var mockResult: Result<FeedlyStreamIds, Error>?
|
||||
var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
|
||||
var getStreamIdsExpectation: XCTestExpectation?
|
||||
|
||||
func getStreamIds(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStreamIds, Error>) -> ()) {
|
||||
guard let result = mockResult else {
|
||||
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
|
||||
return
|
||||
}
|
||||
parameterTester?(resource, continuation, newerThan, unreadOnly)
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(result)
|
||||
self.getStreamIdsExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// TestMarkArticlesService.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 30/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class TestMarkArticlesService: FeedlyMarkArticlesService {
|
||||
|
||||
var didMarkExpectation: XCTestExpectation?
|
||||
var parameterTester: ((Set<String>, FeedlyMarkAction) -> ())?
|
||||
var mockResult: Result<Void, Error> = .success(())
|
||||
|
||||
func mark(_ articleIds: Set<String>, as action: FeedlyMarkAction, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.parameterTester?(articleIds, action)
|
||||
completionHandler(self.mockResult)
|
||||
self.didMarkExpectation?.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,296 @@
|
|||
{
|
||||
"ids": [
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d947baaaa:15c6:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d9444b6ae:155b:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d942c0878:2de:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93f9d9be:2ab:53b826a2",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d93d6ce8f:147d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d93d6ce8f:147c:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93c1a015:251:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93b34eba:241:53b826a2",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d93b31822:142f:d4506071",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d93b31822:142e:d4506071",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d93b31822:142d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d939fda8c:13f2:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93950469:21b:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93950469:21a:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d938aedbe:216:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93852581:211:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9380dda0:209:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9376eefc:202:53b826a2",
|
||||
"kv2DIas8GblflohzMAcClzUErTYUYammDtqm4auH/og=_16d936e2abe:1359:d4506071",
|
||||
"kv2DIas8GblflohzMAcClzUErTYUYammDtqm4auH/og=_16d936e2abe:1358:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d936d06f3:1ef:53b826a2",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d9368e67a:133d:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d93445712:12e2:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d933ea3c0:c4:53b826a2",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d9331f22f:1299:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d9331f22f:1298:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d931c60da:8b:53b826a2",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d93167a9c:7c:53b826a2",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d930e3ae3:122b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92fafd47:119c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92fafd47:119b:d4506071",
|
||||
"ja4A8niNlyAfffczL726vcmEyvPHXJ4+zSYKI4xWdUg=_16d92efb228:118b:d4506071",
|
||||
"ja4A8niNlyAfffczL726vcmEyvPHXJ4+zSYKI4xWdUg=_16d92efb228:118a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92e40b08:11:53b826a2",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d92d74711:113f:d4506071",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d92d74711:113e:d4506071",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d92d74711:113d:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d92d6203d:113b:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92cb9b83:d47:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92cb9b83:d46:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92c4095a:10fb:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92c4095a:10fa:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92c19339:d3b:fc4690a0",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d92c10fc0:10f0:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92ad8234:d27:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92a356f6:d1f:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92936251:d0c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92936251:d0b:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d928d1550:103e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d928d1550:103d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d927afac9:ce5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d927afac9:ce4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d926b25dc:cd9:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d9267e977:fc9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d925d00c6:ccf:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d925d00c6:cce:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d925620e1:f90:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d924895c3:cb8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:cac:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:cab:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:caa:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:ca9:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d922ebbce:c9e:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d922ebbce:c9d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d921f2cdb:eef:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d921f2cdb:eee:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d920c42e7:c86:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91f7f422:c70:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d91e838fb:e54:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91d9837f:c5c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91d3a580:c56:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c49:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c48:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c47:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d91c0ea34:dcf:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d91b143b9:d96:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d919d0e1b:c27:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9198dae9:c21:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3f:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3d:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3c:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3b:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3a:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b39:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b38:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b37:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b36:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b35:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c15:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c14:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c13:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afe:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afd:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afc:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9174a7bc:c02:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d90f19878:b55:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8f76c4f0:884:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8edefa36:703:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec5b3f5:6f1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec0c455:6dc:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8eafe951:655:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e7a5c11:5b8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e47ea63:4f3:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:555:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:554:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a2:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e29a65f:44f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8e19672d:4b9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e157c76:3ca:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8df74270:3a3:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f3:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f2:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f1:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"ja4A8niNlyAfffczL726vcmEyvPHXJ4+zSYKI4xWdUg=_16d8dab6a11:2d6:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"Ai/HDbZBn/DqS4YSFb8RbnuS8su16El+mi83Mpt/WqQ=_16d88a4dca2:50e96:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"Ai/HDbZBn/DqS4YSFb8RbnuS8su16El+mi83Mpt/WqQ=_16d875a7313:503b8:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.all",
|
||||
"updated": 1572574658046,
|
||||
"items": []
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,258 @@
|
|||
{
|
||||
"ids": [
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92cb9b83:d47:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92cb9b83:d46:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92c4095a:10fb:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d92c4095a:10fa:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92c19339:d3b:fc4690a0",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d92c10fc0:10f0:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92ad8234:d27:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92a356f6:d1f:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92936251:d0c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d92936251:d0b:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d928d1550:103e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d928d1550:103d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d927afac9:ce5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d927afac9:ce4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d926b25dc:cd9:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d9267e977:fc9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d925d00c6:ccf:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d925d00c6:cce:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d925620e1:f90:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d924895c3:cb8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:cac:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:cab:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:caa:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d923e9078:ca9:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d922ebbce:c9e:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d922ebbce:c9d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d921f2cdb:eef:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d921f2cdb:eee:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d920c42e7:c86:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91f7f422:c70:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d91e838fb:e54:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91d9837f:c5c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91d3a580:c56:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c49:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c48:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d91c5604c:c47:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d91c0ea34:dcf:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d91b143b9:d96:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d919d0e1b:c27:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9198dae9:c21:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3f:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3d:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3c:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3b:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b3a:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b39:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b38:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b37:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b36:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d9189e253:b35:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c15:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c14:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9188f606:c13:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afe:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afd:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d917a4e34:afc:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9174a7bc:c02:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d90f19878:b55:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8f76c4f0:884:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8edefa36:703:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec5b3f5:6f1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec0c455:6dc:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8eafe951:655:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e7a5c11:5b8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e47ea63:4f3:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:555:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:554:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a2:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e29a65f:44f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8e19672d:4b9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e157c76:3ca:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8df74270:3a3:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f3:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f2:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f1:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"ja4A8niNlyAfffczL726vcmEyvPHXJ4+zSYKI4xWdUg=_16d8dab6a11:2d6:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"Ai/HDbZBn/DqS4YSFb8RbnuS8su16El+mi83Mpt/WqQ=_16d88a4dca2:50e96:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"Ai/HDbZBn/DqS4YSFb8RbnuS8su16El+mi83Mpt/WqQ=_16d875a7313:503b8:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.all",
|
||||
"updated": 1572574658046,
|
||||
"items": []
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,178 @@
|
|||
{
|
||||
"ids": [
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f1:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,198 @@
|
|||
{
|
||||
"ids": [
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d9174a7bc:c02:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d90f19878:b55:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8f76c4f0:884:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8edefa36:703:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec5b3f5:6f1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ec0c455:6dc:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8eafe951:655:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e7a5c11:5b8:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e47ea63:4f3:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:555:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8e42013b:554:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e33b2d9:4a2:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e29a65f:44f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8e19672d:4b9:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8e157c76:3ca:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8df74270:3a3:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f3:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f2:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f1:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.all",
|
||||
"updated": 1572574658046,
|
||||
"items": []
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"ids": [
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3f0:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8de25f3c:3ef:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:359:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dd92b46:358:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35c:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35b:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8dd41847:35a:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dcf0acd:353:fc4690a0",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d8dcbdbb5:347:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8dc4fe3f:34a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8db0dc98:33a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:335:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8da6e95b:334:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:298:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:297:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d9d23f4:296:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d928ffb:326:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d888cfb:31b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d7654f4:30d:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30c:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30b:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:30a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d764432:309:fc4690a0",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d74755c:222:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21e:d4506071",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8d744f3f:21d:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e5:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d662f72:1e4:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d603c89:2f6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d4c2bf2:2e5:fc4690a0",
|
||||
"AY80t/Vl4TMkqqPjtnpcUzVjYAqDn3a9sFmvt0zCpmI=_16d8d3d9585:154:d4506071",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8d3d59be:153:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d3:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d380a62:2d2:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11e:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8d2f3a93:11d:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a6:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a5:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8d19c4b0:2a4:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfeb0cc:291:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cfe9cb4:28d:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:4a:d4506071",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cf8458f:49:d4506071",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cf5d0ea:284:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cdcb80b:270:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cd33bb7:26a:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cc927f0:260:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:760:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8cc153e9:75f:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cbe6239:258:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8cb47ebe:252:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ca03279:23f:fc4690a0",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d8c98318d:6ee:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c9638e1:23a:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b7:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c8a5f44:6b6:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c6de21e:221:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c59cf46:213:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:448:5e4732b4",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8c536ab3:447:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c45920d:206:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1ff:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c3b82d3:1fe:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8c10839d:1e1:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8bf29449:1ce:fc4690a0",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8ba1ae86:1b1:fc4690a0",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8a2de26c:4a:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8a041299:26:8e83c13e",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:2:5e4732b4",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d89eeb096:1:5e4732b4",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89d099d9:5:c963e369",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51566:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89bff80b:51565:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89942421:5148f:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d8988fd3e:51455:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89797893:1f1:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d896af822:1ac:db1c1742",
|
||||
"4rURtf64IHW6ygZs9tzPieSmabigMiZqHVuUPwvicWg=_16d895c218f:51370:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d89520343:5132b:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d894cd2ff:108:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89471ca5:105:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d893313be:f1:db1c1742",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d89254704:5121b:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d8924f769:5121a:18991ffa",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d8924f6cc:51219:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d8:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d891f3096:d7:db1c1742",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d891b096a:511c9:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d89066ab8:c5:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88f67b14:b8:db1c1742",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d88ed77ca:510a4:18991ffa",
|
||||
"wEPPtpyxm18xeuKJtd0RXOmRfdiYTcxSNO1zsjNlPdg=_16d88e0db64:5104a:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff8:18991ffa",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d88da4abd:50ff7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88d1c100:a3:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88ce0d5a:a0:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88c41716:9a:db1c1742",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d88b5f1f6:50f66:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4d:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d88b25c45:50f4c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88ad1618:50f0c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88a5d80e:7b:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d889bd061:72:db1c1742",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d888b305c:50dda:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da6:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d887b3ce7:50da5:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5c:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5b:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d88761c68:50d5a:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88737305:15:db1c1742",
|
||||
"ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16d886c439f:50ca1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d88695b6f:f:db1c1742",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d885899d3:2a4e:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d88540f01:50bf6:18991ffa",
|
||||
"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16d8846e4fe:50bc1:18991ffa",
|
||||
"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d884666a1:50bba:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d8844085c:50b99:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8841112b:2a39:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d883f2213:50b6e:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d881ce4bb:509fb:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29f0:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ef:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d880ef0b1:29ee:90d684ff",
|
||||
"E51hsZSss+6XSdMAYelFIdkn3CDBqFF2zAtZLRbxUrQ=_16d880e78f1:50990:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d8804b013:29e2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d7:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f42846:29d6:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d2:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87f08676:29d1:90d684ff",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080b:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:5080a:18991ffa",
|
||||
"A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16d87e5a8de:50809:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cd:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d87d5a0da:507cc:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a2:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d87d11796:507a1:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87be329a:29aa:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87b7bc0b:29a4:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87aa0b8d:299b:90d684ff",
|
||||
"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d87a8b438:50635:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d879fec1f:2992:90d684ff",
|
||||
"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d879fb6f0:50612:18991ffa",
|
||||
"mstho8+q3WlqKF/sQV4c1Sm0g7GZMkqDkTxYnXthJHo=_16d879f90ef:5060a:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ee:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d879e7b4c:505ed:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c8:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d879a0784:505c7:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2982:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d87856d15:2981:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d876d90e1:2970:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2956:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d874f7ba1:2955:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293c:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293b:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d873148ae:293a:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d872c01bd:4ff2d:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d870280bb:2920:90d684ff",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd4:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd3:18991ffa",
|
||||
"1JeSDrJ7WnuWJBtfP8UBdStvgnDdH8wtt082mlVgo4k=_16d86f8e582:4fdd2:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86f894e7:2914:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d86be7055:28f9:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d86bdfb5a:4fc5c:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e5:90d684ff",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d869ef75d:28e4:90d684ff",
|
||||
"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d86867c9b:4fb46:18991ffa",
|
||||
"AxO6mug+YPRclcA3EJcsykvvS1qcjXH62IXONGWCBII=_16d867b7bb5:28d3:90d684ff",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0e:18991ffa",
|
||||
"BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16d865004b0:4fa0d:18991ffa"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ids": []
|
||||
}
|
|
@ -16,7 +16,7 @@ class TestAccountManager {
|
|||
static let shared = TestAccountManager()
|
||||
|
||||
var accountsFolder: URL {
|
||||
return FileManager.default.temporaryDirectory
|
||||
return try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ class TestAccountManager {
|
|||
try FileManager.default.removeItem(atPath: account.dataFolder)
|
||||
}
|
||||
catch let error as CocoaError where error.code == .fileNoSuchFile {
|
||||
print("Unable to delete folder at: \(account.dataFolder) because \(error)")
|
||||
|
||||
}
|
||||
catch {
|
||||
assertionFailure("Could not delete folder at: \(account.dataFolder) because \(error)")
|
||||
|
|
|
@ -10,6 +10,10 @@ import Foundation
|
|||
import RSWeb
|
||||
import XCTest
|
||||
|
||||
protocol TestTransportMockResponseProviding: class {
|
||||
func mockResponseFileUrl(for components: URLComponents) -> URL?
|
||||
}
|
||||
|
||||
final class TestTransport: Transport {
|
||||
|
||||
enum TestTransportError: String, Error {
|
||||
|
@ -19,12 +23,7 @@ final class TestTransport: Transport {
|
|||
var testFiles = [String: String]()
|
||||
var testStatusCodes = [String: Int]()
|
||||
|
||||
/// Allows tests to filter time sensitive state out to make matching against test data easier.
|
||||
var blacklistedQueryItemNames = Set([
|
||||
"newerThan", // Feedly: Mock data has a fixed date.
|
||||
"unreadOnly", // Feedly: Mock data is read/unread by test expectation.
|
||||
"count", // Feedly: Mock data is limited by test expectation.
|
||||
])
|
||||
weak var mockResponseFileUrlProvider: TestTransportMockResponseProviding?
|
||||
|
||||
private func httpResponse(for request: URLRequest, statusCode: Int = 200) -> HTTPURLResponse {
|
||||
guard let url = request.url else {
|
||||
|
@ -35,41 +34,45 @@ final class TestTransport: Transport {
|
|||
|
||||
func send(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) {
|
||||
|
||||
guard let url = request.url, var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
completion(.failure(TestTransportError.invalidState))
|
||||
return
|
||||
}
|
||||
|
||||
components.queryItems = components
|
||||
.queryItems?
|
||||
.filter { !blacklistedQueryItemNames.contains($0.name) }
|
||||
|
||||
guard let urlString = components.url?.absoluteString else {
|
||||
guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
completion(.failure(TestTransportError.invalidState))
|
||||
return
|
||||
}
|
||||
|
||||
let urlString = url.absoluteString
|
||||
let response = httpResponse(for: request, statusCode: testStatusCodes[urlString] ?? 200)
|
||||
let testFileURL: URL
|
||||
|
||||
var mockResponseFound = false
|
||||
for (key, testFileName) in testFiles where urlString.contains(key) {
|
||||
let testFileURL = Bundle(for: TestTransport.self).resourceURL!.appendingPathComponent(testFileName)
|
||||
let data = try! Data(contentsOf: testFileURL)
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
completion(.success((response, data)))
|
||||
if let provider = mockResponseFileUrlProvider {
|
||||
guard let providerUrl = provider.mockResponseFileUrl(for: components) else {
|
||||
XCTFail("Test behaviour undefined. Mock provider failed to provide non-nil URL for \(components).")
|
||||
return
|
||||
}
|
||||
mockResponseFound = true
|
||||
break
|
||||
}
|
||||
|
||||
if !mockResponseFound {
|
||||
// XCTFail("Missing mock response for: \(urlString)")
|
||||
testFileURL = providerUrl
|
||||
|
||||
} else if let testKeyAndFileName = testFiles.first(where: { urlString.contains($0.key) }) {
|
||||
testFileURL = Bundle(for: TestTransport.self).resourceURL!.appendingPathComponent(testKeyAndFileName.value)
|
||||
|
||||
} else {
|
||||
// XCTFail("Missing mock response for: \(urlString)")
|
||||
print("***\nWARNING: \(self) missing mock response for:\n\(urlString)\n***")
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
completion(.success((response, nil)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: testFileURL)
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
completion(.success((response, data)))
|
||||
}
|
||||
} catch {
|
||||
XCTFail("Unable to read file at \(testFileURL) because \(error).")
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send(request: URLRequest, method: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
|
|
@ -53,171 +53,6 @@ final class FeedlyAPICaller {
|
|||
return baseUrlComponents.host
|
||||
}
|
||||
|
||||
func getCollections(completionHandler: @escaping (Result<[FeedlyCollection], Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/collections"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
// URLSession.shared.dataTask(with: request) { (data, response, error) in
|
||||
// print(String(data: data!, encoding: .utf8))
|
||||
// }.resume()
|
||||
//
|
||||
transport.send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStream(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/streams/contents"
|
||||
|
||||
var queryItems = [URLQueryItem]()
|
||||
|
||||
if let date = newerThan {
|
||||
let value = String(Int(date.timeIntervalSince1970 * 1000))
|
||||
let queryItem = URLQueryItem(name: "newerThan", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let flag = unreadOnly {
|
||||
let value = flag ? "true" : "false"
|
||||
let queryItem = URLQueryItem(name: "unreadOnly", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let value = continuation, !value.isEmpty {
|
||||
let queryItem = URLQueryItem(name: "continuation", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
queryItems.append(contentsOf: [
|
||||
URLQueryItem(name: "count", value: "1000"),
|
||||
URLQueryItem(name: "streamId", value: resource.id),
|
||||
])
|
||||
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
transport.send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MarkAction {
|
||||
case read
|
||||
case unread
|
||||
case saved
|
||||
case unsaved
|
||||
|
||||
var actionValue: String {
|
||||
switch self {
|
||||
case .read:
|
||||
return "markAsRead"
|
||||
case .unread:
|
||||
return "keepUnread"
|
||||
case .saved:
|
||||
return "markAsSaved"
|
||||
case .unsaved:
|
||||
return "markAsUnsaved"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MarkerEntriesBody: Encodable {
|
||||
let type = "entries"
|
||||
var action: String
|
||||
var entryIds: [String]
|
||||
}
|
||||
|
||||
func mark(_ articleIds: Set<String>, as action: MarkAction, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/markers"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
do {
|
||||
let body = MarkerEntriesBody(action: action.actionValue, entryIds: Array(articleIds))
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(body)
|
||||
request.httpBody = data
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
transport.send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (httpResponse, _)):
|
||||
if httpResponse.statusCode == 200 {
|
||||
completionHandler(.success(()))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func importOpml(_ opmlData: Data, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
|
@ -538,3 +373,264 @@ extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyGetCollectionsService {
|
||||
|
||||
func getCollections(completionHandler: @escaping (Result<[FeedlyCollection], Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/collections"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
transport.send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyGetStreamContentsService {
|
||||
|
||||
func getStreamContents(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/streams/contents"
|
||||
|
||||
var queryItems = [URLQueryItem]()
|
||||
|
||||
if let date = newerThan {
|
||||
let value = String(Int(date.timeIntervalSince1970 * 1000))
|
||||
let queryItem = URLQueryItem(name: "newerThan", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let flag = unreadOnly {
|
||||
let value = flag ? "true" : "false"
|
||||
let queryItem = URLQueryItem(name: "unreadOnly", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let value = continuation, !value.isEmpty {
|
||||
let queryItem = URLQueryItem(name: "continuation", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
queryItems.append(contentsOf: [
|
||||
URLQueryItem(name: "count", value: "1000"),
|
||||
URLQueryItem(name: "streamId", value: resource.id),
|
||||
])
|
||||
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
transport.send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyGetStreamIdsService {
|
||||
|
||||
func getStreamIds(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStreamIds, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/streams/ids"
|
||||
|
||||
var queryItems = [URLQueryItem]()
|
||||
|
||||
if let date = newerThan {
|
||||
let value = String(Int(date.timeIntervalSince1970 * 1000))
|
||||
let queryItem = URLQueryItem(name: "newerThan", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let flag = unreadOnly {
|
||||
let value = flag ? "true" : "false"
|
||||
let queryItem = URLQueryItem(name: "unreadOnly", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
if let value = continuation, !value.isEmpty {
|
||||
let queryItem = URLQueryItem(name: "continuation", value: value)
|
||||
queryItems.append(queryItem)
|
||||
}
|
||||
|
||||
queryItems.append(contentsOf: [
|
||||
URLQueryItem(name: "count", value: "1000"),
|
||||
URLQueryItem(name: "streamId", value: resource.id),
|
||||
])
|
||||
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
transport.send(request: request, resultType: FeedlyStreamIds.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, collections)):
|
||||
if let response = collections {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyGetEntriesService {
|
||||
|
||||
func getEntries(for ids: Set<String>, completionHandler: @escaping (Result<[FeedlyEntry], Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/entries/.mget"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
do {
|
||||
let body = Array(ids)
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(body)
|
||||
request.httpBody = data
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
transport.send(request: request, resultType: [FeedlyEntry].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (_, entries)):
|
||||
if let response = entries {
|
||||
completionHandler(.success(response))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyAPICaller: FeedlyMarkArticlesService {
|
||||
|
||||
private struct MarkerEntriesBody: Encodable {
|
||||
let type = "entries"
|
||||
var action: String
|
||||
var entryIds: [String]
|
||||
}
|
||||
|
||||
func mark(_ articleIds: Set<String>, as action: FeedlyMarkAction, completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard let accessToken = credentials?.secret else {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(CredentialsError.incompleteCredentials))
|
||||
}
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/markers"
|
||||
|
||||
guard let url = components.url else {
|
||||
fatalError("\(components) does not produce a valid URL.")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
|
||||
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
|
||||
do {
|
||||
let body = MarkerEntriesBody(action: action.actionValue, entryIds: Array(articleIds))
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(body)
|
||||
request.httpBody = data
|
||||
} catch {
|
||||
return DispatchQueue.main.async {
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
transport.send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
|
||||
switch result {
|
||||
case .success(let (httpResponse, _)):
|
||||
if httpResponse.statusCode == 200 {
|
||||
completionHandler(.success(()))
|
||||
} else {
|
||||
completionHandler(.failure(URLError(.cannotDecodeContentData)))
|
||||
}
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedly")
|
||||
private let database: SyncDatabase
|
||||
|
||||
private weak var currentSyncAllOperation: FeedlySyncAllOperation?
|
||||
|
||||
init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API = .default) {
|
||||
|
||||
if let transport = transport {
|
||||
|
@ -76,23 +78,44 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
|
||||
// MARK: Account API
|
||||
|
||||
private var syncStrategy: FeedlySyncStrategy?
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let date = Date()
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
guard currentSyncAllOperation == nil else {
|
||||
os_log(.debug, log: log, "Ignoring refreshAll: Feedly sync already in progress.")
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
guard let credentials = credentials else {
|
||||
os_log(.debug, log: log, "Ignoring refreshAll: Feedly account has no credentials.")
|
||||
completion(.failure(FeedlyAccountDelegateError.notLoggedIn))
|
||||
return
|
||||
}
|
||||
|
||||
let log = self.log
|
||||
let progress = refreshProgress
|
||||
progress.addToNumberOfTasksAndRemaining(1)
|
||||
syncStrategy?.startSync { result in
|
||||
os_log(.debug, log: log, "Sync took %.3f seconds", -date.timeIntervalSinceNow)
|
||||
|
||||
let operation = FeedlySyncAllOperation(account: account, credentials: credentials, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetch, log: log)
|
||||
|
||||
let date = Date()
|
||||
operation.syncCompletionHandler = { [weak self] result in
|
||||
self?.accountMetadata?.lastArticleFetch = date
|
||||
|
||||
os_log(.debug, log: log, "Sync took %{public}.3f seconds", -date.timeIntervalSinceNow)
|
||||
progress.completeTask()
|
||||
completion(result)
|
||||
}
|
||||
|
||||
currentSyncAllOperation = operation
|
||||
|
||||
OperationQueue.main.addOperation(operation)
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
|
||||
// Ensure remote articles have the same status as they do locally.
|
||||
let send = FeedlySendArticleStatusesOperation(database: database, caller: caller, log: log)
|
||||
let send = FeedlySendArticleStatusesOperation(database: database, service: caller, log: log)
|
||||
send.completionBlock = {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
|
@ -112,9 +135,32 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
/// - Parameter account: The account whose articles have a remote status.
|
||||
/// - Parameter completion: Call on the main queue.
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
|
||||
refreshAll(for: account) { _ in
|
||||
guard let credentials = credentials else {
|
||||
return completion()
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
let getUnread = FeedlySyncUnreadStatusesOperation(account: account, credentials: credentials, service: caller, newerThan: nil, log: log)
|
||||
|
||||
group.enter()
|
||||
getUnread.completionBlock = {
|
||||
group.leave()
|
||||
|
||||
}
|
||||
|
||||
let getStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: caller, log: log)
|
||||
|
||||
group.enter()
|
||||
getStarred.completionBlock = {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
completion()
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperations([getUnread, getStarred], waitUntilFinished: false)
|
||||
}
|
||||
|
||||
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
@ -410,11 +456,6 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||
|
||||
func accountDidInitialize(_ account: Account) {
|
||||
credentials = try? account.retrieveCredentials(type: .oauthAccessToken)
|
||||
|
||||
syncStrategy = FeedlySyncStrategy(account: account,
|
||||
caller: caller,
|
||||
database: database,
|
||||
log: log)
|
||||
}
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
|
|
|
@ -68,9 +68,9 @@ final class FeedlyAddFeedRequest {
|
|||
let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log)
|
||||
createFeeds.addDependency(addRequest)
|
||||
|
||||
let getStream: FeedlyGetStreamOperation? = {
|
||||
let getStream: FeedlyGetStreamContentsOperation? = {
|
||||
if refreshes {
|
||||
let op = FeedlyGetStreamOperation(account: account, resourceProvider: addRequest, caller: caller, newerThan: nil)
|
||||
let op = FeedlyGetStreamContentsOperation(account: account, resourceProvider: addRequest, service: caller, newerThan: nil)
|
||||
op.addDependency(createFeeds)
|
||||
return op
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ final class FeedlyAddFeedRequest {
|
|||
|
||||
let organiseByFeed: FeedlyOrganiseParsedItemsByFeedOperation? = {
|
||||
if let getStream = getStream {
|
||||
let op = FeedlyOrganiseParsedItemsByFeedOperation(account: account, entryProvider: getStream, log: log)
|
||||
let op = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: getStream, log: log)
|
||||
op.addDependency(getStream)
|
||||
return op
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ struct FeedlyFeedContainerValidator {
|
|||
throw FeedlyAccountDelegateError.notLoggedIn
|
||||
}
|
||||
|
||||
let uncategorized = FeedlyCategoryResourceId.uncategorized(for: userId)
|
||||
let uncategorized = FeedlyCategoryResourceId.Global.uncategorized(for: userId)
|
||||
|
||||
guard collectionId != uncategorized.id else {
|
||||
throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder)
|
||||
|
|
|
@ -84,7 +84,7 @@ struct FeedlyEntryParser {
|
|||
|
||||
var parsedItemRepresentation: ParsedItem {
|
||||
return ParsedItem(syncServiceID: id,
|
||||
uniqueID: id,
|
||||
uniqueID: id, // This value seems to get ignored or replaced.
|
||||
feedURL: feedUrl,
|
||||
url: nil,
|
||||
externalURL: externalUrl,
|
||||
|
|
|
@ -43,19 +43,32 @@ extension FeedlyFeedResourceId {
|
|||
struct FeedlyCategoryResourceId: FeedlyResourceId {
|
||||
var id: String
|
||||
|
||||
static func uncategorized(for userId: String) -> FeedlyCategoryResourceId {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userId)/category/global.uncategorized"
|
||||
return FeedlyCategoryResourceId(id: id)
|
||||
enum Global {
|
||||
|
||||
static func uncategorized(for userId: String) -> FeedlyCategoryResourceId {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userId)/category/global.uncategorized"
|
||||
return FeedlyCategoryResourceId(id: id)
|
||||
}
|
||||
|
||||
/// All articles from all the feeds the user subscribes to.
|
||||
static func all(for userId: String) -> FeedlyCategoryResourceId {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userId)/category/global.all"
|
||||
return FeedlyCategoryResourceId(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedlyTagResourceId: FeedlyResourceId {
|
||||
var id: String
|
||||
|
||||
static func saved(for userId: String) -> FeedlyTagResourceId {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userId)/tag/global.saved"
|
||||
return FeedlyTagResourceId(id: id)
|
||||
enum Global {
|
||||
|
||||
static func saved(for userId: String) -> FeedlyTagResourceId {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userId)/tag/global.saved"
|
||||
return FeedlyTagResourceId(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,13 @@ import Foundation
|
|||
|
||||
struct FeedlyStream: Decodable {
|
||||
var id: String
|
||||
var timestamp: Date?
|
||||
|
||||
/// Of the most recent entry for this stream (regardless of continuation, newerThan, etc).
|
||||
var updated: Date?
|
||||
|
||||
/// Optional string the continuation id to pass to the next stream call, for pagination.
|
||||
/// This id guarantees that no entry will be duplicated in a stream (meaning, there is no need to de-duplicate entries returned by this call).
|
||||
/// If this value is not returned, it means the end of the stream has been reached.
|
||||
var continuation: String?
|
||||
var items: [FeedlyEntry]
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// FeedlyStreamIds.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 18/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyStreamIds: Decodable {
|
||||
var continuation: String?
|
||||
var ids: [String]
|
||||
|
||||
var isStreamEnd: Bool {
|
||||
return continuation == nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// FeedlyCheckpointOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 18/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyCheckpointOperationDelegate: class {
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation)
|
||||
}
|
||||
|
||||
/// Single responsibility is to let the delegate know an instance is executing. The semantics are up to the delegate.
|
||||
final class FeedlyCheckpointOperation: FeedlyOperation {
|
||||
|
||||
weak var checkpointDelegate: FeedlyCheckpointOperationDelegate?
|
||||
|
||||
override func main() {
|
||||
defer { didFinish() }
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
checkpointDelegate?.feedlyCheckpointOperationDidReachCheckpoint(self)
|
||||
}
|
||||
}
|
|
@ -9,28 +9,24 @@
|
|||
import Foundation
|
||||
|
||||
/// An operation with a queue of its own.
|
||||
final class FeedlyCompoundOperation: FeedlyOperation {
|
||||
final class FeedlyCompoundOperation: FeedlyOperation, FeedlyCheckpointOperationDelegate {
|
||||
private let operationQueue = OperationQueue()
|
||||
private var finishOperation: BlockOperation?
|
||||
private let finishOperation = FeedlyCheckpointOperation()
|
||||
|
||||
init(operations: [Operation]) {
|
||||
assert(!operations.isEmpty)
|
||||
operationQueue.isSuspended = true
|
||||
finishOperation = nil
|
||||
|
||||
super.init()
|
||||
|
||||
let finish = BlockOperation {
|
||||
self.didFinish()
|
||||
}
|
||||
|
||||
finishOperation = finish
|
||||
finishOperation.checkpointDelegate = self
|
||||
|
||||
for operation in operations {
|
||||
finish.addDependency(operation)
|
||||
finishOperation.addDependency(operation)
|
||||
}
|
||||
|
||||
var initialOperations = operations
|
||||
initialOperations.append(finish)
|
||||
initialOperations.append(finishOperation)
|
||||
|
||||
operationQueue.addOperations(initialOperations, waitUntilFinished: false)
|
||||
}
|
||||
|
@ -48,9 +44,13 @@ final class FeedlyCompoundOperation: FeedlyOperation {
|
|||
operationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
|
||||
didFinish()
|
||||
}
|
||||
|
||||
func addAnotherOperation(_ operation: Operation) {
|
||||
guard !isCancelled else { return }
|
||||
finishOperation?.addDependency(operation)
|
||||
finishOperation.addDependency(operation)
|
||||
operationQueue.addOperation(operation)
|
||||
}
|
||||
|
|
@ -16,13 +16,13 @@ protocol FeedlyCollectionProviding: class {
|
|||
/// Single responsibility is to get Collections from Feedly.
|
||||
final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding {
|
||||
|
||||
let caller: FeedlyAPICaller
|
||||
let service: FeedlyGetCollectionsService
|
||||
let log: OSLog
|
||||
|
||||
private(set) var collections = [FeedlyCollection]()
|
||||
|
||||
init(caller: FeedlyAPICaller, log: OSLog) {
|
||||
self.caller = caller
|
||||
init(service: FeedlyGetCollectionsService, log: OSLog) {
|
||||
self.service = service
|
||||
self.log = log
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProv
|
|||
|
||||
os_log(.debug, log: log, "Requesting collections.")
|
||||
|
||||
caller.getCollections { result in
|
||||
service.getCollections { result in
|
||||
switch result {
|
||||
case .success(let collections):
|
||||
os_log(.debug, log: self.log, "Received collections: %@.", collections.map { $0.id })
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// FeedlyGetEntriesOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 28/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Single responsibility is to get full entries for the entry identifiers.
|
||||
final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding {
|
||||
let account: Account
|
||||
let service: FeedlyGetEntriesService
|
||||
let provider: FeedlyEntryIdenifierProviding
|
||||
|
||||
init(account: Account, service: FeedlyGetEntriesService, provider: FeedlyEntryIdenifierProviding) {
|
||||
self.account = account
|
||||
self.service = service
|
||||
self.provider = provider
|
||||
}
|
||||
|
||||
private (set) var entries = [FeedlyEntry]()
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
service.getEntries(for: provider.entryIds) { result in
|
||||
switch result {
|
||||
case .success(let entries):
|
||||
self.entries = entries
|
||||
self.didFinish()
|
||||
|
||||
case .failure(let error):
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,18 +9,21 @@
|
|||
import Foundation
|
||||
import RSParser
|
||||
|
||||
protocol FeedlyEntryProviding: class {
|
||||
var resource: FeedlyResourceId { get }
|
||||
protocol FeedlyEntryProviding {
|
||||
var entries: [FeedlyEntry] { get }
|
||||
}
|
||||
|
||||
protocol FeedlyParsedItemProviding {
|
||||
var resource: FeedlyResourceId { get }
|
||||
var parsedEntries: Set<ParsedItem> { get }
|
||||
}
|
||||
|
||||
protocol FeedlyGetStreamOperationDelegate: class {
|
||||
func feedlyGetStreamOperation(_ operation: FeedlyGetStreamOperation, didGet stream: FeedlyStream)
|
||||
protocol FeedlyGetStreamContentsOperationDelegate: class {
|
||||
func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream)
|
||||
}
|
||||
|
||||
/// Single responsibility is to get the stream content of a Collection from Feedly.
|
||||
final class FeedlyGetStreamOperation: FeedlyOperation, FeedlyEntryProviding {
|
||||
final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
|
||||
|
||||
struct ResourceProvider: FeedlyResourceProviding {
|
||||
var resource: FeedlyResourceId
|
||||
|
@ -33,7 +36,7 @@ final class FeedlyGetStreamOperation: FeedlyOperation, FeedlyEntryProviding {
|
|||
}
|
||||
|
||||
var entries: [FeedlyEntry] {
|
||||
guard let entries = storedStream?.items else {
|
||||
guard let entries = stream?.items else {
|
||||
assert(isFinished, "This should only be called when the operation finishes without error.")
|
||||
assertionFailure("Has this operation been addeded as a dependency on the caller?")
|
||||
return []
|
||||
|
@ -52,7 +55,7 @@ final class FeedlyGetStreamOperation: FeedlyOperation, FeedlyEntryProviding {
|
|||
return parsed
|
||||
}
|
||||
|
||||
private var storedStream: FeedlyStream? {
|
||||
private(set) var stream: FeedlyStream? {
|
||||
didSet {
|
||||
storedParsedEntries = nil
|
||||
}
|
||||
|
@ -61,24 +64,24 @@ final class FeedlyGetStreamOperation: FeedlyOperation, FeedlyEntryProviding {
|
|||
private var storedParsedEntries: Set<ParsedItem>?
|
||||
|
||||
let account: Account
|
||||
let caller: FeedlyAPICaller
|
||||
let service: FeedlyGetStreamContentsService
|
||||
let unreadOnly: Bool?
|
||||
let newerThan: Date?
|
||||
let continuation: String?
|
||||
|
||||
weak var streamDelegate: FeedlyGetStreamOperationDelegate?
|
||||
weak var streamDelegate: FeedlyGetStreamContentsOperationDelegate?
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, caller: FeedlyAPICaller, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool? = nil) {
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool? = nil) {
|
||||
self.account = account
|
||||
self.resourceProvider = ResourceProvider(resource: resource)
|
||||
self.caller = caller
|
||||
self.service = service
|
||||
self.continuation = continuation
|
||||
self.unreadOnly = unreadOnly
|
||||
self.newerThan = newerThan
|
||||
}
|
||||
|
||||
convenience init(account: Account, resourceProvider: FeedlyResourceProviding, caller: FeedlyAPICaller, newerThan: Date?, unreadOnly: Bool? = nil) {
|
||||
self.init(account: account, resource: resourceProvider.resource, caller: caller, newerThan: newerThan, unreadOnly: unreadOnly)
|
||||
convenience init(account: Account, resourceProvider: FeedlyResourceProviding, service: FeedlyGetStreamContentsService, newerThan: Date?, unreadOnly: Bool? = nil) {
|
||||
self.init(account: account, resource: resourceProvider.resource, service: service, newerThan: newerThan, unreadOnly: unreadOnly)
|
||||
}
|
||||
|
||||
override func main() {
|
||||
|
@ -87,12 +90,12 @@ final class FeedlyGetStreamOperation: FeedlyOperation, FeedlyEntryProviding {
|
|||
return
|
||||
}
|
||||
|
||||
caller.getStream(for: resourceProvider.resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in
|
||||
service.getStreamContents(for: resourceProvider.resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in
|
||||
switch result {
|
||||
case .success(let stream):
|
||||
self.storedStream = stream
|
||||
self.stream = stream
|
||||
|
||||
self.streamDelegate?.feedlyGetStreamOperation(self, didGet: stream)
|
||||
self.streamDelegate?.feedlyGetStreamContentsOperation(self, didGetContentsOf: stream)
|
||||
|
||||
self.didFinish()
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// FeedlyGetStreamIdsOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 18/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
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 {
|
||||
|
||||
var entryIds: Set<String> {
|
||||
guard let ids = streamIds?.ids else {
|
||||
assert(isFinished, "This should only be called when the operation finishes without error.")
|
||||
assertionFailure("Has this operation been addeded as a dependency on the caller?")
|
||||
return []
|
||||
}
|
||||
return Set(ids)
|
||||
}
|
||||
|
||||
private(set) var streamIds: FeedlyStreamIds?
|
||||
|
||||
let account: Account
|
||||
let service: FeedlyGetStreamIdsService
|
||||
let continuation: String?
|
||||
let resource: FeedlyResourceId
|
||||
let unreadOnly: Bool?
|
||||
let newerThan: Date?
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, continuation: String? = nil, newerThan: Date? = nil, unreadOnly: Bool?) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.continuation = continuation
|
||||
self.newerThan = newerThan
|
||||
self.unreadOnly = unreadOnly
|
||||
}
|
||||
|
||||
weak var streamIdsDelegate: FeedlyGetStreamIdsOperationDelegate?
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
service.getStreamIds(for: resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in
|
||||
switch result {
|
||||
case .success(let stream):
|
||||
self.streamIds = stream
|
||||
|
||||
self.streamIdsDelegate?.feedlyGetStreamIdsOperation(self, didGet: stream)
|
||||
|
||||
self.didFinish()
|
||||
|
||||
case .failure(let error):
|
||||
self.didFinish(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ protocol FeedlyFeedsAndFoldersProviding {
|
|||
/// Single responsibility is accurately reflect Collections from Feedly as Folders.
|
||||
final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyCollectionsAndFoldersProviding, FeedlyFeedsAndFoldersProviding {
|
||||
|
||||
let caller: FeedlyAPICaller
|
||||
let account: Account
|
||||
let collectionsProvider: FeedlyCollectionProviding
|
||||
let log: OSLog
|
||||
|
@ -28,10 +27,9 @@ final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyCo
|
|||
private(set) var collectionsAndFolders = [(FeedlyCollection, Folder)]()
|
||||
private(set) var feedsAndFolders = [([FeedlyFeed], Folder)]()
|
||||
|
||||
init(account: Account, collectionsProvider: FeedlyCollectionProviding, caller: FeedlyAPICaller, log: OSLog) {
|
||||
init(account: Account, collectionsProvider: FeedlyCollectionProviding, log: OSLog) {
|
||||
self.collectionsProvider = collectionsProvider
|
||||
self.account = account
|
||||
self.caller = caller
|
||||
self.log = log
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ class FeedlyOperation: Operation {
|
|||
return isExecutingOperation
|
||||
}
|
||||
|
||||
var isExecutingOperation = false {
|
||||
private var isExecutingOperation = false {
|
||||
willSet {
|
||||
willChangeValue(for: \.isExecuting)
|
||||
}
|
|
@ -18,7 +18,7 @@ protocol FeedlyParsedItemsByFeedProviding {
|
|||
/// Single responsibility is to group articles by their feeds.
|
||||
final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyParsedItemsByFeedProviding {
|
||||
private let account: Account
|
||||
private let entryProvider: FeedlyEntryProviding
|
||||
private let parsedItemProvider: FeedlyParsedItemProviding
|
||||
private let log: OSLog
|
||||
|
||||
var parsedItemsKeyedByFeedId: [String : Set<ParsedItem>] {
|
||||
|
@ -27,14 +27,14 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar
|
|||
}
|
||||
|
||||
var providerName: String {
|
||||
return entryProvider.resource.id
|
||||
return parsedItemProvider.resource.id
|
||||
}
|
||||
|
||||
private var itemsKeyedByFeedId = [String: Set<ParsedItem>]()
|
||||
|
||||
init(account: Account, entryProvider: FeedlyEntryProviding, log: OSLog) {
|
||||
init(account: Account, parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) {
|
||||
self.account = account
|
||||
self.entryProvider = entryProvider
|
||||
self.parsedItemProvider = parsedItemProvider
|
||||
self.log = log
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar
|
|||
|
||||
guard !isCancelled else { return }
|
||||
|
||||
let items = entryProvider.parsedEntries
|
||||
let items = parsedItemProvider.parsedEntries
|
||||
var dict = [String: Set<ParsedItem>](minimumCapacity: items.count)
|
||||
|
||||
for item in items {
|
||||
|
@ -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, entryProvider.resource.id)
|
||||
os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.resource.id)
|
||||
|
||||
itemsKeyedByFeedId = dict
|
||||
}
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
import os.log
|
||||
|
||||
protocol FeedlyRequestStreamsOperationDelegate: class {
|
||||
func feedlyRequestStreamsOperation(_ operation: FeedlyRequestStreamsOperation, enqueue collectionStreamOperation: FeedlyGetStreamOperation)
|
||||
func feedlyRequestStreamsOperation(_ operation: FeedlyRequestStreamsOperation, enqueue collectionStreamOperation: FeedlyGetStreamContentsOperation)
|
||||
}
|
||||
|
||||
/// Single responsibility is to create one stream request operation for one Feedly collection.
|
||||
|
@ -20,15 +20,15 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation {
|
|||
weak var queueDelegate: FeedlyRequestStreamsOperationDelegate?
|
||||
|
||||
let collectionsProvider: FeedlyCollectionProviding
|
||||
let caller: FeedlyAPICaller
|
||||
let service: FeedlyGetStreamContentsService
|
||||
let account: Account
|
||||
let log: OSLog
|
||||
let newerThan: Date?
|
||||
let unreadOnly: Bool?
|
||||
|
||||
init(account: Account, collectionsProvider: FeedlyCollectionProviding, newerThan: Date?, unreadOnly: Bool?, caller: FeedlyAPICaller, log: OSLog) {
|
||||
init(account: Account, collectionsProvider: FeedlyCollectionProviding, newerThan: Date?, unreadOnly: Bool?, service: FeedlyGetStreamContentsService, log: OSLog) {
|
||||
self.account = account
|
||||
self.caller = caller
|
||||
self.service = service
|
||||
self.collectionsProvider = collectionsProvider
|
||||
self.newerThan = newerThan
|
||||
self.unreadOnly = unreadOnly
|
||||
|
@ -46,9 +46,9 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation {
|
|||
|
||||
for collection in collectionsProvider.collections {
|
||||
let resource = FeedlyCategoryResourceId(id: collection.id)
|
||||
let operation = FeedlyGetStreamOperation(account: account,
|
||||
let operation = FeedlyGetStreamContentsOperation(account: account,
|
||||
resource: resource,
|
||||
caller: caller,
|
||||
service: service,
|
||||
newerThan: newerThan,
|
||||
unreadOnly: unreadOnly)
|
||||
queueDelegate?.feedlyRequestStreamsOperation(self, enqueue: operation)
|
|
@ -11,15 +11,15 @@ import Articles
|
|||
import SyncDatabase
|
||||
import os.log
|
||||
|
||||
/// Single responsibility is to update or ensure articles from the entry provider are the only starred articles.
|
||||
/// Single responsibility is to take changes to statuses of articles locally and apply them to the corresponding the articles remotely.
|
||||
final class FeedlySendArticleStatusesOperation: FeedlyOperation {
|
||||
private let database: SyncDatabase
|
||||
private let log: OSLog
|
||||
private let caller: FeedlyAPICaller
|
||||
private let service: FeedlyMarkArticlesService
|
||||
|
||||
init(database: SyncDatabase, caller: FeedlyAPICaller, log: OSLog) {
|
||||
init(database: SyncDatabase, service: FeedlyMarkArticlesService, log: OSLog) {
|
||||
self.database = database
|
||||
self.caller = caller
|
||||
self.service = service
|
||||
self.log = log
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation {
|
|||
|
||||
let pending = database.selectForProcessing()
|
||||
|
||||
let statuses: [(status: ArticleStatus.Key, flag: Bool, action: FeedlyAPICaller.MarkAction)] = [
|
||||
let statuses: [(status: ArticleStatus.Key, flag: Bool, action: FeedlyMarkAction)] = [
|
||||
(.read, false, .unread),
|
||||
(.read, true, .read),
|
||||
(.starred, true, .saved),
|
||||
|
@ -52,14 +52,18 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation {
|
|||
let ids = Set(articleIds.map { $0.articleID })
|
||||
let database = self.database
|
||||
group.enter()
|
||||
caller.mark(ids, as: pairing.action) { result in
|
||||
service.mark(ids, as: pairing.action) { result in
|
||||
assert(Thread.isMainThread)
|
||||
switch result {
|
||||
case .success:
|
||||
database.deleteSelectedForProcessing(Array(ids))
|
||||
database.deleteSelectedForProcessing(Array(ids)) {
|
||||
group.leave()
|
||||
}
|
||||
case .failure:
|
||||
database.resetSelectedForProcessing(Array(ids))
|
||||
database.resetSelectedForProcessing(Array(ids)) {
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,15 +9,20 @@
|
|||
import Foundation
|
||||
import os.log
|
||||
|
||||
/// Single responsibility is to update or ensure articles from the entry provider are the only starred articles.
|
||||
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 allStarredEntriesProvider: FeedlyEntryProviding
|
||||
private let allStarredEntryIdsProvider: FeedlyStarredEntryIdProviding
|
||||
private let log: OSLog
|
||||
|
||||
init(account: Account, allStarredEntriesProvider: FeedlyEntryProviding, log: OSLog) {
|
||||
init(account: Account, allStarredEntryIdsProvider: FeedlyStarredEntryIdProviding, log: OSLog) {
|
||||
self.account = account
|
||||
self.allStarredEntriesProvider = allStarredEntriesProvider
|
||||
self.allStarredEntryIdsProvider = allStarredEntryIdsProvider
|
||||
self.log = log
|
||||
}
|
||||
|
||||
|
@ -28,7 +33,7 @@ final class FeedlySetStarredArticlesOperation: FeedlyOperation {
|
|||
return
|
||||
}
|
||||
|
||||
let remoteStarredArticleIds = Set(allStarredEntriesProvider.entries.map { $0.id })
|
||||
let remoteStarredArticleIds = allStarredEntryIdsProvider.entryIds
|
||||
let localStarredArticleIDs = account.fetchStarredArticleIDs()
|
||||
|
||||
// Mark articles as starred
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
let remoteUnreadArticleIds = allUnreadIdsProvider.entryIds
|
||||
//Set(entries.filter { $0.unread }.map { $0.id })
|
||||
let localUnreadArticleIds = account.fetchUnreadArticleIDs()
|
||||
|
||||
// Mark articles as unread
|
||||
let deltaUnreadArticleIds = remoteUnreadArticleIds.subtracting(localUnreadArticleIds)
|
||||
let markUnreadArticles = account.fetchArticles(.articleIDs(deltaUnreadArticleIds))
|
||||
account.update(markUnreadArticles, statusKey: .read, flag: false)
|
||||
|
||||
// Save any unread statuses for articles we haven't yet received
|
||||
let markUnreadArticleIDs = Set(markUnreadArticles.map { $0.articleID })
|
||||
let missingUnreadArticleIDs = deltaUnreadArticleIds.subtracting(markUnreadArticleIDs)
|
||||
account.ensureStatuses(missingUnreadArticleIDs, true, .read, false)
|
||||
|
||||
// Mark articles as read
|
||||
let deltaReadArticleIds = localUnreadArticleIds.subtracting(remoteUnreadArticleIds)
|
||||
let markReadArticles = account.fetchArticles(.articleIDs(deltaReadArticleIds))
|
||||
account.update(markReadArticles, statusKey: .read, flag: true)
|
||||
|
||||
// Save any read statuses for articles we haven't yet received
|
||||
let markReadArticleIDs = Set(markReadArticles.map { $0.articleID })
|
||||
let missingReadArticleIDs = deltaReadArticleIds.subtracting(markReadArticleIDs)
|
||||
account.ensureStatuses(missingReadArticleIDs, true, .read, true)
|
||||
|
||||
didFinish()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
//
|
||||
// FeedlySyncAllOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 19/9/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import SyncDatabase
|
||||
|
||||
/// Single responsibility is to compose the operations necessary to get the entire set of articles, feeds and folders with the statuses the user expects between now and a certain date in the past.
|
||||
final class FeedlySyncAllOperation: FeedlyOperation {
|
||||
private let operationQueue: OperationQueue
|
||||
private let log: OSLog
|
||||
let syncUUID: UUID
|
||||
|
||||
var syncCompletionHandler: ((Result<Void, Error>) -> ())?
|
||||
|
||||
init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredArticlesService: FeedlyGetStreamContentsService, database: SyncDatabase, log: OSLog) {
|
||||
self.syncUUID = UUID()
|
||||
self.log = log
|
||||
self.operationQueue = OperationQueue()
|
||||
self.operationQueue.isSuspended = true
|
||||
|
||||
super.init()
|
||||
|
||||
// Send any read/unread/starred article statuses to Feedly before anything else.
|
||||
let sendArticleStatuses = FeedlySendArticleStatusesOperation(database: database, service: markArticlesService, log: log)
|
||||
sendArticleStatuses.delegate = self
|
||||
self.operationQueue.addOperation(sendArticleStatuses)
|
||||
|
||||
// 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)
|
||||
getUnread.delegate = self
|
||||
getUnread.addDependency(sendArticleStatuses)
|
||||
self.operationQueue.addOperation(getUnread)
|
||||
|
||||
// Get all the Collections the user has.
|
||||
let getCollections = FeedlyGetCollectionsOperation(service: getCollectionsService, log: log)
|
||||
getCollections.delegate = self
|
||||
getCollections.addDependency(sendArticleStatuses)
|
||||
self.operationQueue.addOperation(getCollections)
|
||||
|
||||
// Ensure a folder exists for each Collection, removing Folders without a corresponding Collection.
|
||||
let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, log: log)
|
||||
mirrorCollectionsAsFolders.delegate = self
|
||||
mirrorCollectionsAsFolders.addDependency(getCollections)
|
||||
self.operationQueue.addOperation(mirrorCollectionsAsFolders)
|
||||
|
||||
// Ensure feeds are created and grouped by their folders.
|
||||
let createFeedsOperation = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: mirrorCollectionsAsFolders, log: log)
|
||||
createFeedsOperation.delegate = self
|
||||
createFeedsOperation.addDependency(mirrorCollectionsAsFolders)
|
||||
self.operationQueue.addOperation(createFeedsOperation)
|
||||
|
||||
// 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.addDependency(getCollections)
|
||||
getStreamContents.addDependency(getUnread)
|
||||
getStreamContents.addDependency(createFeedsOperation)
|
||||
self.operationQueue.addOperation(getStreamContents)
|
||||
|
||||
// Get each and every starred article.
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, service: getStarredArticlesService, log: log)
|
||||
syncStarred.addDependency(createFeedsOperation)
|
||||
self.operationQueue.addOperation(syncStarred)
|
||||
|
||||
// Once this operation's dependencies, their dependencies etc finish, we can finish.
|
||||
let finishOperation = FeedlyCheckpointOperation()
|
||||
finishOperation.checkpointDelegate = self
|
||||
finishOperation.addDependency(getStreamContents)
|
||||
finishOperation.addDependency(syncStarred)
|
||||
|
||||
self.operationQueue.addOperation(finishOperation)
|
||||
}
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, 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, log: log)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
os_log(.debug, log: log, "Cancelling sync %{public}@", syncUUID.uuidString)
|
||||
self.operationQueue.cancelAllOperations()
|
||||
|
||||
syncCompletionHandler?(.failure(URLError(.cancelled)))
|
||||
syncCompletionHandler = nil
|
||||
|
||||
self.didFinish()
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
os_log(.debug, log: log, "Starting sync %{public}@", syncUUID.uuidString)
|
||||
operationQueue.isSuspended = false
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlySyncAllOperation: FeedlyCheckpointOperationDelegate {
|
||||
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
|
||||
assert(Thread.isMainThread)
|
||||
os_log(.debug, log: self.log, "Sync completed: %{public}@", syncUUID.uuidString)
|
||||
|
||||
syncCompletionHandler?(.success(()))
|
||||
syncCompletionHandler = nil
|
||||
|
||||
didFinish()
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlySyncAllOperation: FeedlyOperationDelegate {
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
assert(Thread.isMainThread)
|
||||
os_log(.debug, log: log, "%{public}@ failed with error: %{public}@.", operation, error.localizedDescription)
|
||||
|
||||
syncCompletionHandler?(.failure(error))
|
||||
syncCompletionHandler = nil
|
||||
|
||||
cancel()
|
||||
}
|
||||
}
|
|
@ -10,16 +10,17 @@ import Foundation
|
|||
import os.log
|
||||
import RSParser
|
||||
|
||||
final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamOperationDelegate {
|
||||
final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
private let account: Account
|
||||
private let operationQueue: OperationQueue
|
||||
private let caller: FeedlyAPICaller
|
||||
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 {
|
||||
private class StarredEntryProvider: FeedlyEntryProviding, FeedlyStarredEntryIdProviding, FeedlyParsedItemProviding {
|
||||
var resource: FeedlyResourceId
|
||||
|
||||
private(set) var parsedEntries = Set<ParsedItem>()
|
||||
|
@ -29,37 +30,46 @@ final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperation
|
|||
self.resource = resource
|
||||
}
|
||||
|
||||
func addEntries(from provider: FeedlyEntryProviding) {
|
||||
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
|
||||
|
||||
init(account: Account, credentials: Credentials, caller: FeedlyAPICaller, log: OSLog) {
|
||||
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.caller = caller
|
||||
self.service = service
|
||||
self.operationQueue = OperationQueue()
|
||||
self.operationQueue.isSuspended = true
|
||||
self.finishOperation = FeedlyCheckpointOperation()
|
||||
self.log = log
|
||||
|
||||
let saved = FeedlyTagResourceId.saved(for: credentials.username)
|
||||
let provider = StarredEntryProvider(resource: saved)
|
||||
let provider = StarredEntryProvider(resource: resource)
|
||||
self.entryProvider = provider
|
||||
self.setStatuses = FeedlySetStarredArticlesOperation(account: account,
|
||||
allStarredEntriesProvider: provider,
|
||||
allStarredEntryIdsProvider: provider,
|
||||
log: log)
|
||||
|
||||
super.init()
|
||||
|
||||
let getFirstPage = FeedlyGetStreamOperation(account: account,
|
||||
resource: saved,
|
||||
caller: caller,
|
||||
let getFirstPage = FeedlyGetStreamContentsOperation(account: account,
|
||||
resource: resource,
|
||||
service: service,
|
||||
newerThan: nil)
|
||||
|
||||
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
|
||||
entryProvider: provider,
|
||||
parsedItemProvider: provider,
|
||||
log: log)
|
||||
|
||||
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
|
||||
|
@ -78,12 +88,7 @@ final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperation
|
|||
updateAccount.addDependency(organiseByFeed)
|
||||
updateAccount.delegate = self
|
||||
|
||||
let finishOperation = BlockOperation { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self?.didFinish()
|
||||
}
|
||||
}
|
||||
|
||||
finishOperation.checkpointDelegate = self
|
||||
finishOperation.addDependency(updateAccount)
|
||||
|
||||
let operations = [getFirstPage, setStatuses, organiseByFeed, updateAccount, finishOperation]
|
||||
|
@ -104,7 +109,7 @@ final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperation
|
|||
operationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
func feedlyGetStreamOperation(_ operation: FeedlyGetStreamOperation, didGet stream: FeedlyStream) {
|
||||
func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) {
|
||||
entryProvider.addEntries(from: operation)
|
||||
os_log(.debug, log: log, "Collecting %i items from %@", stream.items.count, stream.id)
|
||||
|
||||
|
@ -112,9 +117,9 @@ final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperation
|
|||
return
|
||||
}
|
||||
|
||||
let nextPageOperation = FeedlyGetStreamOperation(account: operation.account,
|
||||
let nextPageOperation = FeedlyGetStreamContentsOperation(account: operation.account,
|
||||
resource: operation.resource,
|
||||
caller: operation.caller,
|
||||
service: operation.service,
|
||||
continuation: continuation,
|
||||
newerThan: operation.newerThan)
|
||||
nextPageOperation.delegate = self
|
||||
|
@ -124,7 +129,12 @@ final class FeedlySyncStarredArticlesOperation: FeedlyOperation, FeedlyOperation
|
|||
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)
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// FeedlySyncStreamContentsOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 17/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import RSParser
|
||||
|
||||
final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
private let account: Account
|
||||
private let resource: FeedlyResourceId
|
||||
private let operationQueue: OperationQueue
|
||||
private let service: FeedlyGetStreamContentsService
|
||||
private let newerThan: Date?
|
||||
private let log: OSLog
|
||||
private let finishOperation: FeedlyCheckpointOperation
|
||||
|
||||
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, newerThan: Date?, log: OSLog) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.operationQueue = OperationQueue()
|
||||
self.operationQueue.isSuspended = true
|
||||
self.newerThan = newerThan
|
||||
self.log = log
|
||||
self.finishOperation = FeedlyCheckpointOperation()
|
||||
|
||||
super.init()
|
||||
|
||||
self.operationQueue.addOperation(self.finishOperation)
|
||||
self.finishOperation.checkpointDelegate = self
|
||||
enqueueOperations(for: nil)
|
||||
}
|
||||
|
||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamContentsService, newerThan: Date?, log: OSLog) {
|
||||
let all = FeedlyCategoryResourceId.Global.all(for: credentials.username)
|
||||
self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log)
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
operationQueue.cancelAllOperations()
|
||||
super.cancel()
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
operationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
func enqueueOperations(for continuation: String?) {
|
||||
os_log(.debug, log: log, "Requesting page for %@", resource.id)
|
||||
let operations = pageOperations(for: continuation)
|
||||
operationQueue.addOperations(operations, waitUntilFinished: false)
|
||||
}
|
||||
|
||||
func pageOperations(for continuation: String?) -> [Operation] {
|
||||
let getPage = FeedlyGetStreamContentsOperation(account: account,
|
||||
resource: resource,
|
||||
service: service,
|
||||
continuation: continuation,
|
||||
newerThan: newerThan)
|
||||
|
||||
|
||||
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
|
||||
parsedItemProvider: getPage,
|
||||
log: log)
|
||||
|
||||
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
|
||||
organisedItemsProvider: organiseByFeed,
|
||||
log: log)
|
||||
|
||||
getPage.delegate = self
|
||||
getPage.streamDelegate = self
|
||||
|
||||
organiseByFeed.addDependency(getPage)
|
||||
organiseByFeed.delegate = self
|
||||
|
||||
updateAccount.addDependency(organiseByFeed)
|
||||
updateAccount.delegate = self
|
||||
|
||||
finishOperation.addDependency(updateAccount)
|
||||
|
||||
return [getPage, organiseByFeed, updateAccount]
|
||||
}
|
||||
|
||||
func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) {
|
||||
os_log(.debug, log: log, "Ingesting %i items from %@", stream.items.count, stream.id)
|
||||
|
||||
guard let continuation = stream.continuation else {
|
||||
os_log(.debug, log: log, "Reached end of stream for %@", stream.id)
|
||||
return
|
||||
}
|
||||
|
||||
enqueueOperations(for: continuation)
|
||||
}
|
||||
|
||||
func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) {
|
||||
os_log(.debug, log: log, "Completed ingesting items from %@", resource.id)
|
||||
didFinish()
|
||||
}
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
operationQueue.cancelAllOperations()
|
||||
didFinish(error)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
//
|
||||
// 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)
|
||||
|
||||
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() {
|
||||
operationQueue.cancelAllOperations()
|
||||
super.cancel()
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
operationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
func feedlyGetStreamIdsOperation(_ operation: FeedlyGetStreamIdsOperation, didGet streamIds: FeedlyStreamIds) {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
//
|
||||
// FeedlyRefreshStreamEntriesStatusOperation.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 25/9/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
/// Single responsibility is to update the read status of articles stored locally with the unread status of the entries in a Collection's stream from Feedly.
|
||||
final class FeedlyRefreshStreamEntriesStatusOperation: FeedlyOperation {
|
||||
private let account: Account
|
||||
private let entryProvider: FeedlyEntryProviding
|
||||
private let log: OSLog
|
||||
|
||||
init(account: Account, entryProvider: FeedlyEntryProviding, log: OSLog) {
|
||||
self.account = account
|
||||
self.entryProvider = entryProvider
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else {
|
||||
didFinish()
|
||||
return
|
||||
}
|
||||
|
||||
let entries = entryProvider.entries
|
||||
let unreadArticleIds = Set(entries.filter { $0.unread }.map { $0.id })
|
||||
|
||||
// Mark articles as unread
|
||||
let currentUnreadArticleIDs = account.fetchUnreadArticleIDs()
|
||||
let deltaUnreadArticleIDs = unreadArticleIds.subtracting(currentUnreadArticleIDs)
|
||||
let markUnreadArticles = account.fetchArticles(.articleIDs(deltaUnreadArticleIDs))
|
||||
account.update(markUnreadArticles, statusKey: .read, flag: false)
|
||||
|
||||
let readAritcleIds = Set(entries.filter { !$0.unread }.map { $0.id })
|
||||
|
||||
let deltaReadArticleIDs = currentUnreadArticleIDs.intersection(readAritcleIds)
|
||||
let markReadArticles = account.fetchArticles(.articleIDs(deltaReadArticleIDs))
|
||||
account.update(markReadArticles, statusKey: .read, flag: true)
|
||||
|
||||
// os_log(.debug, log: log, "\"%@\" - updated %i UNREAD and %i read article(s).", collection.label, unreadArticleIds.count, markReadArticles.count)
|
||||
|
||||
didFinish()
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
//
|
||||
// FeedlySyncStrategy.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 19/9/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import SyncDatabase
|
||||
|
||||
final class FeedlySyncStrategy {
|
||||
|
||||
let account: Account
|
||||
let caller: FeedlyAPICaller
|
||||
let operationQueue: OperationQueue
|
||||
let database: SyncDatabase
|
||||
let log: OSLog
|
||||
|
||||
init(account: Account, caller: FeedlyAPICaller, database: SyncDatabase, log: OSLog) {
|
||||
self.account = account
|
||||
self.caller = caller
|
||||
self.operationQueue = OperationQueue()
|
||||
self.log = log
|
||||
self.database = database
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
os_log(.debug, log: log, "Cancelling all operations.")
|
||||
self.operationQueue.cancelAllOperations()
|
||||
}
|
||||
|
||||
private var startSyncCompletionHandler: ((Result<Void, Error>) -> ())?
|
||||
|
||||
private var newerThan: Date? {
|
||||
if let date = account.metadata.lastArticleFetch {
|
||||
return date
|
||||
} else {
|
||||
return Calendar.current.date(byAdding: .day, value: -31, to: Date())
|
||||
}
|
||||
}
|
||||
|
||||
/// The truth is in the cloud.
|
||||
func startSync(completionHandler: @escaping (Result<Void, Error>) -> ()) {
|
||||
guard operationQueue.operationCount == 0 else {
|
||||
os_log(.debug, log: log, "Reqeusted start sync but ignored because a sync is already in progress.")
|
||||
completionHandler(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
guard let credentials = caller.credentials else {
|
||||
completionHandler(.failure(FeedlyAccountDelegateError.notLoggedIn))
|
||||
return
|
||||
}
|
||||
|
||||
let sendArticleStatuses = FeedlySendArticleStatusesOperation(database: database, caller: caller, log: log)
|
||||
sendArticleStatuses.delegate = self
|
||||
|
||||
|
||||
// Since the truth is in the cloud, everything hinges of what Collections the user has.
|
||||
let getCollections = FeedlyGetCollectionsOperation(caller: caller, log: log)
|
||||
getCollections.delegate = self
|
||||
getCollections.addDependency(sendArticleStatuses)
|
||||
|
||||
// Ensure a folder exists for each Collection, removing Folders without a corresponding Collection.
|
||||
let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account,
|
||||
collectionsProvider: getCollections,
|
||||
caller: caller,
|
||||
log: log)
|
||||
mirrorCollectionsAsFolders.delegate = self
|
||||
mirrorCollectionsAsFolders.addDependency(getCollections)
|
||||
|
||||
// Ensure feeds are created and grouped by their folders.
|
||||
let createFeedsOperation = FeedlyCreateFeedsForCollectionFoldersOperation(account: account,
|
||||
feedsAndFoldersProvider: mirrorCollectionsAsFolders,
|
||||
log: log)
|
||||
createFeedsOperation.delegate = self
|
||||
createFeedsOperation.addDependency(mirrorCollectionsAsFolders)
|
||||
|
||||
|
||||
// Get the streams for each Collection. It will call back to enqueue more operations.
|
||||
let getCollectionStreams = FeedlyRequestStreamsOperation(account: account,
|
||||
collectionsProvider: getCollections,
|
||||
newerThan: newerThan,
|
||||
unreadOnly: false,
|
||||
caller: caller,
|
||||
log: log)
|
||||
getCollectionStreams.delegate = self
|
||||
getCollectionStreams.queueDelegate = self
|
||||
getCollectionStreams.addDependency(getCollections)
|
||||
|
||||
let syncStarred = FeedlySyncStarredArticlesOperation(account: account, credentials: credentials, caller: caller, log: log)
|
||||
syncStarred.addDependency(getCollections)
|
||||
syncStarred.addDependency(mirrorCollectionsAsFolders)
|
||||
syncStarred.addDependency(createFeedsOperation)
|
||||
|
||||
// Last operation to perform, which should be dependent on any other operation added to the queue.
|
||||
let syncId = UUID().uuidString
|
||||
let lastArticleFetchDate = Date()
|
||||
let completionOperation = BlockOperation { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
if let self = self {
|
||||
self.account.metadata.lastArticleFetch = lastArticleFetchDate
|
||||
os_log(.debug, log: self.log, "Sync completed: %@", syncId)
|
||||
self.startSyncCompletionHandler = nil
|
||||
}
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
completionOperation.addDependency(sendArticleStatuses)
|
||||
completionOperation.addDependency(getCollections)
|
||||
completionOperation.addDependency(mirrorCollectionsAsFolders)
|
||||
completionOperation.addDependency(createFeedsOperation)
|
||||
completionOperation.addDependency(getCollectionStreams)
|
||||
completionOperation.addDependency(syncStarred)
|
||||
|
||||
finalOperation = completionOperation
|
||||
startSyncCompletionHandler = completionHandler
|
||||
|
||||
let minimumOperations = [sendArticleStatuses,
|
||||
getCollections,
|
||||
mirrorCollectionsAsFolders,
|
||||
createFeedsOperation,
|
||||
getCollectionStreams,
|
||||
syncStarred,
|
||||
completionOperation]
|
||||
|
||||
operationQueue.addOperations(minimumOperations, waitUntilFinished: false)
|
||||
|
||||
os_log(.debug, log: log, "Sync started: %@", syncId)
|
||||
}
|
||||
|
||||
private weak var finalOperation: Operation?
|
||||
}
|
||||
|
||||
extension FeedlySyncStrategy: FeedlyRequestStreamsOperationDelegate {
|
||||
|
||||
func feedlyRequestStreamsOperation(_ operation: FeedlyRequestStreamsOperation, enqueue streamOperation: FeedlyGetStreamOperation) {
|
||||
|
||||
streamOperation.delegate = self
|
||||
|
||||
// os_log(.debug, log: log, "Requesting stream for collection \"%@\"", streamOperation.collection.label)
|
||||
|
||||
// Group the stream's content by feed.
|
||||
let groupItemsByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
|
||||
entryProvider: streamOperation,
|
||||
log: log)
|
||||
groupItemsByFeed.delegate = self
|
||||
groupItemsByFeed.addDependency(streamOperation)
|
||||
|
||||
// Update the account with the articles for the feeds in the stream.
|
||||
let updateOperation = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
|
||||
organisedItemsProvider: groupItemsByFeed,
|
||||
log: log)
|
||||
updateOperation.delegate = self
|
||||
updateOperation.addDependency(groupItemsByFeed)
|
||||
|
||||
// Once the articles are in the account, ensure they have the correct status
|
||||
let ensureUnreadOperation = FeedlyRefreshStreamEntriesStatusOperation(account: account,
|
||||
entryProvider: streamOperation,
|
||||
log: log)
|
||||
|
||||
ensureUnreadOperation.delegate = self
|
||||
ensureUnreadOperation.addDependency(updateOperation)
|
||||
|
||||
// Sync completes successfully when the account has been updated with all the parsedd entries from the stream.
|
||||
if let operation = finalOperation {
|
||||
operation.addDependency(ensureUnreadOperation)
|
||||
}
|
||||
|
||||
let operations = [streamOperation, groupItemsByFeed, updateOperation, ensureUnreadOperation]
|
||||
|
||||
operationQueue.addOperations(operations, waitUntilFinished: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlySyncStrategy: FeedlyOperationDelegate {
|
||||
|
||||
func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) {
|
||||
os_log(.debug, log: log, "%@ failed so sync failed with error %@", operation, error.localizedDescription)
|
||||
cancel()
|
||||
|
||||
startSyncCompletionHandler?(.failure(error))
|
||||
startSyncCompletionHandler = nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// FeedlyGetCollectionsService.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 21/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyGetCollectionsService: class {
|
||||
func getCollections(completionHandler: @escaping (Result<[FeedlyCollection], Error>) -> ())
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// FeedlyGetEntriesService.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 28/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyGetEntriesService: class {
|
||||
func getEntries(for ids: Set<String>, completionHandler: @escaping (Result<[FeedlyEntry], Error>) -> ())
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// FeedlyGetStreamContentsService.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 21/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyGetStreamContentsService: class {
|
||||
func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ())
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue