This commit is contained in:
Kiel Gillard 2019-10-03 21:23:49 +10:00
parent b3a5929d6d
commit 39d3999a0d
7 changed files with 75 additions and 21 deletions

View File

@ -281,8 +281,9 @@ class AccountFeedlySyncTest: XCTestCase {
} }
func set(testFiles: TestFiles, with transport: TestTransport) { 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 endpoint = "https://sandbox7.feedly.com/v3"
let category = "\(endpoint)/streams/contents?unreadOnly=false&count=500&streamId=user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category" let category = "\(endpoint)/streams/contents?streamId=user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category"
switch testFiles { switch testFiles {
case .initial: case .initial:

View File

@ -18,6 +18,13 @@ final class TestTransport: Transport {
var testFiles = [String: String]() var testFiles = [String: String]()
var testStatusCodes = [String: Int]() var testStatusCodes = [String: Int]()
/// Allows tests to filter time sensitive state out to make matching against test data easier.
var blacklistedQueryItemNames = Set([
"newerThan", // Feedly: Mock data has a fixed date.
"unreadOnly", // Feedly: Mock data is read/unread by test expectation.
"count", // Feedly: Mock data is limited by test expectation.
])
private func httpResponse(for request: URLRequest, statusCode: Int = 200) -> HTTPURLResponse { private func httpResponse(for request: URLRequest, statusCode: Int = 200) -> HTTPURLResponse {
guard let url = request.url else { guard let url = request.url else {
fatalError("Attempting to mock a http response for a request without a URL \(request).") fatalError("Attempting to mock a http response for a request without a URL \(request).")
@ -27,7 +34,16 @@ final class TestTransport: Transport {
func send(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) { func send(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) {
guard let urlString = request.url?.absoluteString else { guard let url = request.url, var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
completion(.failure(TestTransportError.invalidState))
return
}
components.queryItems = components
.queryItems?
.filter { !blacklistedQueryItemNames.contains($0.name) }
guard let urlString = components.url?.absoluteString else {
completion(.failure(TestTransportError.invalidState)) completion(.failure(TestTransportError.invalidState))
return return
} }

View File

@ -89,20 +89,36 @@ final class FeedlyAPICaller {
} }
} }
func getStream(for collection: FeedlyCollection, unreadOnly: Bool = false, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) { func getStream(for collection: FeedlyCollection, newerThan: Date? = nil, unreadOnly: Bool? = nil, completionHandler: @escaping (Result<FeedlyStream, Error>) -> ()) {
guard let accessToken = credentials?.secret else { guard let accessToken = credentials?.secret else {
return DispatchQueue.main.async { return DispatchQueue.main.async {
completionHandler(.failure(CredentialsError.incompleteCredentials)) completionHandler(.failure(CredentialsError.incompleteCredentials))
} }
} }
var components = baseUrlComponents var components = baseUrlComponents
components.path = "/v3/streams/contents" components.path = "/v3/streams/contents"
// If you change these, check AccountFeedlySyncTest.set(testFiles:with:). // If you change these, check AccountFeedlySyncTest.set(testFiles:with:).
components.queryItems = [ var queryItems = [URLQueryItem]()
URLQueryItem(name: "unreadOnly", value: unreadOnly ? "true" : "false"),
URLQueryItem(name: "count", value: "500"), if let date = newerThan {
let value = String(Int(date.timeIntervalSince1970 * 1000))
let queryItem = URLQueryItem(name: "newerThan", value: value)
queryItems.append(queryItem)
}
if let flag = unreadOnly {
let value = flag ? "true" : "false"
let queryItem = URLQueryItem(name: "unreadOnly", value: value)
queryItems.append(queryItem)
}
queryItems.append(contentsOf: [
URLQueryItem(name: "count", value: "1000"),
URLQueryItem(name: "streamId", value: collection.id), URLQueryItem(name: "streamId", value: collection.id),
] ])
components.queryItems = queryItems
guard let url = components.url else { guard let url = components.url else {
fatalError("\(components) does not produce a valid URL.") fatalError("\(components) does not produce a valid URL.")

View File

@ -86,10 +86,8 @@ final class FeedlyAccountDelegate: AccountDelegate {
progress.addToNumberOfTasksAndRemaining(1) progress.addToNumberOfTasksAndRemaining(1)
syncStrategy?.startSync { result in syncStrategy?.startSync { result in
os_log(.debug, log: log, "Sync took %.3f seconds", -date.timeIntervalSinceNow) os_log(.debug, log: log, "Sync took %.3f seconds", -date.timeIntervalSinceNow)
DispatchQueue.main.async { progress.completeTask()
progress.completeTask() completion(result)
completion(result)
}
} }
} }

View File

@ -30,13 +30,15 @@ final class FeedlyGetCollectionStreamOperation: FeedlyOperation, FeedlyCollectio
let account: Account let account: Account
let caller: FeedlyAPICaller let caller: FeedlyAPICaller
let unreadOnly: Bool let unreadOnly: Bool?
let newerThan: Date?
init(account: Account, collection: FeedlyCollection, caller: FeedlyAPICaller, unreadOnly: Bool = false) { init(account: Account, collection: FeedlyCollection, caller: FeedlyAPICaller, newerThan: Date?, unreadOnly: Bool? = nil) {
self.account = account self.account = account
self.collection = collection self.collection = collection
self.caller = caller self.caller = caller
self.unreadOnly = unreadOnly self.unreadOnly = unreadOnly
self.newerThan = newerThan
} }
override func main() { override func main() {
@ -45,8 +47,7 @@ final class FeedlyGetCollectionStreamOperation: FeedlyOperation, FeedlyCollectio
return return
} }
//TODO: Use account metadata to get articles newer than some date. caller.getStream(for: collection, newerThan: newerThan, unreadOnly: unreadOnly) { result in
caller.getStream(for: collection, unreadOnly: unreadOnly) { result in
switch result { switch result {
case .success(let stream): case .success(let stream):
self.storedStream = stream self.storedStream = stream

View File

@ -23,11 +23,15 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation {
let caller: FeedlyAPICaller let caller: FeedlyAPICaller
let account: Account let account: Account
let log: OSLog let log: OSLog
let newerThan: Date?
let unreadOnly: Bool?
init(account: Account, collectionsProvider: FeedlyCollectionProviding, caller: FeedlyAPICaller, log: OSLog) { init(account: Account, collectionsProvider: FeedlyCollectionProviding, newerThan: Date?, unreadOnly: Bool?, caller: FeedlyAPICaller, log: OSLog) {
self.account = account self.account = account
self.caller = caller self.caller = caller
self.collectionsProvider = collectionsProvider self.collectionsProvider = collectionsProvider
self.newerThan = newerThan
self.unreadOnly = unreadOnly
self.log = log self.log = log
} }
@ -41,7 +45,11 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation {
// TODO: Prioritise the must read collection/category before others so the most important content for the user loads first. // TODO: Prioritise the must read collection/category before others so the most important content for the user loads first.
for collection in collectionsProvider.collections { for collection in collectionsProvider.collections {
let operation = FeedlyGetCollectionStreamOperation(account: account, collection: collection, caller: caller) let operation = FeedlyGetCollectionStreamOperation(account: account,
collection: collection,
caller: caller,
newerThan: newerThan,
unreadOnly: unreadOnly)
queueDelegate?.feedlyRequestStreamsOperation(self, enqueue: operation) queueDelegate?.feedlyRequestStreamsOperation(self, enqueue: operation)
} }

View File

@ -32,6 +32,14 @@ final class FeedlySyncStrategy {
private var startSyncCompletionHandler: ((Result<Void, Error>) -> ())? private var startSyncCompletionHandler: ((Result<Void, Error>) -> ())?
private var newerThan: Date? {
if let date = account.metadata.lastArticleFetch {
return date
} else {
return Calendar.current.date(byAdding: .day, value: -31, to: Date())
}
}
/// The truth is in the cloud. /// The truth is in the cloud.
func startSync(completionHandler: @escaping (Result<Void, Error>) -> ()) { func startSync(completionHandler: @escaping (Result<Void, Error>) -> ()) {
guard operationQueue.operationCount == 0 else { guard operationQueue.operationCount == 0 else {
@ -63,6 +71,8 @@ final class FeedlySyncStrategy {
// Get the streams for each Collection. It will call back to enqueue more operations. // Get the streams for each Collection. It will call back to enqueue more operations.
let getCollectionStreams = FeedlyRequestStreamsOperation(account: account, let getCollectionStreams = FeedlyRequestStreamsOperation(account: account,
collectionsProvider: getCollections, collectionsProvider: getCollections,
newerThan: newerThan,
unreadOnly: false,
caller: caller, caller: caller,
log: log) log: log)
getCollectionStreams.delegate = self getCollectionStreams.delegate = self
@ -71,12 +81,16 @@ final class FeedlySyncStrategy {
// Last operation to perform, which should be dependent on any other operation added to the queue. // Last operation to perform, which should be dependent on any other operation added to the queue.
let syncId = UUID().uuidString let syncId = UUID().uuidString
let lastArticleFetchDate = Date()
let completionOperation = BlockOperation { [weak self] in let completionOperation = BlockOperation { [weak self] in
if let self = self { DispatchQueue.main.async {
os_log(.debug, log: self.log, "Sync completed: %@", syncId) if let self = self {
self.startSyncCompletionHandler = nil self.account.metadata.lastArticleFetch = lastArticleFetchDate
os_log(.debug, log: self.log, "Sync completed: %@", syncId)
self.startSyncCompletionHandler = nil
}
completionHandler(.success(()))
} }
completionHandler(.success(()))
} }
completionOperation.addDependency(getCollections) completionOperation.addDependency(getCollections)