mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-01-29 02:09:30 +01:00
Merge pull request #1094 from kielgillard/master
Feedly unit tests and fixes
This commit is contained in:
commit
046ec7de51
@ -679,6 +679,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func removeFeeds(_ feeds: Set<Feed>) {
|
||||
guard !feeds.isEmpty else {
|
||||
return
|
||||
}
|
||||
topLevelFeeds.subtract(feeds)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func addFeed(_ feed: Feed) {
|
||||
topLevelFeeds.insert(feed)
|
||||
structureDidChange()
|
||||
|
@ -75,6 +75,11 @@
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */; };
|
||||
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F73CF0202788D80000BCEF /* ArticleFetcher.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 */; };
|
||||
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */; };
|
||||
9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */; };
|
||||
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */; };
|
||||
9E1D154D233370D800F4944C /* FeedlySyncStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154C233370D800F4944C /* FeedlySyncStrategy.swift */; };
|
||||
9E1D154F233371DD00F4944C /* FeedlyGetCollectionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D154E233371DD00F4944C /* FeedlyGetCollectionsOperation.swift */; };
|
||||
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D15502334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift */; };
|
||||
@ -85,6 +90,17 @@
|
||||
9E1D155B2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155A2334423300F4944C /* FeedlyOrganiseParsedItemsByFeedOperation.swift */; };
|
||||
9E1D155D233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1D155C233447F000F4944C /* FeedlyUpdateAccountFeedsWithItemsOperation.swift */; };
|
||||
9E713653233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.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 */; };
|
||||
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 */; };
|
||||
@ -215,6 +231,11 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyTag.swift; sourceTree = "<group>"; };
|
||||
9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceId.swift; sourceTree = "<group>"; };
|
||||
9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyResourceIdTests.swift; sourceTree = "<group>"; };
|
||||
9E1D154C233370D800F4944C /* FeedlySyncStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlySyncStrategy.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>"; };
|
||||
@ -225,6 +246,17 @@
|
||||
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>"; };
|
||||
9E713652233AD63E00765C84 /* FeedlyRefreshStreamEntriesStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedlyRefreshStreamEntriesStatusOperation.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>"; };
|
||||
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>"; };
|
||||
@ -432,12 +464,56 @@
|
||||
5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */,
|
||||
5107A09C227DE77700C7C3C5 /* TestTransport.swift */,
|
||||
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */,
|
||||
9E7F15082341E97100F860D1 /* Feedly */,
|
||||
51D58756227F62E300900287 /* JSON */,
|
||||
848935061F62485000CEBD24 /* Info.plist */,
|
||||
);
|
||||
path = AccountTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E7F15082341E97100F860D1 /* Feedly */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E7F15062341E96700F860D1 /* AccountFeedlySyncTest.swift */,
|
||||
9E1773DA234593CF0056A5A8 /* FeedlyResourceIdTests.swift */,
|
||||
9E7F150B2341F2A700F860D1 /* Initial */,
|
||||
9E832B1A234344DA00D83249 /* AddCollection */,
|
||||
9E832B21234416B400D83249 /* AddFeed */,
|
||||
);
|
||||
path = Feedly;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E7F150B2341F2A700F860D1 /* Initial */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
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 = (
|
||||
@ -473,12 +549,16 @@
|
||||
9EBC31B32338AC2E002A567B /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E1773D823458D590056A5A8 /* FeedlyResourceId.swift */,
|
||||
9EAEC60B2332FE830085D7C9 /* FeedlyCollection.swift */,
|
||||
9EAEC60D2332FEC20085D7C9 /* FeedlyFeed.swift */,
|
||||
9EAEC623233315F60085D7C9 /* FeedlyEntry.swift */,
|
||||
9E1773D4234570E30056A5A8 /* FeedlyEntryParser.swift */,
|
||||
9EAEC625233318400085D7C9 /* FeedlyStream.swift */,
|
||||
9EAEC62723331C350085D7C9 /* FeedlyCategory.swift */,
|
||||
9EAEC62923331EE70085D7C9 /* FeedlyOrigin.swift */,
|
||||
9E1773D22345700E0056A5A8 /* FeedlyLink.swift */,
|
||||
9E1773D6234575AB0056A5A8 /* FeedlyTag.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@ -647,10 +727,20 @@
|
||||
5133230E2281089500C30F19 /* icons.json in Resources */,
|
||||
51D5875B227F630B00900287 /* tags_add.json 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 */,
|
||||
51D5875A227F630B00900287 /* tags_delete.json in Resources */,
|
||||
5165D71722821C2400D9D53D /* taggings_add.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 */,
|
||||
5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -691,6 +781,7 @@
|
||||
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 */,
|
||||
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */,
|
||||
84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */,
|
||||
@ -714,8 +805,10 @@
|
||||
552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */,
|
||||
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */,
|
||||
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */,
|
||||
9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */,
|
||||
552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */,
|
||||
9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */,
|
||||
9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */,
|
||||
9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */,
|
||||
84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */,
|
||||
84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */,
|
||||
@ -725,6 +818,7 @@
|
||||
846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */,
|
||||
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
|
||||
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,
|
||||
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */,
|
||||
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */,
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
|
||||
9EBC31B7233987C1002A567B /* FeedlyArticleStatusCoordinator.swift in Sources */,
|
||||
@ -736,12 +830,14 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9E7F15072341E96700F860D1 /* AccountFeedlySyncTest.swift in Sources */,
|
||||
5165D7122282080C00D9D53D /* AccountFolderContentsSyncTest.swift in Sources */,
|
||||
51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */,
|
||||
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */,
|
||||
513323082281070D00C30F19 /* AccountFeedSyncTest.swift in Sources */,
|
||||
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */,
|
||||
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */,
|
||||
9E1773DB234593CF0056A5A8 /* FeedlyResourceIdTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1100"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "848934F51F62484F00CEBD24"
|
||||
BuildableName = "Account.framework"
|
||||
BlueprintName = "Account"
|
||||
ReferencedContainer = "container:Account.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "848934FE1F62484F00CEBD24"
|
||||
BuildableName = "AccountTests.xctest"
|
||||
BlueprintName = "AccountTests"
|
||||
ReferencedContainer = "container:Account.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "848934F51F62484F00CEBD24"
|
||||
BuildableName = "Account.framework"
|
||||
BlueprintName = "Account"
|
||||
ReferencedContainer = "container:Account.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -0,0 +1,322 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
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) {
|
||||
let endpoint = "https://sandbox7.feedly.com/v3"
|
||||
let category = "\(endpoint)/streams/contents?unreadOnly=false&count=500&streamId=user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category"
|
||||
|
||||
switch testFiles {
|
||||
case .initial:
|
||||
let dict = [
|
||||
"\(endpoint)/collections": "feedly_collections_initial.json",
|
||||
"\(category)/5ca4d61d-e55d-4999-a8d1-c3b9d8789815": "macintosh_initial.json",
|
||||
"\(category)/global.must": "mustread_initial.json",
|
||||
"\(category)/885f2e01-d314-4e63-abac-17dcb063f5b5": "programming_initial.json",
|
||||
"\(category)/66132046-6f14-488d-b590-8e93422723c8": "uncategorized_initial.json",
|
||||
"\(category)/e31b3fcb-27f6-4f3e-b96c-53902586e366": "weblogs_initial.json",
|
||||
]
|
||||
transport.testFiles = dict
|
||||
|
||||
case .addCollection:
|
||||
set(testFiles: .initial, with: transport)
|
||||
|
||||
var dict = transport.testFiles
|
||||
dict["\(endpoint)/collections"] = "feedly_collections_addcollection.json"
|
||||
dict["\(category)/fc09f383-5a9a-4daa-a575-3efc1733b173"] = "newcollection_addcollection.json"
|
||||
transport.testFiles = dict
|
||||
|
||||
case .addFeed:
|
||||
set(testFiles: .addCollection, with: transport)
|
||||
|
||||
var dict = transport.testFiles
|
||||
dict["\(endpoint)/collections"] = "feedly_collections_addfeed.json"
|
||||
dict["\(category)/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,27 @@
|
||||
//
|
||||
// FeedlyResourceIdTests.swift
|
||||
// AccountTests
|
||||
//
|
||||
// Created by Kiel Gillard on 3/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Account
|
||||
|
||||
class FeedlyResourceIdTests: XCTestCase {
|
||||
|
||||
func testFeedResourceId() {
|
||||
let expectedUrl = "http://ranchero.com/blog/atom.xml"
|
||||
|
||||
let feedResource = FeedlyFeedResourceId(id: "feed/\(expectedUrl)")
|
||||
let urlResource = FeedlyFeedResourceId(id: expectedUrl)
|
||||
let otherResource = FeedlyFeedResourceId(id: "whiskey/\(expectedUrl)")
|
||||
let invalidResource = FeedlyFeedResourceId(id: "")
|
||||
|
||||
XCTAssertEqual(feedResource.url, expectedUrl)
|
||||
XCTAssertEqual(urlResource.url, expectedUrl)
|
||||
XCTAssertEqual(otherResource.url, otherResource.id)
|
||||
XCTAssertEqual(invalidResource.url, invalidResource.id)
|
||||
}
|
||||
}
|
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 @@
|
||||
{"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
@ -23,16 +23,16 @@ class TestAccountManager {
|
||||
func createAccount(type: AccountType, username: String? = nil, password: String? = nil, transport: Transport) -> Account {
|
||||
|
||||
let accountID = UUID().uuidString
|
||||
let accountFolder = accountsFolder.appendingPathComponent("\(type.rawValue)_\(accountID)").absoluteString
|
||||
let accountFolder = accountsFolder.appendingPathComponent("\(type.rawValue)_\(accountID)")
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(atPath: accountFolder, withIntermediateDirectories: true, attributes: nil)
|
||||
try FileManager.default.createDirectory(at: accountFolder, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
assertionFailure("Could not create folder for \(accountID) account.")
|
||||
abort()
|
||||
}
|
||||
|
||||
let account = Account(dataFolder: accountFolder, type: type, accountID: accountID, transport: transport)!
|
||||
let account = Account(dataFolder: accountFolder.absoluteString, type: type, accountID: accountID, transport: transport)!
|
||||
|
||||
return account
|
||||
|
||||
@ -43,8 +43,11 @@ class TestAccountManager {
|
||||
do {
|
||||
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 create folder for OnMyMac account.")
|
||||
assertionFailure("Could not delete folder at: \(account.dataFolder) because \(error)")
|
||||
abort()
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,11 @@ extension Feed {
|
||||
public extension Article {
|
||||
|
||||
var account: Account? {
|
||||
return AccountManager.shared.existingAccount(with: accountID)
|
||||
// The force unwrapped shared instance was crashing Account.framework unit tests.
|
||||
guard let manager = AccountManager.shared else {
|
||||
return nil
|
||||
}
|
||||
return manager.existingAccount(with: accountID)
|
||||
}
|
||||
|
||||
var feed: Feed? {
|
||||
|
@ -97,9 +97,11 @@ final class FeedlyAPICaller {
|
||||
}
|
||||
var components = baseUrlComponents
|
||||
components.path = "/v3/streams/contents"
|
||||
// If you change these, check AccountFeedlySyncTest.set(testFiles:with:).
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "unreadOnly", value: unreadOnly ? "true" : "false"),
|
||||
URLQueryItem(name: "count", value: "500"),
|
||||
URLQueryItem(name: "streamId", value: collection.id),
|
||||
URLQueryItem(name: "unreadOnly", value: unreadOnly ? "true" : "false")
|
||||
]
|
||||
|
||||
guard let url = components.url else {
|
||||
|
@ -31,7 +31,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
didSet {
|
||||
// https://developer.feedly.com/v3/developer/
|
||||
if let devToken = ProcessInfo.processInfo.environment["FEEDLY_DEV_ACCESS_TOKEN"], !devToken.isEmpty {
|
||||
caller.credentials = Credentials(type: .oauthAccessToken, username: "", secret: devToken)
|
||||
caller.credentials = Credentials(type: .oauthAccessToken, username: "Developer", secret: devToken)
|
||||
} else {
|
||||
caller.credentials = credentials
|
||||
}
|
||||
@ -88,6 +88,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
os_log(.debug, log: log, "Sync took %.3f seconds", -date.timeIntervalSinceNow)
|
||||
DispatchQueue.main.async {
|
||||
progress.completeTask()
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,11 +242,6 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
caller: caller,
|
||||
articleStatusCoordinator: articleStatusCoodinator,
|
||||
log: log)
|
||||
|
||||
//TODO: Figure out how other accounts get refreshed automatically.
|
||||
refreshAll(for: account) { result in
|
||||
print("sync after initialise did complete")
|
||||
}
|
||||
}
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
|
@ -43,25 +43,29 @@ final class FeedlyArticleStatusCoordinator {
|
||||
/// Ensures local articles have the same status as they do remotely.
|
||||
func refreshArticleStatus(for account: Account, stream: FeedlyStream, collection: FeedlyCollection, completion: @escaping (() -> Void)) {
|
||||
|
||||
guard let folder = account.existingFolder(with: collection.label) else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
let unreadArticleIds = Set(
|
||||
stream.items
|
||||
.filter { $0.unread }
|
||||
.map { $0.id }
|
||||
)
|
||||
let localArticles = folder.fetchArticles()
|
||||
let localArticleIds = localArticles.articleIDs()
|
||||
let readArticleIds = localArticleIds.subtracting(unreadArticleIds)
|
||||
account.update(localArticles.filter { readArticleIds.contains($0.articleID) }, statusKey: .read, flag: true)
|
||||
// account.ensureStatuses(readArticleIds, true, .read, true)
|
||||
account.update(localArticles.filter { unreadArticleIds.contains($0.articleID) }, statusKey: .read, flag: false)
|
||||
// account.ensureStatuses(unreadArticleIds, false, .read, false)
|
||||
|
||||
os_log(.debug, log: log, "Ensured %i UNREAD and %i read article(s) in \"%@\".", unreadArticleIds.count, readArticleIds.count, collection.label)
|
||||
// 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(
|
||||
stream.items
|
||||
.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)
|
||||
|
||||
completion()
|
||||
|
||||
|
@ -48,9 +48,13 @@ struct FeedlyEntry: Decodable {
|
||||
|
||||
/// the feed from which this article was crawled. If present, “streamId” will contain the feed id, “title” will contain the feed title, and “htmlUrl” will contain the feed’s website.
|
||||
var origin: FeedlyOrigin?
|
||||
//
|
||||
// /// a list of alternate links for this article. Each link object contains a media type and a URL. Typically, a single object is present, with a link to the original web page.
|
||||
// var alternate: [Link]?
|
||||
|
||||
/// Used to help find the URL to visit an article on a web site.
|
||||
/// See https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
|
||||
var canonical: [FeedlyLink]?
|
||||
|
||||
/// a list of alternate links for this article. Each link object contains a media type and a URL. Typically, a single object is present, with a link to the original web page.
|
||||
var alternate: [FeedlyLink]?
|
||||
//
|
||||
// // var origin:
|
||||
// // Optional origin object the feed from which this article was crawled. If present, “streamId” will contain the feed id, “title” will contain the feed title, and “htmlUrl” will contain the feed’s website.
|
||||
@ -62,8 +66,8 @@ struct FeedlyEntry: Decodable {
|
||||
/// Was this entry read by the user? If an Authorization header is not provided, this will always return false. If an Authorization header is provided, it will reflect if the user has read this entry or not.
|
||||
var unread: Bool
|
||||
//
|
||||
// /// a list of tag objects (“id” and “label”) that the user added to this entry. This value is only returned if an Authorization header is provided, and at least one tag has been added. If the entry has been explicitly marked as read (not the feed itself), the “global.read” tag will be present.
|
||||
// var tags: [Tag]?
|
||||
/// a list of tag objects (“id” and “label”) that the user added to this entry. This value is only returned if an Authorization header is provided, and at least one tag has been added. If the entry has been explicitly marked as read (not the feed itself), the “global.read” tag will be present.
|
||||
var tags: [FeedlyTag]?
|
||||
//
|
||||
/// a list of category objects (“id” and “label”) that the user associated with the feed of this entry. This value is only returned if an Authorization header is provided.
|
||||
var categories: [FeedlyCategory]?
|
||||
@ -74,8 +78,8 @@ struct FeedlyEntry: Decodable {
|
||||
// /// Timestamp for tagged articles, contains the timestamp when the article was tagged by the user. This will only be returned when the entry is returned through the streams API.
|
||||
// var actionTimestamp: Date?
|
||||
//
|
||||
// /// A list of media links (videos, images, sound etc) provided by the feed. Some entries do not have a summary or content, only a collection of media links.
|
||||
// var enclosure: [Link]?
|
||||
/// A list of media links (videos, images, sound etc) provided by the feed. Some entries do not have a summary or content, only a collection of media links.
|
||||
var enclosure: [FeedlyLink]?
|
||||
//
|
||||
// /// The article fingerprint. This value might change if the article is updated.
|
||||
// var fingerprint: String
|
||||
|
103
Frameworks/Account/Feedly/Models/FeedlyEntryParser.swift
Normal file
103
Frameworks/Account/Feedly/Models/FeedlyEntryParser.swift
Normal file
@ -0,0 +1,103 @@
|
||||
//
|
||||
// FeedlyEntryParser.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 3/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Articles
|
||||
import RSParser
|
||||
|
||||
struct FeedlyEntryParser {
|
||||
var entry: FeedlyEntry
|
||||
|
||||
var id: String {
|
||||
return entry.id
|
||||
}
|
||||
|
||||
var feedUrl: String {
|
||||
guard let id = entry.origin?.streamId else {
|
||||
assertionFailure()
|
||||
return ""
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
/// 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? {
|
||||
let multidimensionalArrayOfLinks = [entry.canonical, entry.alternate]
|
||||
let withExistingValues = multidimensionalArrayOfLinks.compactMap { $0 }
|
||||
let flattened = withExistingValues.flatMap { $0 }
|
||||
let webPageLinks = flattened.filter { $0.type == nil || $0.type == "text/html" }
|
||||
return webPageLinks.first?.href
|
||||
}
|
||||
|
||||
var title: String? {
|
||||
return entry.title
|
||||
}
|
||||
|
||||
var contentHMTL: String? {
|
||||
return entry.content?.content ?? entry.summary?.content
|
||||
}
|
||||
|
||||
var contentText: String? {
|
||||
// We could strip HTML from contentHTML?
|
||||
return nil
|
||||
}
|
||||
|
||||
var summary: String? {
|
||||
return entry.summary?.content
|
||||
}
|
||||
|
||||
var datePublished: Date {
|
||||
return entry.published
|
||||
}
|
||||
|
||||
var dateModified: Date? {
|
||||
return entry.updated
|
||||
}
|
||||
|
||||
var authors: Set<ParsedAuthor>? {
|
||||
guard let name = entry.author else {
|
||||
return nil
|
||||
}
|
||||
return Set([ParsedAuthor(name: name, url: nil, avatarURL: nil, emailAddress: nil)])
|
||||
}
|
||||
|
||||
var tags: Set<String>? {
|
||||
guard let labels = entry.tags?.compactMap({ $0.label }), !labels.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return Set(labels)
|
||||
}
|
||||
|
||||
var attachments: Set<ParsedAttachment>? {
|
||||
guard let enclosure = entry.enclosure, !enclosure.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
let attachments = enclosure.compactMap { ParsedAttachment(url: $0.href, mimeType: $0.type, title: nil, sizeInBytes: nil, durationInSeconds: nil) }
|
||||
return attachments.isEmpty ? nil : Set(attachments)
|
||||
}
|
||||
|
||||
var parsedItemRepresentation: ParsedItem {
|
||||
return ParsedItem(syncServiceID: id,
|
||||
uniqueID: id,
|
||||
feedURL: feedUrl,
|
||||
url: nil,
|
||||
externalURL: externalUrl,
|
||||
title: title,
|
||||
contentHTML: contentHMTL,
|
||||
contentText: contentText,
|
||||
summary: summary,
|
||||
imageURL: nil,
|
||||
bannerImageURL: nil,
|
||||
datePublished: datePublished,
|
||||
dateModified: dateModified,
|
||||
authors: authors,
|
||||
tags: tags,
|
||||
attachments: attachments)
|
||||
}
|
||||
}
|
@ -13,4 +13,5 @@ struct FeedlyFeed: Codable {
|
||||
var id: String
|
||||
var title: String
|
||||
var updated: Date?
|
||||
var website: String?
|
||||
}
|
||||
|
18
Frameworks/Account/Feedly/Models/FeedlyLink.swift
Normal file
18
Frameworks/Account/Feedly/Models/FeedlyLink.swift
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// FeedlyLink.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 3/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyLink: Decodable {
|
||||
var href: String
|
||||
|
||||
/// The mime type of the resource located by `href`.
|
||||
/// When `nil`, it's probably a web page?
|
||||
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
|
||||
var type: String?
|
||||
}
|
37
Frameworks/Account/Feedly/Models/FeedlyResourceId.swift
Normal file
37
Frameworks/Account/Feedly/Models/FeedlyResourceId.swift
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// FeedlyResourceId.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 3/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The kinds of Resource Ids is documented here: https://developer.feedly.com/cloud/
|
||||
protocol FeedlyResourceId {
|
||||
|
||||
/// The resource Id from Feedly.
|
||||
var id: String { get }
|
||||
|
||||
/// The location of the kind of resource a concrete type represents.
|
||||
/// If the conrete type cannot strip the resource type from the Id, it should just return the Id
|
||||
/// since the Id is a legitimate URL.
|
||||
var url: String { get }
|
||||
}
|
||||
|
||||
/// The Feed Resource is documented here: https://developer.feedly.com/cloud/
|
||||
struct FeedlyFeedResourceId: FeedlyResourceId {
|
||||
var id: String
|
||||
|
||||
var url: String {
|
||||
if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex {
|
||||
var mutant = id
|
||||
mutant.removeSubrange(range)
|
||||
return mutant
|
||||
}
|
||||
|
||||
// It seems values like "something/https://my.blog/posts.xml" is a legit URL.
|
||||
return id
|
||||
}
|
||||
}
|
14
Frameworks/Account/Feedly/Models/FeedlyTag.swift
Normal file
14
Frameworks/Account/Feedly/Models/FeedlyTag.swift
Normal file
@ -0,0 +1,14 @@
|
||||
//
|
||||
// FeedlyTag.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 3/10/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyTag: Decodable {
|
||||
var id: String
|
||||
var label: String?
|
||||
}
|
@ -31,6 +31,19 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||
let feedsBefore = localFeeds
|
||||
let pairs = collectionsAndFoldersProvider.collectionsAndFolders
|
||||
|
||||
// Remove feeds in a folder which are not in the corresponding collection.
|
||||
for (collection, folder) in pairs {
|
||||
let feedsInFolder = folder.topLevelFeeds
|
||||
let feedsInCollection = Set(collection.feeds.map { $0.id })
|
||||
let feedsToRemove = feedsInFolder.filter { !feedsInCollection.contains($0.feedID) }
|
||||
if !feedsToRemove.isEmpty {
|
||||
folder.removeFeeds(feedsToRemove)
|
||||
os_log(.debug, log: log, "\"%@\" - removed: %@", collection.label, feedsToRemove.map { $0.feedID }, feedsInCollection)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Pair each Feed with its Folder.
|
||||
let feedsAndFolders = pairs
|
||||
.compactMap { ($0.0.feeds, $0.1) }
|
||||
.map({ (collectionFeeds, folder) -> [(FeedlyFeed, Folder)] in
|
||||
@ -43,18 +56,15 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||
|
||||
// find an existing feed
|
||||
for feed in localFeeds {
|
||||
if feed.feedID == collectionFeed.feedId {
|
||||
if feed.feedID == collectionFeed.id {
|
||||
return (feed, folder)
|
||||
}
|
||||
}
|
||||
|
||||
// no exsiting feed, create a new one
|
||||
let url = collectionFeed.id
|
||||
let metadata = FeedMetadata(feedID: url)
|
||||
// TODO: More metadata
|
||||
|
||||
let feed = Feed(account: account, url: url, metadata: metadata)
|
||||
feed.name = collectionFeed.title
|
||||
let id = collectionFeed.id
|
||||
let url = FeedlyFeedResourceId(id: id).url
|
||||
let feed = account.createFeed(with: collectionFeed.title, url: url, feedID: id, homePageURL: collectionFeed.website)
|
||||
|
||||
// So the same feed isn't created more than once.
|
||||
localFeeds.insert(feed)
|
||||
@ -69,11 +79,10 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove feeds without folders/collections.
|
||||
let feedsAfter = Set(feedsAndFolders.map { $0.0 })
|
||||
let feedsWithoutCollections = feedsBefore.subtracting(feedsAfter)
|
||||
for unmatched in feedsWithoutCollections {
|
||||
account.removeFeed(unmatched)
|
||||
}
|
||||
account.removeFeeds(feedsWithoutCollections)
|
||||
|
||||
if !feedsWithoutCollections.isEmpty {
|
||||
os_log(.debug, log: log, "Removed %i feeds", feedsWithoutCollections.count)
|
||||
|
@ -45,32 +45,7 @@ final class FeedlyGetStreamParsedItemsOperation: FeedlyOperation, FeedlyStreamPa
|
||||
|
||||
guard !isCancelled else { return }
|
||||
|
||||
parsedItems = stream.items.compactMap { entry -> ParsedItem? in
|
||||
guard let origin = entry.origin else {
|
||||
// Assertion might be too heavy handed here as our understanding of the data quality from Feedly grows.
|
||||
print("Entry has no origin and no way for us to figure out which feed it should belong to: \(entry)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Sensible values here.
|
||||
let parsed = ParsedItem(syncServiceID: entry.id,
|
||||
uniqueID: entry.id,
|
||||
feedURL: origin.streamId,
|
||||
url: nil,
|
||||
externalURL: origin.htmlUrl,
|
||||
title: entry.title,
|
||||
contentHTML: entry.content?.content,
|
||||
contentText: nil, // Seems there is no corresponding field in the JSON, so we might have to derive a value.
|
||||
summary: nil,
|
||||
imageURL: nil,
|
||||
bannerImageURL: nil,
|
||||
datePublished: entry.published,
|
||||
dateModified: entry.updated,
|
||||
authors: nil,
|
||||
tags: nil,
|
||||
attachments: nil)
|
||||
return parsed
|
||||
}
|
||||
parsedItems = stream.items.map { FeedlyEntryParser(entry: $0).parsedItemRepresentation }
|
||||
|
||||
os_log(.debug, log: log, "Parsed %i items of %i entries for %@", parsedItems.count, stream.items.count, collection.label)
|
||||
}
|
||||
|
@ -98,10 +98,26 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func addFeeds(_ feeds: Set<Feed>) {
|
||||
guard !feeds.isEmpty else {
|
||||
return
|
||||
}
|
||||
topLevelFeeds.formUnion(feeds)
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func removeFeed(_ feed: Feed) {
|
||||
topLevelFeeds.remove(feed)
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
public func removeFeeds(_ feeds: Set<Feed>) {
|
||||
guard !feeds.isEmpty else {
|
||||
return
|
||||
}
|
||||
topLevelFeeds.subtract(feeds)
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
|
@ -84,12 +84,15 @@ class AccountsFeedlyWebWindowController: NSWindowController, WKNavigationDelegat
|
||||
// TODO: Find an already existing account for this username?
|
||||
let account = AccountManager.shared.createAccount(type: .feedly)
|
||||
do {
|
||||
try account.storeCredentials(grant.accessToken)
|
||||
|
||||
// Store the refresh token first because it sends this token to the account delegate.
|
||||
if let token = grant.refreshToken {
|
||||
try account.storeCredentials(token)
|
||||
}
|
||||
|
||||
// Now store the access token because we want the account delegate to use it.
|
||||
try account.storeCredentials(grant.accessToken)
|
||||
|
||||
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
} catch {
|
||||
NSApplication.shared.presentError(error)
|
||||
|
Loading…
x
Reference in New Issue
Block a user