From 913f8de70d30ef25bc876b71728e38f8e7418a97 Mon Sep 17 00:00:00 2001 From: Jacob Sikorski Date: Thu, 16 Nov 2023 12:33:11 -0700 Subject: [PATCH] Remove TabMO migration --- App/iOS/Delegates/AppState.swift | 1 - .../Brave/Frontend/Browser/SessionData.swift | 110 ------- Sources/Brave/Frontend/Browser/Tab.swift | 1 - Sources/Brave/Migration/Migration.swift | 99 ------ .../Model.xcdatamodeld/.xccurrentversion | 2 +- .../Model26.xcdatamodel/contents | 221 +++++++++++++ Sources/Data/models/TabMO.swift | 241 --------------- Tests/DataTests/DataControllerTests.swift | 3 - Tests/DataTests/TabMOTests.swift | 290 ------------------ 9 files changed, 222 insertions(+), 746 deletions(-) delete mode 100644 Sources/Brave/Frontend/Browser/SessionData.swift create mode 100644 Sources/Data/models/Model.xcdatamodeld/Model26.xcdatamodel/contents delete mode 100644 Sources/Data/models/TabMO.swift delete mode 100644 Tests/DataTests/TabMOTests.swift diff --git a/App/iOS/Delegates/AppState.swift b/App/iOS/Delegates/AppState.swift index 20b43cb7562..eade1547a26 100644 --- a/App/iOS/Delegates/AppState.swift +++ b/App/iOS/Delegates/AppState.swift @@ -49,7 +49,6 @@ public class AppState { didBecomeActive = true DataController.shared.initializeOnce() Migration.postCoreDataInitMigrations() - Migration.migrateTabStateToWebkitState(diskImageStore: diskImageStore) Migration.migrateLostTabsActiveWindow() } break diff --git a/Sources/Brave/Frontend/Browser/SessionData.swift b/Sources/Brave/Frontend/Browser/SessionData.swift deleted file mode 100644 index 7b73c1faebe..00000000000 --- a/Sources/Brave/Frontend/Browser/SessionData.swift +++ /dev/null @@ -1,110 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import Foundation - -import Shared -import Data - -class SessionData: NSObject, NSSecureCoding { - let currentPage: Int - let urls: [URL] - let lastUsedTime: Timestamp - let isPrivate: Bool - - struct Keys { - static let currentPage = "currentPage" - static let history = "history" - static let lastUsedTime = "lastUsedTime" - static let urls = "url" - static let isPrivate = "isPrivate" - } - - var jsonDictionary: [String: Any] { - return [ - SessionData.Keys.currentPage: String(self.currentPage), - SessionData.Keys.lastUsedTime: String(self.lastUsedTime), - SessionData.Keys.urls: urls.map { $0.absoluteString }, - SessionData.Keys.isPrivate: self.isPrivate, - ] - } - - /** - Creates a new SessionData object representing a serialized tab. - - - parameter currentPage: The active page index. Must be in the range of (-N, 0], - where 1-N is the first page in history, and 0 is the last. - - parameter urls: The sequence of URLs in this tab's session history. - - parameter lastUsedTime: The last time this tab was modified. - **/ - init(currentPage: Int, urls: [URL], lastUsedTime: Timestamp, isPrivate: Bool) { - self.currentPage = currentPage - self.urls = SessionData.updateSessionURLs(urls: urls) - self.lastUsedTime = lastUsedTime - self.isPrivate = isPrivate - - assert(!urls.isEmpty, "Session has at least one entry") - assert(currentPage > -urls.count && currentPage <= 0, "Session index is valid") - } - - required init?(coder: NSCoder) { - self.currentPage = coder.decodeInteger(forKey: SessionData.Keys.currentPage) - self.urls = coder.decodeObject(of: [NSURL.self], forKey: "urls") as? [URL] ?? [] - self.lastUsedTime = UInt64(coder.decodeInt64(forKey: SessionData.Keys.lastUsedTime)) - self.isPrivate = coder.decodeBool(forKey: SessionData.Keys.isPrivate) - } - - func encode(with coder: NSCoder) { - coder.encode(currentPage, forKey: SessionData.Keys.currentPage) - coder.encode(urls, forKey: SessionData.Keys.urls) - coder.encode(Int64(lastUsedTime), forKey: SessionData.Keys.lastUsedTime) - coder.encode(isPrivate, forKey: SessionData.Keys.isPrivate) - } - - // This is not a fully direct mapping, but rather an attempt to reconcile data differences, primarily used for tab restoration - var savedTabData: SavedTab { - let urlStrings = jsonDictionary[SessionData.Keys.urls] as? [String] ?? [] - let isPrivate = jsonDictionary[SessionData.Keys.isPrivate] as? Bool ?? false - let currentURL = urlStrings[(currentPage < 0 ? max(urlStrings.count - 1, 0) : currentPage)] - - return SavedTab(id: "InvalidId", title: nil, url: currentURL, isSelected: false, order: -1, screenshot: nil, history: urlStrings, historyIndex: Int16(currentPage), isPrivate: isPrivate) - } - - static var supportsSecureCoding: Bool { - return true - } - - /// PR: https://github.com/mozilla-mobile/firefox-ios/pull/4387 - /// Commit: https://github.com/mozilla-mobile/firefox-ios/commit/8b1450fbeb87f1f559a2f8e42971c715dc96bcaf - /// InternalURL helps encapsulate all internal scheme logic for urls rather than using URL extension. Extensions to built-in classes should be more minimal that what was being done previously. - /// This migration was required mainly for above PR which is related to a PI request that reduces security risk. Also, this particular method helps in cleaning up / migrating old localhost:6571 URLs to internal: SessionData urls - static func updateSessionURLs(urls: [URL]) -> [URL] { - return urls.compactMap { url in - var url = url - let port = AppConstants.webServerPort - [ - ("http://localhost:\(port)/errors/error.html?url=", "\(InternalURL.baseUrl)/\(SessionRestoreHandler.path)?url="), - // ("http://localhost:\(port)/reader-mode?url=", "\(InternalURL.baseUrl)/\(ReaderModeHandler.path)?url=") - ].forEach { - oldItem, newItem in - if url.absoluteString.hasPrefix(oldItem) { - var urlStr = url.absoluteString.replacingOccurrences(of: oldItem, with: newItem) - let comp = urlStr.components(separatedBy: newItem) - if comp.count > 2 { - // get the last instance of incorrectly nested urls - urlStr = newItem + (comp.last ?? "") - assertionFailure("SessionData urls have nested internal links, investigate: [\(url.absoluteString)]") - } - url = URL(string: urlStr) ?? url - } - } - - if let internalUrl = InternalURL(url), internalUrl.isAuthorized, let stripped = URL(string: internalUrl.stripAuthorization) { - return stripped - } - - return url - } - } -} diff --git a/Sources/Brave/Frontend/Browser/Tab.swift b/Sources/Brave/Frontend/Browser/Tab.swift index f5225749763..971f50ffea9 100644 --- a/Sources/Brave/Frontend/Browser/Tab.swift +++ b/Sources/Brave/Frontend/Browser/Tab.swift @@ -551,7 +551,6 @@ class Tab: NSObject { guard let lastTitle = lastTitle, !lastTitle.isEmpty else { // FF uses url?.displayURL?.absoluteString ?? "" - // but we can grab the title from `TabMO` if let title = url?.absoluteString { syncTab?.setTitle(title) return title diff --git a/Sources/Brave/Migration/Migration.swift b/Sources/Brave/Migration/Migration.swift index d3904fef425..c88d2191183 100644 --- a/Sources/Brave/Migration/Migration.swift +++ b/Sources/Brave/Migration/Migration.swift @@ -53,102 +53,6 @@ public class Migration { braveCore.syncAPI.enableSyncTypes(syncProfileService: braveCore.syncProfileService) } - // Migrate from TabMO to SessionTab and SessionWindow - public static func migrateTabStateToWebkitState(diskImageStore: DiskImageStore?) { - let isPrivate = false // Private tabs at the time of writing this code was never persistent, so it wouldn't "restore". - - if Preferences.Migration.tabMigrationToInteractionStateCompleted.value { - SessionWindow.createIfNeeded(index: 0, isPrivate: isPrivate, isSelected: true) - return - } - - // Get all the old Tabs from TabMO - let oldTabIDs = TabMO.getAll().map({ $0.objectID }) - - // Nothing to migrate - if oldTabIDs.isEmpty { - // Create a SessionWindow (default window) - // Set the window selected by default - TabMO.migrate { context in - _ = SessionWindow(context: context, index: 0, isPrivate: isPrivate, isSelected: true) - } - - Preferences.Migration.tabMigrationToInteractionStateCompleted.value = true - return - } - - TabMO.migrate { context in - let oldTabs = oldTabIDs.compactMap({ context.object(with: $0) as? TabMO }) - if oldTabs.isEmpty { return } // Migration failed - - // Create a SessionWindow (default window) - // Set the window selected by default - let sessionWindow = SessionWindow(context: context, index: 0, isPrivate: isPrivate, isSelected: true) - - oldTabs.forEach { oldTab in - guard let urlString = oldTab.url, - let url = NSURL(idnString: urlString) as? URL ?? URL(string: urlString) else { - return - } - - var tabId: UUID - if let syncUUID = oldTab.syncUUID { - tabId = UUID(uuidString: syncUUID) ?? UUID() - } else { - tabId = UUID() - } - - var historyURLs = [URL]() - let tabTitle = oldTab.title ?? Strings.newTab - let historySnapshot = oldTab.urlHistorySnapshot as? [String] ?? [] - - for url in historySnapshot { - guard let url = NSURL(idnString: url) as? URL ?? URL(string: url) else { - Logger.module.error("Failed to parse URL: \(url) during Migration!") - continue - } - if let internalUrl = InternalURL(url), !internalUrl.isAuthorized, let authorizedURL = InternalURL.authorize(url: url) { - historyURLs.append(authorizedURL) - } else { - historyURLs.append(url) - } - } - - if historyURLs.count == 0 { - Logger.module.error("User has zero history to migrate!") - return - } - - // currentPage is -webView.backForwardList.forwardList.count - // If for some reason current page can be negative, we clamp it to [0, inf]. - let currentPage = max((historyURLs.count - 1) + Int(oldTab.urlHistoryCurrentIndex), 0) - - // Create WebKit interactionState - let interactionState = SynthesizedSessionRestore.serialize(withTitle: tabTitle, - historyURLs: historyURLs, - pageIndex: UInt(currentPage), - isPrivateBrowsing: isPrivate) - - // Create SessionTab and associate it with a SessionWindow - // Tabs currently do not have groups, so sessionTabGroup is nil by default - _ = SessionTab(context: context, - sessionWindow: sessionWindow, - sessionTabGroup: nil, - index: Int32(oldTab.order), - interactionState: interactionState, - isPrivate: isPrivate, - isSelected: oldTab.isSelected, - lastUpdated: oldTab.lastUpdate ?? .now, - screenshotData: Data(), // Do not migrate screenshot data - title: tabTitle, - url: url, - tabId: tabId) - } - - Preferences.Migration.tabMigrationToInteractionStateCompleted.value = true - } - } - public static func migrateLostTabsActiveWindow() { if UIApplication.shared.supportsMultipleScenes { return } if Preferences.Migration.lostTabsWindowIDMigrationOne.value { return } @@ -194,9 +98,6 @@ public class Migration { public static func postCoreDataInitMigrations() { if Preferences.Migration.coreDataCompleted.value { return } - - TabMO.deleteAllPrivateTabs() - Preferences.Migration.coreDataCompleted.value = true } diff --git a/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion b/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion index bcbcdab8fd3..d81839d4a34 100644 --- a/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion +++ b/Sources/Data/models/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model25.xcdatamodel + Model26.xcdatamodel diff --git a/Sources/Data/models/Model.xcdatamodeld/Model26.xcdatamodel/contents b/Sources/Data/models/Model.xcdatamodeld/Model26.xcdatamodel/contents new file mode 100644 index 00000000000..e943ca58ced --- /dev/null +++ b/Sources/Data/models/Model.xcdatamodeld/Model26.xcdatamodel/contents @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/Data/models/TabMO.swift b/Sources/Data/models/TabMO.swift deleted file mode 100644 index 0200e3876be..00000000000 --- a/Sources/Data/models/TabMO.swift +++ /dev/null @@ -1,241 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import UIKit -import CoreData -import Foundation -import Shared -import WebKit -import Strings -import os.log - -/// Properties we want to extract from Tab/TabManager and save in TabMO -public struct SavedTab { - public let id: String - public let title: String? - public let url: String - public let isSelected: Bool - public let order: Int16 - public let screenshot: UIImage? - public let history: [String] - public let historyIndex: Int16 - public let isPrivate: Bool - - /// For the love of all developers everywhere, if you use this constructor, **PLEASE** use - /// `SessionData.updateSessionURLs(urls)` **BEFORE** passing in the URLs for the `history` parameter!!! - /// If you don't, you **WILL break session restore**. - public init( - id: String, title: String?, url: String, isSelected: Bool, order: Int16, screenshot: UIImage?, - history: [String], historyIndex: Int16, isPrivate: Bool) { - self.id = id - self.title = title - self.url = url - self.isSelected = isSelected - self.order = order - self.screenshot = screenshot - self.history = history - self.historyIndex = historyIndex - self.isPrivate = isPrivate - - } -} - -public final class TabMO: NSManagedObject, CRUD { - - @NSManaged public var title: String? - @NSManaged public var url: String? - @NSManaged public var syncUUID: String? - @NSManaged public var order: Int16 - @NSManaged public var urlHistorySnapshot: NSArray? // array of strings for urls - @NSManaged public var urlHistoryCurrentIndex: Int16 - @NSManaged public var screenshot: Data? - @NSManaged public var isSelected: Bool - @NSManaged public var color: String? - @NSManaged public var screenshotUUID: String? - /// Last time this tab was updated. Required for 'purge unused tabs' feature. - @NSManaged public var lastUpdate: Date? - @NSManaged public var isPrivate: Bool - - public override func prepareForDeletion() { - super.prepareForDeletion() - } - - // MARK: - Public interface - - public static func migrate(_ block: @escaping (NSManagedObjectContext) -> Void) { - DataController.performOnMainContext(save: true) { context in - block(context) - - do { - try context.save() - } catch { - Logger.module.error("Error saving context: \(error)") - } - } - } - - // MARK: Create - - /// Creates new tab and returns its syncUUID. If you want to add urls to existing tabs use `update()` method. - public class func create(title: String, uuidString: String = UUID().uuidString) -> String { - createInternal(uuidString: uuidString, title: title, lastUpdateDate: Date()) - return uuidString - } - - class func createInternal(uuidString: String, title: String, lastUpdateDate: Date) { - DataController.perform(task: { context in - guard let entity = entity(context) else { - Logger.module.error("Error fetching the entity 'Tab' from Managed Object-Model") - return - } - - let tab = TabMO(entity: entity, insertInto: context) - // TODO: replace with logic to create sync uuid then buble up new uuid to browser. - tab.syncUUID = uuidString - tab.title = title - tab.lastUpdate = lastUpdateDate - }) - } - - // MARK: Read - - public class func getAll() -> [TabMO] { - let sortDescriptors = [NSSortDescriptor(key: #keyPath(TabMO.order), ascending: true)] - return all(sortDescriptors: sortDescriptors) ?? [] - } - - public class func all(noOlderThan timeInterval: TimeInterval) -> [TabMO] { - let lastUpdateKeyPath = #keyPath(TabMO.lastUpdate) - let date = Date().advanced(by: -timeInterval) as NSDate - - let sortDescriptors = [NSSortDescriptor(key: #keyPath(TabMO.order), ascending: true)] - let predicate = NSPredicate(format: "\(lastUpdateKeyPath) = nil OR \(lastUpdateKeyPath) > %@", date) - return all(where: predicate, sortDescriptors: sortDescriptors) ?? [] - } - - public class func get(fromId id: String?) -> TabMO? { - return getInternal(fromId: id) - } - - // MARK: Update - - // Updates existing tab with new data. - // Usually called when user navigates to a new website for in his existing tab. - public class func update(tabData: SavedTab) { - DataController.perform { context in - guard let tabToUpdate = getInternal(fromId: tabData.id, context: context) else { return } - - if let screenshot = tabData.screenshot { - tabToUpdate.screenshot = screenshot.jpegData(compressionQuality: 1) - } - tabToUpdate.url = tabData.url - tabToUpdate.order = tabData.order - tabToUpdate.title = tabData.title - tabToUpdate.urlHistorySnapshot = tabData.history as NSArray - tabToUpdate.urlHistoryCurrentIndex = tabData.historyIndex - tabToUpdate.isSelected = tabData.isSelected - tabToUpdate.lastUpdate = Date() - tabToUpdate.isPrivate = tabData.isPrivate - } - } - - // Updates Tab's last accesed time. - public class func touch(tabID: String) { - DataController.perform { context in - guard let tabToUpdate = getInternal(fromId: tabID, context: context) else { return } - tabToUpdate.lastUpdate = Date() - } - } - - public class func selectTabAndDeselectOthers(selectedTabId: String) { - DataController.perform { context in - guard let tabToUpdate = getInternal(fromId: selectedTabId, context: context) else { return } - - let predicate = NSPredicate(format: "isSelected == true") - all(where: predicate, context: context)? - .forEach { - $0.isSelected = false - } - - tabToUpdate.isSelected = true - } - } - - // Deletes the Tab History by removing items except the last one from historysnapshot and setting current index - public class func removeHistory(with tabID: String) { - DataController.perform { context in - guard let tabToUpdate = getInternal(fromId: tabID, context: context) else { return } - - if let lastItem = tabToUpdate.urlHistorySnapshot?.lastObject { - tabToUpdate.urlHistorySnapshot = [lastItem] as NSArray - tabToUpdate.urlHistoryCurrentIndex = 0 - } - } - } - - public class func saveScreenshotUUID(_ uuid: UUID?, tabId: String?) { - DataController.perform { context in - let tabMO = getInternal(fromId: tabId, context: context) - tabMO?.screenshotUUID = uuid?.uuidString - } - } - - public class func saveTabOrder(tabIds: [String]) { - DataController.perform { context in - for (i, tabId) in tabIds.enumerated() { - guard let managedObject = getInternal(fromId: tabId, context: context) else { - Logger.module.error("Error: Tab missing managed object") - continue - } - managedObject.order = Int16(i) - } - } - } - - // MARK: Delete - - public func delete() { - delete(context: .new(inMemory: false)) - } - - public class func deleteAll() { - deleteAll(context: .new(inMemory: false)) - } - - public class func deleteAllPrivateTabs() { - deleteAll(predicate: NSPredicate(format: "isPrivate == true"), context: .new(inMemory: false)) - } - - public class func deleteAll(olderThan timeInterval: TimeInterval) { - let lastUpdateKeyPath = #keyPath(TabMO.lastUpdate) - let date = Date().advanced(by: -timeInterval) as NSDate - - let predicate = NSPredicate(format: "\(lastUpdateKeyPath) != nil AND \(lastUpdateKeyPath) < %@", date) - - self.deleteAll(predicate: predicate) - } -} - -// MARK: - Internal implementations -extension TabMO { - // Currently required, because not `syncable` - private static func entity(_ context: NSManagedObjectContext) -> NSEntityDescription? { - return NSEntityDescription.entity(forEntityName: "TabMO", in: context) - } - - private class func getInternal( - fromId id: String?, - context: NSManagedObjectContext = DataController.viewContext - ) -> TabMO? { - guard let id = id else { return nil } - let predicate = NSPredicate(format: "\(#keyPath(TabMO.syncUUID)) == %@", id) - - return first(where: predicate, context: context) - } - - var imageUrl: URL? { - if let objectId = self.syncUUID, let url = URL(string: "https://imagecache.mo/\(objectId).png") { - return url - } - return nil - } -} diff --git a/Tests/DataTests/DataControllerTests.swift b/Tests/DataTests/DataControllerTests.swift index 351e1e7f147..b9f932f2d6f 100644 --- a/Tests/DataTests/DataControllerTests.swift +++ b/Tests/DataTests/DataControllerTests.swift @@ -23,9 +23,6 @@ class DataControllerTests: CoreDataTestCase { let favoriteFR = NSFetchRequest(entityName: String(describing: "Bookmark")) XCTAssertEqual(try! viewContext.count(for: favoriteFR), 0) - let tabFR = NSFetchRequest(entityName: String(describing: TabMO.self)) - XCTAssertEqual(try! viewContext.count(for: tabFR), 0) - let domainFR = NSFetchRequest(entityName: String(describing: Domain.self)) XCTAssertEqual(try! viewContext.count(for: domainFR), 0) } diff --git a/Tests/DataTests/TabMOTests.swift b/Tests/DataTests/TabMOTests.swift deleted file mode 100644 index b56c69eaf7a..00000000000 --- a/Tests/DataTests/TabMOTests.swift +++ /dev/null @@ -1,290 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import XCTest -import CoreData -import Shared -import TestHelpers -@testable import Data - -class TabMOTests: CoreDataTestCase { - let fetchRequest = NSFetchRequest(entityName: String(describing: TabMO.self)) - - private func entity(for context: NSManagedObjectContext) -> NSEntityDescription { - return NSEntityDescription.entity(forEntityName: String(describing: TabMO.self), in: context)! - } - - func testCreate() { - let title = "New Tab" - let object = createAndWait(title: title) - XCTAssertEqual(try! DataController.viewContext.count(for: fetchRequest), 1) - - XCTAssertNotNil(object.syncUUID) - XCTAssertNotNil(object.imageUrl) - XCTAssertNil(object.url) - XCTAssertEqual(object.title, title) - - // Testing default values - XCTAssertEqual(object.order, 0) - XCTAssertEqual(object.urlHistoryCurrentIndex, 0) - - XCTAssertFalse(object.isSelected) - - XCTAssertNil(object.color) - XCTAssertNil(object.screenshot) - XCTAssertNil(object.screenshotUUID) - XCTAssertNil(object.url) - XCTAssertNil(object.urlHistorySnapshot) - - } - - func testUpdate() { - let newTitle = "UpdatedTitle" - let newUrl = "http://example.com" - - var object = createAndWait() - XCTAssertEqual(try! DataController.viewContext.count(for: fetchRequest), 1) - - XCTAssertNotEqual(object.title, newTitle) - XCTAssertNotEqual(object.url, newUrl) - - let tabData = SavedTab( - id: object.syncUUID!, title: newTitle, url: newUrl, isSelected: true, order: 10, - screenshot: UIImage.sampleImage(), history: ["history1", "history2"], historyIndex: 20, isPrivate: false) - - backgroundSaveAndWaitForExpectation { - TabMO.update(tabData: tabData) - } - XCTAssertEqual(try! DataController.viewContext.count(for: fetchRequest), 1) - - // Need to refresh context here. - DataController.viewContext.reset() - - object = try! DataController.viewContext.fetch(fetchRequest).first! - - XCTAssertNotNil(object.syncUUID) - XCTAssertNotNil(object.imageUrl) - XCTAssertNotNil(object.screenshot) - - XCTAssertEqual(object.url, newUrl) - XCTAssertEqual(object.title, newTitle) - XCTAssertEqual(object.order, 10) - XCTAssertEqual(object.urlHistoryCurrentIndex, 20) - XCTAssertEqual(object.urlHistorySnapshot?.count, 2) - - XCTAssert(object.isSelected) - - XCTAssertNil(object.color) - XCTAssertNil(object.screenshotUUID) - } - - func testUpdateWrongId() { - let newTitle = "UpdatedTitle" - let newUrl = "http://example.com" - let wrongId = "999" - - var object = createAndWait() - XCTAssertEqual(try! DataController.viewContext.count(for: fetchRequest), 1) - - XCTAssertNotEqual(object.title, newTitle) - XCTAssertNotEqual(object.url, newUrl) - - let tabData = SavedTab( - id: wrongId, title: newTitle, url: newUrl, isSelected: true, order: 10, - screenshot: UIImage.sampleImage(), history: ["history1", "history2"], historyIndex: 20, isPrivate: false) - - TabMO.update(tabData: tabData) - // We can't wait for context save here, wrong id is being passed, let's fake it by waiting one second - sleep(UInt32(1)) - - XCTAssertEqual(try! DataController.viewContext.count(for: fetchRequest), 1) - - // Need to refresh context here. - DataController.viewContext.reset() - object = try! DataController.viewContext.fetch(fetchRequest).first! - - // Nothing should change - XCTAssertNotEqual(object.title, newTitle) - XCTAssertNotEqual(object.url, newUrl) - } - - func testDelete() { - let object = createAndWait() - - XCTAssertEqual(try! DataController.viewContext.count(for: fetchRequest), 1) - backgroundSaveAndWaitForExpectation { - object.delete() - } - - XCTAssertEqual(try! DataController.viewContext.count(for: fetchRequest), 0) - } - - func testImageUrl() { - let object = createAndWait() - - XCTAssertEqual(object.imageUrl, URL(string: "https://imagecache.mo/\(object.syncUUID!).png")) - } - - func testSaveScreenshotUUID() { - let newUUID = UUID() - var object = createAndWait() - - XCTAssertNil(object.screenshotUUID) - backgroundSaveAndWaitForExpectation { - TabMO.saveScreenshotUUID(newUUID, tabId: object.syncUUID) - } - DataController.viewContext.reset() - - object = try! DataController.viewContext.fetch(fetchRequest).first! - XCTAssertNotNil(object.screenshotUUID) - } - - func testSaveScreenshotUUIDWrongId() { - let wrongId = "999" - let newUUID = UUID() - var object = createAndWait() - - XCTAssertNil(object.screenshotUUID) - TabMO.saveScreenshotUUID(newUUID, tabId: wrongId) - DataController.viewContext.reset() - - object = try! DataController.viewContext.fetch(fetchRequest).first! - XCTAssertNil(object.screenshotUUID) - } - - private func createAndUpdate(order: Int) { - let object = createAndWait() - - let tabData = SavedTab( - id: object.syncUUID!, title: "title\(order)", url: "url\(order)", isSelected: false, order: Int16(order), - screenshot: nil, history: [], historyIndex: 0, isPrivate: false) - backgroundSaveAndWaitForExpectation { - TabMO.update(tabData: tabData) - } - - } - - func testGetAll() { - createAndUpdate(order: 1) - createAndUpdate(order: 3) - createAndUpdate(order: 2) - - DataController.viewContext.refreshAllObjects() - - // Getting all objects and sorting them manually by order - let objectsSortedByOrder = try! DataController.viewContext.fetch(fetchRequest).sorted(by: { $0.order < $1.order }) - - // Verify objects were updated with correct order. - XCTAssertEqual(objectsSortedByOrder[0].order, 1) - XCTAssertEqual(objectsSortedByOrder[1].order, 2) - XCTAssertEqual(objectsSortedByOrder[2].order, 3) - - let all = TabMO.getAll() - XCTAssertEqual(all.count, 3) - - // getAll() also should return objects sorted by order - XCTAssertEqual(all[0].syncUUID, objectsSortedByOrder[0].syncUUID) - XCTAssertEqual(all[1].syncUUID, objectsSortedByOrder[1].syncUUID) - XCTAssertEqual(all[2].syncUUID, objectsSortedByOrder[2].syncUUID) - } - - func testGetAllNoOlderThan() { - let staleTab1 = createAndWait(lastUpdateDate: dateFrom(string: "2021-01-01")) - let staleTab2 = createAndWait(lastUpdateDate: dateFrom(string: "2021-01-10")) - - let now = Date() - - // Now - let freshTab1 = createAndWait() - // Past 3 days - let freshTab2 = createAndWait(lastUpdateDate: now.advanced(by: -(3 * 60 * 60 * 24))) - // 3 days in the future - let freshTab3 = createAndWait(lastUpdateDate: now.advanced(by: 3 * 60 * 60 * 24)) - - let all = TabMO.getAll() - XCTAssertEqual(all.count, 5) - - let freshOnly = TabMO.all(noOlderThan: 7 * 60 * 60 * 24) - XCTAssertEqual(freshOnly.count, 3) - - XCTAssert(freshOnly.contains(freshTab1)) - XCTAssert(freshOnly.contains(freshTab2)) - XCTAssert(freshOnly.contains(freshTab3)) - XCTAssertFalse(freshOnly.contains(staleTab1)) - XCTAssertFalse(freshOnly.contains(staleTab2)) - } - - func testDeleteOlderThan() { - let staleTab1 = createAndWait(lastUpdateDate: dateFrom(string: "2021-01-01")) - let staleTab2 = createAndWait(lastUpdateDate: dateFrom(string: "2021-01-10")) - - let now = Date() - - // Now - let freshTab1 = createAndWait() - // Past 3 days - let freshTab2 = createAndWait(lastUpdateDate: now.advanced(by: -(3 * 60 * 60 * 24))) - // 3 days in the future - let freshTab3 = createAndWait(lastUpdateDate: now.advanced(by: 3 * 60 * 60 * 24)) - - let all = TabMO.getAll() - XCTAssertEqual(all.count, 5) - - backgroundSaveAndWaitForExpectation { - TabMO.deleteAll(olderThan: 7 * 60 * 60 * 24) - } - - let allAfterDelete = TabMO.getAll() - XCTAssertEqual(allAfterDelete.count, 3) - - XCTAssert(allAfterDelete.contains(freshTab1)) - XCTAssert(allAfterDelete.contains(freshTab2)) - XCTAssert(allAfterDelete.contains(freshTab3)) - XCTAssertFalse(allAfterDelete.contains(staleTab1)) - XCTAssertFalse(allAfterDelete.contains(staleTab2)) - } - - func testGetFromId() { - let wrongId = "999" - let object = createAndWait() - - XCTAssertNotNil(TabMO.get(fromId: object.syncUUID!)) - XCTAssertNil(TabMO.get(fromId: wrongId)) - } - - @discardableResult private func createAndWait(lastUpdateDate: Date = Date(), title: String = "New Tab") -> TabMO { - let uuid = UUID().uuidString - - backgroundSaveAndWaitForExpectation { - TabMO.createInternal(uuidString: uuid, title: title, lastUpdateDate: lastUpdateDate) - } - - return TabMO.get(fromId: uuid)! - } - - private func dateFrom(string: String) -> Date { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd" - dateFormatter.timeZone = TimeZone(abbreviation: "GMT")! - - return dateFormatter.date(from: string)! - } -} - -private extension UIImage { - class func sampleImage() -> UIImage { - let color = UIColor.blue - let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 1, height: 1)) - UIGraphicsBeginImageContext(rect.size) - let context = UIGraphicsGetCurrentContext()! - - context.setFillColor(color.cgColor) - context.fill(rect) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return image! - } -}