Merge pull request #1094 from kielgillard/master

Feedly unit tests and fixes
This commit is contained in:
Maurice Parker 2019-10-03 08:17:22 -05:00 committed by GitHub
commit 046ec7de51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 801 additions and 69 deletions

View File

@ -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()

View File

@ -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;
};

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()
}

View File

@ -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? {

View File

@ -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 {

View File

@ -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) {

View File

@ -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()

View File

@ -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 feeds 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 feeds 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

View 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)
}
}

View File

@ -13,4 +13,5 @@ struct FeedlyFeed: Codable {
var id: String
var title: String
var updated: Date?
var website: String?
}

View 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?
}

View 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
}
}

View 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?
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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)