NetNewsWire/Frameworks/Account/AccountTests/Feedly/AccountFeedlySyncTest.swift

324 lines
11 KiB
Swift

//
// 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) {
// TestTransport blacklists certain query items to make mocking responses easier.
let endpoint = "https://sandbox7.feedly.com/v3"
let category = "\(endpoint)/streams/contents?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)
}
}
}