NetNewsWire/Frameworks/Account/AccountTests/Feedly/FeedlyAddNewFeedOperationTe...

335 lines
11 KiB
Swift

//
// FeedlyAddNewFeedOperationTests.swift
// AccountTests
//
// Created by Kiel Gillard on 2/12/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import XCTest
@testable import Account
import RSWeb
class FeedlyAddNewFeedOperationTests: XCTestCase {
private var account: Account!
private let support = FeedlyTestSupport()
override func setUp() {
super.setUp()
account = support.makeTestAccount()
}
override func tearDown() {
if let account = account {
support.destroy(account)
}
super.tearDown()
}
private var transport = TestTransport()
lazy var caller: FeedlyAPICaller = {
let caller = FeedlyAPICaller(transport: transport, api: .sandbox)
caller.credentials = support.accessToken
return caller
}()
private func getFolderByLoadingInitialContent() -> Folder? {
let subdirectory = "feedly-add-new-feed"
let provider = InitialMockResponseProvider(findingMocksIn: subdirectory)
transport.mockResponseFileUrlProvider = provider
let getCollections = FeedlyGetCollectionsOperation(service: caller, log: support.log)
let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, log: support.log)
mirrorCollectionsAsFolders.addDependency(getCollections)
let createFolders = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: mirrorCollectionsAsFolders, log: support.log)
createFolders.addDependency(mirrorCollectionsAsFolders)
let completionExpectation = expectation(description: "Did Finish")
createFolders.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperations([getCollections, mirrorCollectionsAsFolders, createFolders], waitUntilFinished: false)
waitForExpectations(timeout: 2)
support.checkFoldersAndFeeds(in: account, againstCollectionsAndFeedsInJSONNamed: "emptyCollections", subdirectory: subdirectory)
guard let folder = account.folders?.first else {
XCTFail("Unable to load test folder to add a feed into.")
return nil
}
XCTAssertEqual(folder.topLevelWebFeeds.count, 0)
return folder
}
func expectationForCompletion(of progress: DownloadProgress) -> XCTestExpectation {
return expectation(forNotification: .DownloadProgressDidChange, object: progress) { notification -> Bool in
guard let progress = notification.object as? DownloadProgress else {
return false
}
// We want to assert the progress completes.
if progress.isComplete {
return true
}
return false
}
}
let searchUrl = "https://macrumors.com"
func testCancel() {
guard let folder = getFolderByLoadingInitialContent() else {
return
}
let progress = DownloadProgress(numberOfTasks: 0)
let _ = expectationForCompletion(of: progress)
let addNewFeed = try! FeedlyAddNewFeedOperation(account: account,
credentials: support.accessToken,
url: searchUrl,
feedName: nil,
searchService: caller,
addToCollectionService: caller,
syncUnreadIdsService: caller,
getStreamContentsService: caller,
container: folder,
progress: progress,
log: support.log)
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
let completionExpectation = expectation(description: "Did Finish")
addNewFeed.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(addNewFeed)
XCTAssert(progress.numberRemaining > 0)
addNewFeed.cancel()
waitForExpectations(timeout: 2)
XCTAssert(progress.isComplete)
}
func testAddNewFeedSuccess() throws {
guard let folder = getFolderByLoadingInitialContent() else {
return
}
let progress = DownloadProgress(numberOfTasks: 0)
let _ = expectationForCompletion(of: progress)
let subdirectory = "feedly-add-new-feed"
let searchUrl = self.searchUrl
let provider = MockResponseProvider(findingMocksIn: subdirectory)
provider.searchQueryHandler = { query in
XCTAssertEqual(query, searchUrl)
}
transport.mockResponseFileUrlProvider = provider
let addNewFeed = try! FeedlyAddNewFeedOperation(account: account,
credentials: support.accessToken,
url: searchUrl,
feedName: nil,
searchService: caller,
addToCollectionService: caller,
syncUnreadIdsService: caller,
getStreamContentsService: caller,
container: folder,
progress: progress,
log: support.log)
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
let completionExpectation = expectation(description: "Did Finish")
addNewFeed.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(addNewFeed)
XCTAssert(progress.numberRemaining > 0)
waitForExpectations(timeout: 2)
XCTAssert(progress.isComplete)
try support.checkArticles(in: account, againstItemsInStreamInJSONNamed: "feedStream", subdirectory: subdirectory)
support.checkUnreadStatuses(in: account, againstIdsInStreamInJSONNamed: "unreadIds", subdirectory: subdirectory, testCase: self)
}
class TestFeedlyAddFeedToCollectionService: FeedlyAddFeedToCollectionService {
var mockResult: Result<[FeedlyFeed], Error>?
var addFeedExpectation: XCTestExpectation?
var parameterTester: ((FeedlyFeedResourceId, String?, String) -> ())?
func addFeed(with feedId: FeedlyFeedResourceId, title: String?, toCollectionWith collectionId: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ()) {
guard let result = mockResult else {
XCTFail("Missing mock result. Test may time out because the completion will not be called.")
return
}
parameterTester?(feedId, title, collectionId)
DispatchQueue.main.async {
completion(result)
self.addFeedExpectation?.fulfill()
}
}
}
func testAddNewFeedFailure() {
guard let folder = getFolderByLoadingInitialContent() else {
return
}
let progress = DownloadProgress(numberOfTasks: 0)
let _ = expectationForCompletion(of: progress)
let subdirectory = "feedly-add-new-feed"
let searchUrl = self.searchUrl
let feedName = "MacRumours with a \"u\" because I am Australian"
let provider = MockResponseProvider(findingMocksIn: subdirectory)
provider.searchQueryHandler = { query in
XCTAssertEqual(query, searchUrl)
}
transport.mockResponseFileUrlProvider = provider
let service = TestFeedlyAddFeedToCollectionService()
service.mockResult = .failure(URLError(.timedOut))
service.addFeedExpectation = expectation(description: "Add New Feed Called")
service.parameterTester = { feedResource, title, collectionId in
XCTAssertEqual(feedResource.id, "feed/http://feeds.macrumors.com/MacRumors-All")
XCTAssertEqual(title, feedName)
XCTAssertEqual(collectionId, folder.externalID)
}
let addNewFeed = try! FeedlyAddNewFeedOperation(account: account,
credentials: support.accessToken,
url: searchUrl,
feedName: feedName,
searchService: caller,
addToCollectionService: service,
syncUnreadIdsService: caller,
getStreamContentsService: caller,
container: folder,
progress: progress,
log: support.log)
// If this expectation is not fulfilled, the operation is not calling `didFinish`.
let completionExpectation = expectation(description: "Did Finish")
addNewFeed.completionBlock = {
completionExpectation.fulfill()
}
OperationQueue.main.addOperation(addNewFeed)
XCTAssert(progress.numberRemaining > 0)
waitForExpectations(timeout: 2)
XCTAssert(progress.isComplete)
XCTAssertEqual(folder.topLevelWebFeeds.count, 0)
}
}
private class InitialMockResponseProvider: TestTransportMockResponseProviding {
let subdirectory: String
init(findingMocksIn subdirectory: String) {
self.subdirectory = subdirectory
}
func mockResponseFileUrl(for components: URLComponents) -> URL? {
let bundle = Bundle(for: type(of: self))
// When we get a request for the initial collections content, use these results.
if components.path.contains("/v3/collections") {
return bundle.url(forResource: "emptyCollections", withExtension: "json", subdirectory: subdirectory)
}
return nil
}
}
private class MockResponseProvider: TestTransportMockResponseProviding {
let subdirectory: String
init(findingMocksIn subdirectory: String) {
self.subdirectory = subdirectory
}
var searchQueryHandler: ((String) -> ())?
func mockResponseFileUrl(for components: URLComponents) -> URL? {
let bundle = Bundle(for: type(of: self))
let queryItems = components.queryItems ?? []
let query = queryItems.first(where: { $0.name.contains("query") })?.value
// When we get the search request, use these results.
if components.path.contains("search/feeds") {
if let query = query {
searchQueryHandler?(query)
} else {
XCTFail("`query` missing from URL query items in search request: \(components)")
}
return bundle.url(forResource: "searchResults", withExtension: "json", subdirectory: subdirectory)
}
// When we get a request to add a feed, use these results.
if components.path.contains("/v3/collections") && components.path.contains("/feeds") {
return bundle.url(forResource: "putFeed", withExtension: "json", subdirectory: subdirectory)
}
// When we get a request for the initial collections content, use these results.
if components.path.contains("/v3/collections") {
return bundle.url(forResource: "collections", withExtension: "json", subdirectory: subdirectory)
}
let continuation = queryItems.first(where: { $0.name.contains("continuation") })?.value
// When we get a request for unread article ids, use these results.
if components.path.contains("streams/ids") {
// if there is a continuation, return the page for it
if let continuation = continuation, let data = continuation.data(using: .utf8) {
let base64 = data.base64EncodedString() // at least base64 can be used as a path component.
return bundle.url(forResource: "unreadIds@\(base64)", withExtension: "json", subdirectory: subdirectory)
} else {
// return first page
return bundle.url(forResource: "unreadIds", withExtension: "json", subdirectory: subdirectory)
}
}
// When we get a request for the contents of the feed stream, use these results.
if components.path.contains("streams/contents") {
// if there is a continuation, return the page for it
if let continuation = continuation, let data = continuation.data(using: .utf8) {
let base64 = data.base64EncodedString() // at least base64 can be used as a path component.
return bundle.url(forResource: "feedStream@\(base64)", withExtension: "json", subdirectory: subdirectory)
} else {
// return first page
return bundle.url(forResource: "feedStream", withExtension: "json", subdirectory: subdirectory)
}
}
return nil
}
}