mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-10 02:08:38 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
46
mobile/ios/StorageTests/CertTests.swift
Normal file
46
mobile/ios/StorageTests/CertTests.swift
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* 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 Storage
|
||||
|
||||
class CertTests: XCTestCase {
|
||||
func testCertStore() {
|
||||
let certStore = CertStore()
|
||||
let origin1 = "www.mozilla.org:80"
|
||||
let origin2 = "people.mozilla.org:80"
|
||||
let cert1 = getCertificate("testcert1")
|
||||
let cert2 = getCertificate("testcert2")
|
||||
|
||||
// Check that contains return false for certs not in store.
|
||||
XCTAssertFalse(certStore.containsCertificate(cert1, forOrigin: origin1))
|
||||
|
||||
// Add a certificate.
|
||||
certStore.addCertificate(cert1, forOrigin: origin1)
|
||||
|
||||
// Check that the cert is in the store.
|
||||
XCTAssert(certStore.containsCertificate(cert1, forOrigin: origin1))
|
||||
|
||||
// Check that the cert is unique to the origin.
|
||||
XCTAssertFalse(certStore.containsCertificate(cert1, forOrigin: origin2))
|
||||
XCTAssertFalse(certStore.containsCertificate(cert2, forOrigin: origin1))
|
||||
|
||||
// Add a different certificate for the same origin.
|
||||
certStore.addCertificate(cert2, forOrigin: origin1)
|
||||
|
||||
// Check that adding a cert for an existing origin doesn't do a replace.
|
||||
XCTAssert(certStore.containsCertificate(cert1, forOrigin: origin1))
|
||||
XCTAssert(certStore.containsCertificate(cert2, forOrigin: origin1))
|
||||
|
||||
// Check that adding an existing cert has no effect.
|
||||
certStore.addCertificate(cert1, forOrigin: origin1)
|
||||
XCTAssert(certStore.containsCertificate(cert1, forOrigin: origin1))
|
||||
}
|
||||
|
||||
fileprivate func getCertificate(_ file: String) -> SecCertificate {
|
||||
let path = Bundle(for: type(of: self)).path(forResource: file, ofType: "pem")
|
||||
let data = try? Data(contentsOf: URL(fileURLWithPath: path!))
|
||||
return SecCertificateCreateWithData(nil, data! as CFData)!
|
||||
}
|
||||
}
|
||||
76
mobile/ios/StorageTests/DiskImageStoreTests.swift
Normal file
76
mobile/ios/StorageTests/DiskImageStoreTests.swift
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
import UIKit
|
||||
|
||||
import XCTest
|
||||
|
||||
class DiskImageStoreTests: XCTestCase {
|
||||
var files: FileAccessor!
|
||||
var store: DiskImageStore!
|
||||
|
||||
override func setUp() {
|
||||
files = MockFiles()
|
||||
store = DiskImageStore(files: files, namespace: "DiskImageStoreTests", quality: 1)
|
||||
|
||||
_ = store.clearExcluding(Set()).value
|
||||
}
|
||||
|
||||
func testStore() {
|
||||
var success = false
|
||||
|
||||
// Avoid image comparison and use size of the image for equality
|
||||
let redImage = makeImageWithColor(UIColor.red, size: CGSize(width: 100, height: 100))
|
||||
let blueImage = makeImageWithColor(UIColor.blue, size: CGSize(width: 17, height: 17))
|
||||
|
||||
[(key: "blue", image: blueImage), (key: "red", image: redImage)].forEach() { (key, image) in
|
||||
XCTAssertNil(getImage(key), "\(key) key is nil")
|
||||
success = putImage(key, image: image)
|
||||
XCTAssert(success, "\(key) image added to store")
|
||||
XCTAssertEqual(getImage(key)!.size.width, image.size.width, "Images are equal")
|
||||
|
||||
success = putImage(key, image: image)
|
||||
XCTAssertFalse(success, "\(key) image not added again")
|
||||
}
|
||||
|
||||
_ = store.clearExcluding(Set(["red"])).value
|
||||
XCTAssertNotNil(getImage("red"), "Red image still exists")
|
||||
XCTAssertNil(getImage("blue"), "Blue image cleared")
|
||||
}
|
||||
|
||||
private func makeImageWithColor(_ color: UIColor, size: CGSize) -> UIImage {
|
||||
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
|
||||
color.setFill()
|
||||
UIRectFill(rect)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
return image
|
||||
}
|
||||
|
||||
private func getImage(_ key: String) -> UIImage? {
|
||||
let expectation = self.expectation(description: "Get succeeded")
|
||||
var image: UIImage?
|
||||
store.get(key).upon {
|
||||
image = $0.successValue
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
return image
|
||||
}
|
||||
|
||||
private func putImage(_ key: String, image: UIImage) -> Bool {
|
||||
let expectation = self.expectation(description: "Put succeeded")
|
||||
var success = false
|
||||
store.put(key, image: image).upon {
|
||||
success = $0.isSuccess
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
return success
|
||||
}
|
||||
}
|
||||
24
mobile/ios/StorageTests/Info.plist
Normal file
24
mobile/ios/StorageTests/Info.plist
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
23
mobile/ios/StorageTests/MockFiles.swift
Normal file
23
mobile/ios/StorageTests/MockFiles.swift
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
|
||||
import XCTest
|
||||
|
||||
class MockFiles: FileAccessor {
|
||||
init() {
|
||||
let docPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
|
||||
super.init(rootPath: (docPath as NSString).appendingPathComponent("testing"))
|
||||
}
|
||||
}
|
||||
|
||||
class SupportingFiles: FileAccessor {
|
||||
init() {
|
||||
let path = Bundle.main.bundlePath + "/PlugIns/StorageTests.xctest/"
|
||||
NSLog("Supporting files: \(path)")
|
||||
super.init(rootPath: path)
|
||||
}
|
||||
}
|
||||
140
mobile/ios/StorageTests/StorageTestUtils.swift
Normal file
140
mobile/ios/StorageTests/StorageTestUtils.swift
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/* 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/. */
|
||||
|
||||
// Note that this file is imported into SyncTests, too.
|
||||
|
||||
import Deferred
|
||||
import Foundation
|
||||
import Shared
|
||||
@testable import Storage
|
||||
import XCTest
|
||||
|
||||
extension BrowserDB {
|
||||
func assertQueryReturns(_ query: String, int: Int) {
|
||||
XCTAssertEqual(int, self.runQuery(query, args: nil, factory: IntFactory).value.successValue![0])
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowserDB {
|
||||
func moveLocalToMirrorForTesting() {
|
||||
// This is a risky process -- it's not the same logic that the real synchronizer uses
|
||||
// (because I haven't written it yet), so it might end up lying. We do what we can.
|
||||
let valueSQL = [
|
||||
"INSERT OR IGNORE INTO \(TableBookmarksMirror)",
|
||||
"(guid, type, date_added, bmkUri, title, parentid, parentName, feedUri, siteUri, pos,",
|
||||
" description, tags, keyword, folderName, queryId,",
|
||||
" is_overridden, server_modified, faviconID)",
|
||||
"SELECT guid, type, date_added, bmkUri, title, parentid, parentName,",
|
||||
"feedUri, siteUri, pos, description, tags, keyword, folderName, queryId,",
|
||||
"0 AS is_overridden, \(Date.now()) AS server_modified, faviconID",
|
||||
"FROM \(TableBookmarksLocal)",
|
||||
].joined(separator: " ")
|
||||
|
||||
// Copy its mirror structure.
|
||||
let structureSQL = "INSERT INTO \(TableBookmarksMirrorStructure) SELECT * FROM \(TableBookmarksLocalStructure)"
|
||||
|
||||
// Throw away the old.
|
||||
let deleteLocalStructureSQL = "DELETE FROM \(TableBookmarksLocalStructure)"
|
||||
let deleteLocalSQL = "DELETE FROM \(TableBookmarksLocal)"
|
||||
|
||||
self.run([
|
||||
valueSQL,
|
||||
structureSQL,
|
||||
deleteLocalStructureSQL,
|
||||
deleteLocalSQL,
|
||||
]).succeeded()
|
||||
}
|
||||
|
||||
func moveBufferToMirrorForTesting() {
|
||||
let valueSQL = [
|
||||
"INSERT OR IGNORE INTO \(TableBookmarksMirror)",
|
||||
"(guid, type, date_added, bmkUri, title, parentid, parentName, feedUri, siteUri, pos,",
|
||||
"description, tags, keyword, folderName, queryId, server_modified)",
|
||||
"SELECT",
|
||||
"guid, type, date_added, bmkUri, title, parentid, parentName, feedUri, siteUri, pos,",
|
||||
"description, tags, keyword, folderName, queryId, server_modified",
|
||||
"FROM \(TableBookmarksBuffer)",
|
||||
].joined(separator: " ")
|
||||
|
||||
let structureSQL = "INSERT INTO \(TableBookmarksMirrorStructure) SELECT * FROM \(TableBookmarksBufferStructure)"
|
||||
let deleteBufferStructureSQL = "DELETE FROM \(TableBookmarksBufferStructure)"
|
||||
let deleteBufferSQL = "DELETE FROM \(TableBookmarksBuffer)"
|
||||
|
||||
self.run([
|
||||
valueSQL,
|
||||
structureSQL,
|
||||
deleteBufferStructureSQL,
|
||||
deleteBufferSQL,
|
||||
]).succeeded()
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowserDB {
|
||||
func getGUIDs(_ sql: String) -> [GUID] {
|
||||
func guidFactory(_ row: SDRow) -> GUID {
|
||||
return row[0] as! GUID
|
||||
}
|
||||
|
||||
guard let cursor = self.runQuery(sql, args: nil, factory: guidFactory).value.successValue else {
|
||||
XCTFail("Unable to get cursor.")
|
||||
return []
|
||||
}
|
||||
return cursor.asArray()
|
||||
}
|
||||
|
||||
func getPositionsForChildrenOfParent(_ parent: GUID, fromTable table: String) -> [GUID: Int] {
|
||||
let args: Args = [parent]
|
||||
let factory: (SDRow) -> (GUID, Int) = {
|
||||
return ($0["child"] as! GUID, $0["idx"] as! Int)
|
||||
}
|
||||
let cursor = self.runQuery("SELECT child, idx FROM \(table) WHERE parent = ?", args: args, factory: factory).value.successValue!
|
||||
return cursor.reduce([:], { (dict, pair) in
|
||||
var dict = dict
|
||||
if let (k, v) = pair {
|
||||
dict[k] = v
|
||||
}
|
||||
return dict
|
||||
})
|
||||
}
|
||||
|
||||
func isLocallyDeleted(_ guid: GUID) -> Bool? {
|
||||
let args: Args = [guid]
|
||||
let cursor = self.runQuery("SELECT is_deleted FROM \(TableBookmarksLocal) WHERE guid = ?", args: args, factory: { $0.getBoolean("is_deleted") }).value.successValue!
|
||||
return cursor[0]
|
||||
}
|
||||
|
||||
func isOverridden(_ guid: GUID) -> Bool? {
|
||||
let args: Args = [guid]
|
||||
let cursor = self.runQuery("SELECT is_overridden FROM \(TableBookmarksMirror) WHERE guid = ?", args: args, factory: { $0.getBoolean("is_overridden") }).value.successValue!
|
||||
return cursor[0]
|
||||
}
|
||||
|
||||
func getSyncStatusForGUID(_ guid: GUID) -> SyncStatus? {
|
||||
let args: Args = [guid]
|
||||
let cursor = self.runQuery("SELECT sync_status FROM \(TableBookmarksLocal) WHERE guid = ?", args: args, factory: { $0[0] as! Int }).value.successValue!
|
||||
if let raw = cursor[0] {
|
||||
return SyncStatus(rawValue: raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRecordByURL(_ url: String, fromTable table: String) -> BookmarkMirrorItem {
|
||||
let args: Args = [url]
|
||||
return self.runQuery("SELECT * FROM \(table) WHERE bmkUri = ?", args: args, factory: BookmarkFactory.mirrorItemFactory).value.successValue![0]!
|
||||
}
|
||||
|
||||
func getRecordByGUID(_ guid: GUID, fromTable table: String) -> BookmarkMirrorItem {
|
||||
let args: Args = [guid]
|
||||
return self.runQuery("SELECT * FROM \(table) WHERE guid = ?", args: args, factory: BookmarkFactory.mirrorItemFactory).value.successValue![0]!
|
||||
}
|
||||
|
||||
func getChildrenOfFolder(_ folder: GUID) -> [GUID] {
|
||||
let args: Args = [folder]
|
||||
let sql =
|
||||
"SELECT child FROM \(ViewBookmarksLocalStructureOnMirror) " +
|
||||
"WHERE parent = ? " +
|
||||
"ORDER BY idx ASC"
|
||||
return self.runQuery(sql, args: args, factory: { $0[0] as! GUID }).value.successValue!.asArray()
|
||||
}
|
||||
}
|
||||
237
mobile/ios/StorageTests/SyncCommandsTests.swift
Normal file
237
mobile/ios/StorageTests/SyncCommandsTests.swift
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
|
||||
import XCTest
|
||||
import SwiftyJSON
|
||||
|
||||
func byValue(_ a: SyncCommand, b: SyncCommand) -> Bool {
|
||||
return a.value < b.value
|
||||
}
|
||||
|
||||
func byClient(_ a: RemoteClient, b: RemoteClient) -> Bool {
|
||||
return a.guid! < b.guid!
|
||||
}
|
||||
|
||||
class SyncCommandsTests: XCTestCase {
|
||||
|
||||
var clients: [RemoteClient] = [RemoteClient]()
|
||||
var clientsAndTabs: SQLiteRemoteClientsAndTabs!
|
||||
|
||||
var shareItems = [ShareItem]()
|
||||
|
||||
var multipleCommands: [ShareItem] = [ShareItem]()
|
||||
var wipeCommand: SyncCommand!
|
||||
var db: BrowserDB!
|
||||
|
||||
override func setUp() {
|
||||
let files = MockFiles()
|
||||
do {
|
||||
try files.remove("browser.db")
|
||||
} catch _ {
|
||||
}
|
||||
db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files)
|
||||
|
||||
// create clients
|
||||
let now = Date.now()
|
||||
let client1GUID = Bytes.generateGUID()
|
||||
let client2GUID = Bytes.generateGUID()
|
||||
let client3GUID = Bytes.generateGUID()
|
||||
|
||||
self.clients.append(RemoteClient(guid: client1GUID, name: "Test client 1", modified: (now - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: "55.0.1", fxaDeviceId: nil))
|
||||
self.clients.append(RemoteClient(guid: client2GUID, name: "Test client 2", modified: (now - OneHourInMilliseconds), type: "desktop", formfactor: "laptop", os: "Darwin", version: "55.0.1", fxaDeviceId: nil))
|
||||
self.clients.append(RemoteClient(guid: client3GUID, name: "Test local client", modified: (now - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: "55.0.1", fxaDeviceId: nil))
|
||||
clientsAndTabs = SQLiteRemoteClientsAndTabs(db: db)
|
||||
clientsAndTabs.insertOrUpdateClients(clients).succeeded()
|
||||
|
||||
shareItems.append(ShareItem(url: "http://mozilla.com", title: "Mozilla", favicon: nil))
|
||||
shareItems.append(ShareItem(url: "http://slashdot.org", title: "Slashdot", favicon: nil))
|
||||
shareItems.append(ShareItem(url: "http://news.bbc.co.uk", title: "BBC News", favicon: nil))
|
||||
shareItems.append(ShareItem(url: "http://news.bbc.co.uk", title: nil, favicon: nil))
|
||||
|
||||
wipeCommand = SyncCommand(value: "{'command':'wipeAll', 'args':[]}")
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
clientsAndTabs.deleteCommands().succeeded()
|
||||
clientsAndTabs.clear().succeeded()
|
||||
}
|
||||
|
||||
func testCreateSyncCommandFromShareItem() {
|
||||
let shareItem = shareItems[0]
|
||||
let syncCommand = SyncCommand.displayURIFromShareItem(shareItem, asClient: "abcdefghijkl")
|
||||
XCTAssertNil(syncCommand.commandID)
|
||||
XCTAssertNotNil(syncCommand.value)
|
||||
let jsonObj: [String: Any] = [
|
||||
"command": "displayURI",
|
||||
"args": [shareItem.url, "abcdefghijkl", shareItem.title ?? ""]
|
||||
]
|
||||
XCTAssertEqual(JSON(object: jsonObj).stringValue(), syncCommand.value)
|
||||
}
|
||||
|
||||
func testInsertWithNoURLOrTitle() {
|
||||
// Test insert command to table for
|
||||
let e = self.expectation(description: "Insert.")
|
||||
clientsAndTabs.insertCommand(self.wipeCommand, forClients: clients).upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
XCTAssertEqual(3, $0.successValue!)
|
||||
|
||||
let commandCursorDeferred = self.db.withConnection { connection -> Cursor<Int> in
|
||||
let select = "SELECT COUNT(*) FROM \(TableSyncCommands)"
|
||||
return connection.executeQuery(select, factory: IntFactory, withArgs: nil)
|
||||
}
|
||||
|
||||
let commandCursor = commandCursorDeferred.value.successValue!
|
||||
|
||||
XCTAssertNotNil(commandCursor[0])
|
||||
XCTAssertEqual(3, commandCursor[0]!)
|
||||
e.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testInsertWithURLOnly() {
|
||||
let shareItem = shareItems[3]
|
||||
let syncCommand = SyncCommand.displayURIFromShareItem(shareItem, asClient: "abcdefghijkl")
|
||||
|
||||
let e = self.expectation(description: "Insert.")
|
||||
clientsAndTabs.insertCommand(syncCommand, forClients: clients).upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
XCTAssertEqual(3, $0.successValue!)
|
||||
|
||||
let commandCursorDeferred = self.db.withConnection { connection -> Cursor<Int> in
|
||||
let select = "SELECT COUNT(*) FROM \(TableSyncCommands)"
|
||||
return connection.executeQuery(select, factory: IntFactory, withArgs: nil)
|
||||
}
|
||||
|
||||
let commandCursor = commandCursorDeferred.value.successValue!
|
||||
|
||||
XCTAssertNotNil(commandCursor[0])
|
||||
XCTAssertEqual(3, commandCursor[0]!)
|
||||
e.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testInsertWithMultipleCommands() {
|
||||
let e = self.expectation(description: "Insert.")
|
||||
let syncCommands = shareItems.map { item in
|
||||
return SyncCommand.displayURIFromShareItem(item, asClient: "abcdefghijkl")
|
||||
}
|
||||
clientsAndTabs.insertCommands(syncCommands, forClients: clients).upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
XCTAssertEqual(12, $0.successValue!)
|
||||
|
||||
let commandCursorDeferred = self.db.withConnection { connection -> Cursor<Int> in
|
||||
let select = "SELECT COUNT(*) FROM \(TableSyncCommands)"
|
||||
return connection.executeQuery(select, factory: IntFactory, withArgs: nil)
|
||||
}
|
||||
|
||||
let commandCursor = commandCursorDeferred.value.successValue!
|
||||
|
||||
XCTAssertNotNil(commandCursor[0])
|
||||
XCTAssertEqual(12, commandCursor[0]!)
|
||||
e.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testGetForAllClients() {
|
||||
let syncCommands = shareItems.map { item in
|
||||
return SyncCommand.displayURIFromShareItem(item, asClient: "abcdefghijkl")
|
||||
}.sorted(by: byValue)
|
||||
clientsAndTabs.insertCommands(syncCommands, forClients: clients).succeeded()
|
||||
|
||||
let b = self.expectation(description: "Get for invalid client.")
|
||||
clientsAndTabs.getCommands().upon({ result in
|
||||
XCTAssertTrue(result.isSuccess)
|
||||
if let clientCommands = result.successValue {
|
||||
XCTAssertEqual(clientCommands.count, self.clients.count)
|
||||
for client in clientCommands.keys {
|
||||
XCTAssertEqual(syncCommands, clientCommands[client]!.sorted(by: byValue))
|
||||
}
|
||||
} else {
|
||||
XCTFail("Expected no commands!")
|
||||
}
|
||||
b.fulfill()
|
||||
})
|
||||
self.waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testDeleteForValidClient() {
|
||||
let syncCommands = shareItems.map { item in
|
||||
return SyncCommand.displayURIFromShareItem(item, asClient: "abcdefghijkl")
|
||||
}.sorted(by: byValue)
|
||||
|
||||
var client = self.clients[0]
|
||||
let a = self.expectation(description: "delete for client.")
|
||||
let b = self.expectation(description: "Get for deleted client.")
|
||||
let c = self.expectation(description: "Get for not deleted client.")
|
||||
clientsAndTabs.insertCommands(syncCommands, forClients: clients).upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
XCTAssertEqual(12, $0.successValue!)
|
||||
|
||||
let result = self.clientsAndTabs.deleteCommands(client.guid!).value
|
||||
XCTAssertTrue(result.isSuccess)
|
||||
a.fulfill()
|
||||
|
||||
let commandCursorDeferred = self.db.withConnection { connection -> Cursor<Int> in
|
||||
let select = "SELECT COUNT(*) FROM \(TableSyncCommands) WHERE client_guid = '\(client.guid!)'"
|
||||
return connection.executeQuery(select, factory: IntFactory, withArgs: nil)
|
||||
}
|
||||
|
||||
let commandCursor = commandCursorDeferred.value.successValue!
|
||||
|
||||
XCTAssertNotNil(commandCursor[0])
|
||||
XCTAssertEqual(0, commandCursor[0]!)
|
||||
b.fulfill()
|
||||
|
||||
client = self.clients[1]
|
||||
let commandCursor2Deferred = self.db.withConnection { connection -> Cursor<Int> in
|
||||
let select = "SELECT COUNT(*) FROM \(TableSyncCommands) WHERE client_guid = '\(client.guid!)'"
|
||||
return connection.executeQuery(select, factory: IntFactory, withArgs: nil)
|
||||
}
|
||||
|
||||
let commandCursor2 = commandCursor2Deferred.value.successValue!
|
||||
|
||||
XCTAssertNotNil(commandCursor2[0])
|
||||
XCTAssertEqual(4, commandCursor2[0]!)
|
||||
c.fulfill()
|
||||
}
|
||||
|
||||
self.waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
|
||||
func testDeleteForAllClients() {
|
||||
let syncCommands = shareItems.map { item in
|
||||
return SyncCommand.displayURIFromShareItem(item, asClient: "abcdefghijkl")
|
||||
}
|
||||
|
||||
let a = self.expectation(description: "Wipe for all clients.")
|
||||
let b = self.expectation(description: "Get for clients.")
|
||||
clientsAndTabs.insertCommands(syncCommands, forClients: clients).upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
XCTAssertEqual(12, $0.successValue!)
|
||||
|
||||
let result = self.clientsAndTabs.deleteCommands().value
|
||||
XCTAssertTrue(result.isSuccess)
|
||||
a.fulfill()
|
||||
|
||||
self.clientsAndTabs.getCommands().upon({ result in
|
||||
XCTAssertTrue(result.isSuccess)
|
||||
if let clientCommands = result.successValue {
|
||||
XCTAssertEqual(0, clientCommands.count)
|
||||
} else {
|
||||
XCTFail("Expected no commands!")
|
||||
}
|
||||
b.fulfill()
|
||||
})
|
||||
}
|
||||
|
||||
self.waitForExpectations(timeout: 5, handler: nil)
|
||||
}
|
||||
}
|
||||
118
mobile/ios/StorageTests/SyncTelemetryTests.swift
Normal file
118
mobile/ios/StorageTests/SyncTelemetryTests.swift
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/* 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 SwiftyJSON
|
||||
@testable import Sync
|
||||
|
||||
import XCTest
|
||||
|
||||
fileprivate class MockFailure<T: CleartextPayloadJSON>: MaybeErrorType {
|
||||
let record: Record<T>
|
||||
|
||||
var description: String {
|
||||
return "Failed to store or upload record: \(record)"
|
||||
}
|
||||
|
||||
init(record: Record<T>) {
|
||||
self.record = record
|
||||
}
|
||||
}
|
||||
|
||||
class SyncTelemetryTests: XCTestCase {
|
||||
}
|
||||
|
||||
// MARK: IndependentRecordSynchronizer
|
||||
extension SyncTelemetryTests {
|
||||
private func getMockedIndependentRecordSynchronizer() -> IndependentRecordSynchronizer {
|
||||
let prefs = MockProfilePrefs()
|
||||
let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs)
|
||||
let delegate = MockSyncDelegate()
|
||||
|
||||
return IndependentRecordSynchronizer(scratchpad: scratchpad, delegate: delegate, basePrefs: prefs, why: .scheduled, collection: "mockHistory")
|
||||
}
|
||||
|
||||
func testApplyIncomingRecordsReportsDownloadStats() {
|
||||
let synchronizer = getMockedIndependentRecordSynchronizer()
|
||||
synchronizer.statsSession.start()
|
||||
|
||||
// Fake remote records for incoming changes
|
||||
let payloadA = CleartextPayloadJSON("{\"id\":\"A\",\"title\": \"A\"}")
|
||||
let A = Record<CleartextPayloadJSON>(id: "A", payload: payloadA)
|
||||
|
||||
let payloadB = CleartextPayloadJSON("{\"id\":\"B\",\"title\": \"B\"}")
|
||||
let B = Record<CleartextPayloadJSON>(id: "B", payload: payloadB)
|
||||
|
||||
let remoteRecords = [A, B]
|
||||
|
||||
let _ = synchronizer.applyIncomingRecords(remoteRecords) { record in
|
||||
return record.id == "B" ? deferMaybe(MockFailure(record: record)) : succeed()
|
||||
}.value
|
||||
|
||||
let session = synchronizer.statsSession.end()
|
||||
|
||||
let downloadStats = session.downloadStats
|
||||
XCTAssertEqual(downloadStats.applied, 2)
|
||||
XCTAssertEqual(downloadStats.succeeded, 1)
|
||||
XCTAssertEqual(downloadStats.failed, 1)
|
||||
}
|
||||
|
||||
func testApplyIncomingRecordsToStorageReportsDownloadStats() {
|
||||
let synchronizer = getMockedIndependentRecordSynchronizer()
|
||||
synchronizer.statsSession.start()
|
||||
|
||||
// Fake remote records for incoming changes
|
||||
let payloadA = CleartextPayloadJSON("{\"id\":\"A\",\"title\": \"A\"}")
|
||||
let A = Record<CleartextPayloadJSON>(id: "A", payload: payloadA)
|
||||
|
||||
let payloadB = CleartextPayloadJSON("{\"id\":\"B\",\"title\": \"B\"}")
|
||||
let B = Record<CleartextPayloadJSON>(id: "B", payload: payloadB)
|
||||
|
||||
let records = [A, B]
|
||||
|
||||
let _ = synchronizer.applyIncomingToStorage(records, fetched: Date.now()) { record in
|
||||
return record.id == "B" ? deferMaybe(MockFailure(record: record)) : succeed()
|
||||
}.value
|
||||
|
||||
let session = synchronizer.statsSession.end()
|
||||
|
||||
let downloadStats = session.downloadStats
|
||||
XCTAssertEqual(downloadStats.applied, 2)
|
||||
XCTAssertEqual(downloadStats.succeeded, 1)
|
||||
XCTAssertEqual(downloadStats.failed, 1)
|
||||
}
|
||||
|
||||
func testUploadRecordsReportsUploadStats() {
|
||||
let synchronizer = getMockedIndependentRecordSynchronizer()
|
||||
synchronizer.statsSession.start()
|
||||
|
||||
let now = Date.now()
|
||||
|
||||
// Fake local records for outgoing changes
|
||||
let payloadC = CleartextPayloadJSON("{\"id\":\"C\",\"title\": \"C\"}")
|
||||
let C = Record<CleartextPayloadJSON>(id: "C", payload: payloadC)
|
||||
|
||||
let payloadD = CleartextPayloadJSON("{\"id\":\"D\", \"title\": \"D\"}")
|
||||
let D = Record<CleartextPayloadJSON>(id: "D", payload: payloadD)
|
||||
let records = [C, D]
|
||||
|
||||
// Mock out a response for the uploader
|
||||
let uploader: BatchUploadFunction = { _, _, _ in
|
||||
let result = POSTResult(success: [C.id], failed: [D.id: "Invalid GUID"])
|
||||
let response = StorageResponse<POSTResult>(value: result, metadata: ResponseMetadata(status: 200, headers: [:]))
|
||||
return deferMaybe(response)
|
||||
}
|
||||
|
||||
let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 2, maxPostBytes: 1_048_576, maxTotalRecords: 10, maxTotalBytes: 104_857_600)
|
||||
let collectionClient = MockSyncCollectionClient(uploader: uploader, infoConfig: miniConfig, collection: "mockdata", encrypter: getEncrypter())
|
||||
let _ = synchronizer.uploadRecords(records, lastTimestamp: now, storageClient: collectionClient) { _, _ in
|
||||
return deferMaybe(now)
|
||||
}.value
|
||||
|
||||
let uploadStats = synchronizer.statsSession.uploadStats
|
||||
XCTAssertEqual(uploadStats.sent, 1)
|
||||
XCTAssertEqual(uploadStats.sentFailed, 1)
|
||||
}
|
||||
}
|
||||
124
mobile/ios/StorageTests/TestBrowserDB.swift
Normal file
124
mobile/ios/StorageTests/TestBrowserDB.swift
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
import XCGLogger
|
||||
|
||||
import XCTest
|
||||
|
||||
private let log = XCGLogger.default
|
||||
|
||||
class TestBrowserDB: XCTestCase {
|
||||
let files = MockFiles()
|
||||
|
||||
fileprivate func rm(_ path: String) {
|
||||
do {
|
||||
try files.remove(path)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
rm("foo.db")
|
||||
rm("foo.db-shm")
|
||||
rm("foo.db-wal")
|
||||
rm("foo.db.bak.1")
|
||||
rm("foo.db.bak.1-shm")
|
||||
rm("foo.db.bak.1-wal")
|
||||
}
|
||||
|
||||
class MockFailingSchema: Schema {
|
||||
var name: String { return "FAILURE" }
|
||||
var version: Int { return BrowserSchema.DefaultVersion + 1 }
|
||||
func drop(_ db: SQLiteDBConnection) -> Bool {
|
||||
return true
|
||||
}
|
||||
func create(_ db: SQLiteDBConnection) -> Bool {
|
||||
return false
|
||||
}
|
||||
func update(_ db: SQLiteDBConnection, from: Int) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class MockListener {
|
||||
var notification: Notification?
|
||||
@objc
|
||||
func onDatabaseWasRecreated(_ notification: Notification) {
|
||||
self.notification = notification
|
||||
}
|
||||
}
|
||||
|
||||
func testUpgradeV33toV34RemovesLongURLs() {
|
||||
let db = BrowserDB(filename: "v33.db", schema: BrowserSchema(), files: SupportingFiles())
|
||||
let results = db.runQuery("SELECT bmkUri, title FROM bookmarksLocal WHERE type = 1", args: nil, factory: { row in
|
||||
(row[0] as! String, row[1] as! String)
|
||||
}).value.successValue!
|
||||
|
||||
// The bookmark with the long URL has been deleted.
|
||||
XCTAssertTrue(results.count == 1)
|
||||
|
||||
let remaining = results[0]!
|
||||
|
||||
// This one's title has been truncated to 4096 chars.
|
||||
XCTAssertEqual(remaining.1.characters.count, 4096)
|
||||
XCTAssertEqual(remaining.1.utf8.count, 4096)
|
||||
XCTAssertTrue(remaining.1.hasPrefix("abcdefghijkl"))
|
||||
XCTAssertEqual(remaining.0, "http://example.com/short")
|
||||
}
|
||||
|
||||
func testMovesDB() {
|
||||
var db = BrowserDB(filename: "foo.db", schema: BrowserSchema(), files: self.files)
|
||||
db.run("CREATE TABLE foo (bar TEXT)").succeeded() // Just so we have writes in the WAL.
|
||||
|
||||
XCTAssertTrue(files.exists("foo.db"))
|
||||
XCTAssertTrue(files.exists("foo.db-shm"))
|
||||
XCTAssertTrue(files.exists("foo.db-wal"))
|
||||
|
||||
// Grab a pointer to the -shm so we can compare later.
|
||||
let shmAAttributes = try! files.attributesForFileAt(relativePath: "foo.db-shm")
|
||||
let creationA = shmAAttributes[FileAttributeKey.creationDate] as! Date
|
||||
let inodeA = (shmAAttributes[FileAttributeKey.systemFileNumber] as! NSNumber).uintValue
|
||||
|
||||
XCTAssertFalse(files.exists("foo.db.bak.1"))
|
||||
XCTAssertFalse(files.exists("foo.db.bak.1-shm"))
|
||||
XCTAssertFalse(files.exists("foo.db.bak.1-wal"))
|
||||
|
||||
let center = NotificationCenter.default
|
||||
let listener = MockListener()
|
||||
center.addObserver(listener, selector: #selector(MockListener.onDatabaseWasRecreated(_:)), name: NotificationDatabaseWasRecreated, object: nil)
|
||||
defer { center.removeObserver(listener) }
|
||||
|
||||
// It'll still fail, but it moved our old DB.
|
||||
// Our current observation is that closing the DB deletes the .shm file and also
|
||||
// checkpoints the WAL.
|
||||
db.forceClose()
|
||||
|
||||
db = BrowserDB(filename: "foo.db", schema: MockFailingSchema(), files: self.files)
|
||||
db.run("CREATE TABLE foo (bar TEXT)").failed() // This won't actually write since we'll get a failed connection
|
||||
db = BrowserDB(filename: "foo.db", schema: BrowserSchema(), files: self.files)
|
||||
db.run("CREATE TABLE foo (bar TEXT)").succeeded() // Just so we have writes in the WAL.
|
||||
|
||||
XCTAssertTrue(files.exists("foo.db"))
|
||||
XCTAssertTrue(files.exists("foo.db-shm"))
|
||||
XCTAssertTrue(files.exists("foo.db-wal"))
|
||||
|
||||
// But now it's been reopened, it's not the same -shm!
|
||||
let shmBAttributes = try! files.attributesForFileAt(relativePath: "foo.db-shm")
|
||||
let creationB = shmBAttributes[FileAttributeKey.creationDate] as! Date
|
||||
let inodeB = (shmBAttributes[FileAttributeKey.systemFileNumber] as! NSNumber).uintValue
|
||||
XCTAssertTrue(creationA.compare(creationB) != ComparisonResult.orderedDescending)
|
||||
XCTAssertNotEqual(inodeA, inodeB)
|
||||
|
||||
XCTAssertTrue(files.exists("foo.db.bak.1"))
|
||||
XCTAssertFalse(files.exists("foo.db.bak.1-shm"))
|
||||
XCTAssertFalse(files.exists("foo.db.bak.1-wal"))
|
||||
|
||||
// The right notification was issued.
|
||||
XCTAssertEqual("foo.db", (listener.notification?.object as? String))
|
||||
}
|
||||
}
|
||||
741
mobile/ios/StorageTests/TestLogins.swift
Normal file
741
mobile/ios/StorageTests/TestLogins.swift
Normal file
|
|
@ -0,0 +1,741 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
import XCGLogger
|
||||
|
||||
import XCTest
|
||||
|
||||
private let log = XCGLogger.default
|
||||
|
||||
class TestSQLiteLogins: XCTestCase {
|
||||
var db: BrowserDB!
|
||||
var logins: SQLiteLogins!
|
||||
|
||||
let formSubmitURL = "http://submit.me"
|
||||
let login = Login.createWithHostname("hostname1", username: "username1", password: "password1", formSubmitURL: "http://submit.me")
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let files = MockFiles()
|
||||
self.db = BrowserDB(filename: "testsqlitelogins.db", schema: LoginsSchema(), files: files)
|
||||
self.logins = SQLiteLogins(db: self.db)
|
||||
|
||||
let expectation = self.expectation(description: "Remove all logins.")
|
||||
self.removeAllLogins().upon({ res in expectation.fulfill() })
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testAddLogin() {
|
||||
log.debug("Created \(self.login)")
|
||||
let expectation = self.expectation(description: "Add login")
|
||||
|
||||
addLogin(login)
|
||||
>>> getLoginsFor(login.protectionSpace, expected: [login])
|
||||
>>> done(expectation)
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testGetOrder() {
|
||||
let expectation = self.expectation(description: "Add login")
|
||||
|
||||
// Different GUID.
|
||||
let login2 = Login.createWithHostname("hostname1", username: "username2", password: "password2")
|
||||
login2.formSubmitURL = "http://submit.me"
|
||||
|
||||
addLogin(login) >>> { self.addLogin(login2) } >>>
|
||||
getLoginsFor(login.protectionSpace, expected: [login2, login]) >>>
|
||||
done(expectation)
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testRemoveLogin() {
|
||||
let expectation = self.expectation(description: "Remove login")
|
||||
|
||||
addLogin(login)
|
||||
>>> { self.removeLogin(self.login) }
|
||||
>>> getLoginsFor(login.protectionSpace, expected: [])
|
||||
>>> done(expectation)
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testRemoveLogins() {
|
||||
let loginA = Login.createWithHostname("alphabet.com", username: "username1", password: "password1", formSubmitURL: formSubmitURL)
|
||||
let loginB = Login.createWithHostname("alpha.com", username: "username2", password: "password2", formSubmitURL: formSubmitURL)
|
||||
let loginC = Login.createWithHostname("berry.com", username: "username3", password: "password3", formSubmitURL: formSubmitURL)
|
||||
let loginD = Login.createWithHostname("candle.com", username: "username4", password: "password4", formSubmitURL: formSubmitURL)
|
||||
|
||||
func addLogins() -> Success {
|
||||
addLogin(loginA).succeeded()
|
||||
addLogin(loginB).succeeded()
|
||||
addLogin(loginC).succeeded()
|
||||
addLogin(loginD).succeeded()
|
||||
return succeed()
|
||||
}
|
||||
|
||||
addLogins().succeeded()
|
||||
let guids = [loginA.guid, loginB.guid]
|
||||
logins.removeLoginsWithGUIDs(guids).succeeded()
|
||||
let result = logins.getAllLogins().value.successValue!
|
||||
XCTAssertEqual(result.count, 2)
|
||||
}
|
||||
|
||||
func testRemoveManyLogins() {
|
||||
log.debug("Remove a large number of logins at once")
|
||||
var guids: [GUID] = []
|
||||
for i in 0..<2000 {
|
||||
let login = Login.createWithHostname("mozilla.org", username: "Fire", password: "fox", formSubmitURL: formSubmitURL)
|
||||
if i <= 1000 {
|
||||
guids += [login.guid]
|
||||
}
|
||||
addLogin(login).succeeded()
|
||||
}
|
||||
logins.removeLoginsWithGUIDs(guids).succeeded()
|
||||
let result = logins.getAllLogins().value.successValue!
|
||||
XCTAssertEqual(result.count, 999)
|
||||
}
|
||||
|
||||
func testUpdateLogin() {
|
||||
let expectation = self.expectation(description: "Update login")
|
||||
let updated = Login.createWithHostname("hostname1", username: "username1", password: "password3", formSubmitURL: formSubmitURL)
|
||||
updated.guid = self.login.guid
|
||||
|
||||
addLogin(login) >>> { self.updateLogin(updated) } >>>
|
||||
getLoginsFor(login.protectionSpace, expected: [updated]) >>>
|
||||
done(expectation)
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testAddInvalidLogin() {
|
||||
let emptyPasswordLogin = Login.createWithHostname("hostname1", username: "username1", password: "", formSubmitURL: formSubmitURL)
|
||||
var result = logins.addLogin(emptyPasswordLogin).value
|
||||
XCTAssertNil(result.successValue)
|
||||
XCTAssertNotNil(result.failureValue)
|
||||
XCTAssertEqual(result.failureValue?.description, "Can't add a login with an empty password.")
|
||||
|
||||
let emptyHostnameLogin = Login.createWithHostname("", username: "username1", password: "password", formSubmitURL: formSubmitURL)
|
||||
result = logins.addLogin(emptyHostnameLogin).value
|
||||
XCTAssertNil(result.successValue)
|
||||
XCTAssertNotNil(result.failureValue)
|
||||
XCTAssertEqual(result.failureValue?.description, "Can't add a login with an empty hostname.")
|
||||
|
||||
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
|
||||
let protectionSpace = URLProtectionSpace(host: "https://website.com", port: 443, protocol: "https", realm: "Basic Auth", authenticationMethod: "Basic Auth")
|
||||
let bothFormSubmitURLAndRealm = Login.createWithCredential(credential, protectionSpace: protectionSpace)
|
||||
bothFormSubmitURLAndRealm.formSubmitURL = "http://submit.me"
|
||||
result = logins.addLogin(bothFormSubmitURLAndRealm).value
|
||||
XCTAssertNil(result.successValue)
|
||||
XCTAssertNotNil(result.failureValue)
|
||||
XCTAssertEqual(result.failureValue?.description, "Can't add a login with both a httpRealm and formSubmitURL.")
|
||||
|
||||
let noFormSubmitURLOrRealm = Login.createWithHostname("host", username: "username1", password: "password", formSubmitURL: nil)
|
||||
result = logins.addLogin(noFormSubmitURLOrRealm).value
|
||||
XCTAssertNil(result.successValue)
|
||||
XCTAssertNotNil(result.failureValue)
|
||||
XCTAssertEqual(result.failureValue?.description, "Can't add a login without a httpRealm or formSubmitURL.")
|
||||
}
|
||||
|
||||
func testUpdateInvalidLogin() {
|
||||
let updated = Login.createWithHostname("hostname1", username: "username1", password: "", formSubmitURL: formSubmitURL)
|
||||
updated.guid = self.login.guid
|
||||
|
||||
addLogin(login).succeeded()
|
||||
var result = logins.updateLoginByGUID(login.guid, new: updated, significant: true).value
|
||||
XCTAssertNil(result.successValue)
|
||||
XCTAssertNotNil(result.failureValue)
|
||||
XCTAssertEqual(result.failureValue?.description, "Can't add a login with an empty password.")
|
||||
|
||||
let emptyHostnameLogin = Login.createWithHostname("", username: "username1", password: "", formSubmitURL: formSubmitURL)
|
||||
emptyHostnameLogin.guid = self.login.guid
|
||||
result = logins.updateLoginByGUID(login.guid, new: emptyHostnameLogin, significant: true).value
|
||||
XCTAssertNil(result.successValue)
|
||||
XCTAssertNotNil(result.failureValue)
|
||||
XCTAssertEqual(result.failureValue?.description, "Can't add a login with an empty hostname.")
|
||||
|
||||
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
|
||||
let protectionSpace = URLProtectionSpace(host: "https://website.com", port: 443, protocol: "https", realm: "Basic Auth", authenticationMethod: "Basic Auth")
|
||||
let bothFormSubmitURLAndRealm = Login.createWithCredential(credential, protectionSpace: protectionSpace)
|
||||
bothFormSubmitURLAndRealm.formSubmitURL = "http://submit.me"
|
||||
bothFormSubmitURLAndRealm.guid = self.login.guid
|
||||
result = logins.updateLoginByGUID(login.guid, new: bothFormSubmitURLAndRealm, significant: true).value
|
||||
XCTAssertNil(result.successValue)
|
||||
XCTAssertNotNil(result.failureValue)
|
||||
XCTAssertEqual(result.failureValue?.description, "Can't add a login with both a httpRealm and formSubmitURL.")
|
||||
|
||||
let noFormSubmitURLOrRealm = Login.createWithHostname("host", username: "username1", password: "password", formSubmitURL: nil)
|
||||
noFormSubmitURLOrRealm.guid = self.login.guid
|
||||
result = logins.updateLoginByGUID(login.guid, new: noFormSubmitURLOrRealm, significant: true).value
|
||||
XCTAssertNil(result.successValue)
|
||||
XCTAssertNotNil(result.failureValue)
|
||||
XCTAssertEqual(result.failureValue?.description, "Can't add a login without a httpRealm or formSubmitURL.")
|
||||
}
|
||||
|
||||
func testSearchLogins() {
|
||||
let loginA = Login.createWithHostname("alphabet.com", username: "username1", password: "password1", formSubmitURL: formSubmitURL)
|
||||
let loginB = Login.createWithHostname("alpha.com", username: "username2", password: "password2", formSubmitURL: formSubmitURL)
|
||||
let loginC = Login.createWithHostname("berry.com", username: "username3", password: "password3", formSubmitURL: formSubmitURL)
|
||||
let loginD = Login.createWithHostname("candle.com", username: "username4", password: "password4", formSubmitURL: formSubmitURL)
|
||||
|
||||
func addLogins() -> Success {
|
||||
addLogin(loginA).succeeded()
|
||||
addLogin(loginB).succeeded()
|
||||
addLogin(loginC).succeeded()
|
||||
addLogin(loginD).succeeded()
|
||||
return succeed()
|
||||
}
|
||||
|
||||
func checkAllLogins() -> Success {
|
||||
return logins.getAllLogins() >>== { results in
|
||||
XCTAssertEqual(results.count, 4)
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
func checkSearchHostnames() -> Success {
|
||||
return logins.searchLoginsWithQuery("pha") >>== { results in
|
||||
XCTAssertEqual(results.count, 2)
|
||||
XCTAssertEqual(results[0]!.hostname, "http://alpha.com")
|
||||
XCTAssertEqual(results[1]!.hostname, "http://alphabet.com")
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
func checkSearchUsernames() -> Success {
|
||||
return logins.searchLoginsWithQuery("username") >>== { results in
|
||||
XCTAssertEqual(results.count, 4)
|
||||
XCTAssertEqual(results[0]!.username, "username2")
|
||||
XCTAssertEqual(results[1]!.username, "username1")
|
||||
XCTAssertEqual(results[2]!.username, "username3")
|
||||
XCTAssertEqual(results[3]!.username, "username4")
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
func checkSearchPasswords() -> Success {
|
||||
return logins.searchLoginsWithQuery("pass") >>== { results in
|
||||
XCTAssertEqual(results.count, 4)
|
||||
XCTAssertEqual(results[0]!.password, "password2")
|
||||
XCTAssertEqual(results[1]!.password, "password1")
|
||||
XCTAssertEqual(results[2]!.password, "password3")
|
||||
XCTAssertEqual(results[3]!.password, "password4")
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertTrue(addLogins().value.isSuccess)
|
||||
|
||||
XCTAssertTrue(checkAllLogins().value.isSuccess)
|
||||
XCTAssertTrue(checkSearchHostnames().value.isSuccess)
|
||||
XCTAssertTrue(checkSearchUsernames().value.isSuccess)
|
||||
XCTAssertTrue(checkSearchPasswords().value.isSuccess)
|
||||
|
||||
XCTAssertTrue(removeAllLogins().value.isSuccess)
|
||||
}
|
||||
|
||||
/*
|
||||
func testAddUseOfLogin() {
|
||||
let expectation = self.self.expectation(description: "Add visit")
|
||||
|
||||
if var usageData = login as? LoginUsageData {
|
||||
usageData.timeCreated = Date.nowMicroseconds()
|
||||
}
|
||||
|
||||
addLogin(login) >>>
|
||||
addUseDelayed(login, time: 1) >>>
|
||||
getLoginDetailsFor(login, expected: login as! LoginUsageData) >>>
|
||||
done(login.protectionSpace, expectation: expectation)
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
*/
|
||||
|
||||
func done(_ expectation: XCTestExpectation) -> () -> Success {
|
||||
return {
|
||||
self.removeAllLogins()
|
||||
>>> self.getLoginsFor(self.login.protectionSpace, expected: [])
|
||||
>>> {
|
||||
expectation.fulfill()
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: These functions are all curried so that we pass arguments, but still chain them below
|
||||
func addLogin(_ login: LoginData) -> Success {
|
||||
log.debug("Add \(login)")
|
||||
return logins.addLogin(login)
|
||||
}
|
||||
|
||||
func updateLogin(_ login: LoginData) -> Success {
|
||||
log.debug("Update \(login)")
|
||||
return logins.updateLoginByGUID(login.guid, new: login, significant: true)
|
||||
}
|
||||
|
||||
func addUseDelayed(_ login: Login, time: UInt32) -> Success {
|
||||
sleep(time)
|
||||
login.timeLastUsed = Date.nowMicroseconds()
|
||||
let res = logins.addUseOfLoginByGUID(login.guid)
|
||||
sleep(time)
|
||||
return res
|
||||
}
|
||||
|
||||
func getLoginsFor(_ protectionSpace: URLProtectionSpace, expected: [LoginData]) -> (() -> Success) {
|
||||
return {
|
||||
log.debug("Get logins for \(protectionSpace)")
|
||||
return self.logins.getLoginsForProtectionSpace(protectionSpace) >>== { results in
|
||||
XCTAssertEqual(expected.count, results.count)
|
||||
for (index, login) in expected.enumerated() {
|
||||
XCTAssertEqual(results[index]!.username!, login.username!)
|
||||
XCTAssertEqual(results[index]!.hostname, login.hostname)
|
||||
XCTAssertEqual(results[index]!.password, login.password)
|
||||
}
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func getLoginDetailsFor(login: LoginData, expected: LoginUsageData) -> (() -> Success) {
|
||||
return {
|
||||
log.debug("Get details for \(login)")
|
||||
let deferred = self.logins.getUsageDataForLogin(login)
|
||||
log.debug("Final result \(deferred)")
|
||||
return deferred >>== { l in
|
||||
log.debug("Got cursor")
|
||||
XCTAssertLessThan(expected.timePasswordChanged - l.timePasswordChanged, 10)
|
||||
XCTAssertLessThan(expected.timeLastUsed - l.timeLastUsed, 10)
|
||||
XCTAssertLessThan(expected.timeCreated - l.timeCreated, 10)
|
||||
return succeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func removeLogin(_ login: LoginData) -> Success {
|
||||
log.debug("Remove \(login)")
|
||||
return logins.removeLoginByGUID(login.guid)
|
||||
}
|
||||
|
||||
func removeAllLogins() -> Success {
|
||||
log.debug("Remove All")
|
||||
// Because we don't want to just mark them as deleted.
|
||||
return self.db.run("DELETE FROM \(TableLoginsMirror)") >>> { self.db.run("DELETE FROM \(TableLoginsLocal)") }
|
||||
}
|
||||
}
|
||||
|
||||
class TestSQLiteLoginsPerf: XCTestCase {
|
||||
var db: BrowserDB!
|
||||
var logins: SQLiteLogins!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
let files = MockFiles()
|
||||
self.db = BrowserDB(filename: "testsqlitelogins.db", schema: LoginsSchema(), files: files)
|
||||
self.logins = SQLiteLogins(db: self.db)
|
||||
}
|
||||
|
||||
func testLoginsSearchMatchOnePerf() {
|
||||
populateTestLogins()
|
||||
|
||||
// Measure time to find one entry amongst the 1000 of them
|
||||
self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) {
|
||||
for _ in 0...5 {
|
||||
self.logins.searchLoginsWithQuery("username500").succeeded()
|
||||
}
|
||||
self.stopMeasuring()
|
||||
}
|
||||
|
||||
XCTAssertTrue(removeAllLogins().value.isSuccess)
|
||||
}
|
||||
|
||||
func testLoginsSearchMatchAllPerf() {
|
||||
populateTestLogins()
|
||||
|
||||
// Measure time to find all matching results
|
||||
self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) {
|
||||
for _ in 0...5 {
|
||||
self.logins.searchLoginsWithQuery("username").succeeded()
|
||||
}
|
||||
self.stopMeasuring()
|
||||
}
|
||||
|
||||
XCTAssertTrue(removeAllLogins().value.isSuccess)
|
||||
}
|
||||
|
||||
func testLoginsGetAllPerf() {
|
||||
populateTestLogins()
|
||||
|
||||
// Measure time to find all matching results
|
||||
self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) {
|
||||
for _ in 0...5 {
|
||||
self.logins.getAllLogins().succeeded()
|
||||
}
|
||||
self.stopMeasuring()
|
||||
}
|
||||
|
||||
XCTAssertTrue(removeAllLogins().value.isSuccess)
|
||||
}
|
||||
|
||||
func populateTestLogins() {
|
||||
for i in 0..<1000 {
|
||||
let login = Login.createWithHostname("website\(i).com", username: "username\(i)", password: "password\(i)", formSubmitURL: "test")
|
||||
addLogin(login).succeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func addLogin(_ login: LoginData) -> Success {
|
||||
return logins.addLogin(login)
|
||||
}
|
||||
|
||||
func removeAllLogins() -> Success {
|
||||
log.debug("Remove All")
|
||||
// Because we don't want to just mark them as deleted.
|
||||
return self.db.run("DELETE FROM \(TableLoginsMirror)") >>> { self.db.run("DELETE FROM \(TableLoginsLocal)") }
|
||||
}
|
||||
}
|
||||
|
||||
class TestSyncableLogins: XCTestCase {
|
||||
var db: BrowserDB!
|
||||
var logins: SQLiteLogins!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let files = MockFiles()
|
||||
self.db = BrowserDB(filename: "testsyncablelogins.db", schema: LoginsSchema(), files: files)
|
||||
self.logins = SQLiteLogins(db: self.db)
|
||||
|
||||
let expectation = self.expectation(description: "Remove all logins.")
|
||||
self.removeAllLogins().upon({ res in expectation.fulfill() })
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func removeAllLogins() -> Success {
|
||||
log.debug("Remove All")
|
||||
// Because we don't want to just mark them as deleted.
|
||||
return self.db.run("DELETE FROM \(TableLoginsMirror)") >>> { self.db.run("DELETE FROM \(TableLoginsLocal)") }
|
||||
}
|
||||
|
||||
func testDiffers() {
|
||||
let guid = "abcdabcdabcd"
|
||||
let host = "http://example.com"
|
||||
let user = "username"
|
||||
let loginA1 = Login(guid: guid, hostname: host, username: user, password: "password1")
|
||||
loginA1.formSubmitURL = "\(host)/form1/"
|
||||
loginA1.usernameField = "afield"
|
||||
|
||||
let loginA2 = Login(guid: guid, hostname: host, username: user, password: "password1")
|
||||
loginA2.formSubmitURL = "\(host)/form1/"
|
||||
loginA2.usernameField = "somefield"
|
||||
|
||||
let loginB = Login(guid: guid, hostname: host, username: user, password: "password2")
|
||||
loginB.formSubmitURL = "\(host)/form1/"
|
||||
|
||||
let loginC = Login(guid: guid, hostname: host, username: user, password: "password")
|
||||
loginC.formSubmitURL = "\(host)/form2/"
|
||||
|
||||
XCTAssert(loginA1.isSignificantlyDifferentFrom(loginB))
|
||||
XCTAssert(loginA1.isSignificantlyDifferentFrom(loginC))
|
||||
XCTAssert(loginA2.isSignificantlyDifferentFrom(loginB))
|
||||
XCTAssert(loginA2.isSignificantlyDifferentFrom(loginC))
|
||||
XCTAssert(!loginA1.isSignificantlyDifferentFrom(loginA2))
|
||||
}
|
||||
|
||||
func testLocalNewStaysNewAndIsRemoved() {
|
||||
let guidA = "abcdabcdabcd"
|
||||
let loginA1 = Login(guid: guidA, hostname: "http://example.com", username: "username", password: "password")
|
||||
loginA1.formSubmitURL = "http://example.com/form/"
|
||||
loginA1.timesUsed = 1
|
||||
XCTAssertTrue((self.logins as BrowserLogins).addLogin(loginA1).value.isSuccess)
|
||||
|
||||
let local1 = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue!
|
||||
XCTAssertNotNil(local1)
|
||||
XCTAssertEqual(local1!.guid, guidA)
|
||||
XCTAssertEqual(local1!.syncStatus, SyncStatus.new)
|
||||
XCTAssertEqual(local1!.timesUsed, 1)
|
||||
|
||||
XCTAssertTrue(self.logins.addUseOfLoginByGUID(guidA).value.isSuccess)
|
||||
|
||||
// It's still new.
|
||||
let local2 = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue!
|
||||
XCTAssertNotNil(local2)
|
||||
XCTAssertEqual(local2!.guid, guidA)
|
||||
XCTAssertEqual(local2!.syncStatus, SyncStatus.new)
|
||||
XCTAssertEqual(local2!.timesUsed, 2)
|
||||
|
||||
// It's removed immediately, because it was never synced.
|
||||
XCTAssertTrue((self.logins as BrowserLogins).removeLoginByGUID(guidA).value.isSuccess)
|
||||
XCTAssertNil(self.logins.getExistingLocalRecordByGUID(guidA).value.successValue!)
|
||||
}
|
||||
|
||||
func testApplyLogin() {
|
||||
let guidA = "abcdabcdabcd"
|
||||
let loginA1 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "password", modified: 1234)
|
||||
loginA1.formSubmitURL = "http://example.com/form/"
|
||||
loginA1.timesUsed = 3
|
||||
|
||||
XCTAssertTrue(self.logins.applyChangedLogin(loginA1).value.isSuccess)
|
||||
|
||||
let local = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue!
|
||||
let mirror = self.logins.getExistingMirrorRecordByGUID(guidA).value.successValue!
|
||||
|
||||
XCTAssertTrue(nil == local)
|
||||
XCTAssertTrue(nil != mirror)
|
||||
|
||||
XCTAssertEqual(mirror!.guid, guidA)
|
||||
XCTAssertFalse(mirror!.isOverridden)
|
||||
XCTAssertEqual(mirror!.serverModified, Timestamp(1234), "Timestamp matches.")
|
||||
XCTAssertEqual(mirror!.timesUsed, 3)
|
||||
XCTAssertTrue(nil == mirror!.httpRealm)
|
||||
XCTAssertTrue(nil == mirror!.passwordField)
|
||||
XCTAssertTrue(nil == mirror!.usernameField)
|
||||
XCTAssertEqual(mirror!.formSubmitURL!, "http://example.com/form/")
|
||||
XCTAssertEqual(mirror!.hostname, "http://example.com")
|
||||
XCTAssertEqual(mirror!.username!, "username")
|
||||
XCTAssertEqual(mirror!.password, "password")
|
||||
|
||||
// Change it.
|
||||
let loginA2 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "newpassword", modified: 2234)
|
||||
loginA2.formSubmitURL = "http://example.com/form/"
|
||||
loginA2.timesUsed = 4
|
||||
|
||||
XCTAssertTrue(self.logins.applyChangedLogin(loginA2).value.isSuccess)
|
||||
let changed = self.logins.getExistingMirrorRecordByGUID(guidA).value.successValue!
|
||||
|
||||
XCTAssertTrue(nil != changed)
|
||||
XCTAssertFalse(changed!.isOverridden)
|
||||
XCTAssertEqual(changed!.serverModified, Timestamp(2234), "Timestamp is new.")
|
||||
XCTAssertEqual(changed!.username!, "username")
|
||||
XCTAssertEqual(changed!.password, "newpassword")
|
||||
XCTAssertEqual(changed!.timesUsed, 4)
|
||||
|
||||
// Change it locally.
|
||||
let preUse = Date.now()
|
||||
XCTAssertTrue(self.logins.addUseOfLoginByGUID(guidA).value.isSuccess)
|
||||
|
||||
let localUsed = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue!
|
||||
let mirrorUsed = self.logins.getExistingMirrorRecordByGUID(guidA).value.successValue!
|
||||
|
||||
XCTAssertNotNil(localUsed)
|
||||
XCTAssertNotNil(mirrorUsed)
|
||||
|
||||
XCTAssertEqual(mirrorUsed!.guid, guidA)
|
||||
XCTAssertEqual(localUsed!.guid, guidA)
|
||||
XCTAssertEqual(mirrorUsed!.password, "newpassword")
|
||||
XCTAssertEqual(localUsed!.password, "newpassword")
|
||||
|
||||
XCTAssertTrue(mirrorUsed!.isOverridden) // It's now overridden.
|
||||
XCTAssertEqual(mirrorUsed!.serverModified, Timestamp(2234), "Timestamp is new.")
|
||||
|
||||
XCTAssertTrue(localUsed!.localModified >= preUse) // Local record is modified.
|
||||
XCTAssertEqual(localUsed!.syncStatus, SyncStatus.synced) // Uses aren't enough to warrant upload.
|
||||
|
||||
// Uses are local until reconciled.
|
||||
XCTAssertEqual(localUsed!.timesUsed, 5)
|
||||
XCTAssertEqual(mirrorUsed!.timesUsed, 4)
|
||||
|
||||
// Change the password and form URL locally.
|
||||
let newLocalPassword = Login(guid: guidA, hostname: "http://example.com", username: "username", password: "yupyup")
|
||||
newLocalPassword.formSubmitURL = "http://example.com/form2/"
|
||||
|
||||
let preUpdate = Date.now()
|
||||
|
||||
// Updates always bump our usages, too.
|
||||
XCTAssertTrue(self.logins.updateLoginByGUID(guidA, new: newLocalPassword, significant: true).value.isSuccess)
|
||||
|
||||
let localAltered = self.logins.getExistingLocalRecordByGUID(guidA).value.successValue!
|
||||
let mirrorAltered = self.logins.getExistingMirrorRecordByGUID(guidA).value.successValue!
|
||||
|
||||
XCTAssertFalse(mirrorAltered!.isSignificantlyDifferentFrom(mirrorUsed!)) // The mirror is unchanged.
|
||||
XCTAssertFalse(mirrorAltered!.isSignificantlyDifferentFrom(localUsed!))
|
||||
XCTAssertTrue(mirrorAltered!.isOverridden) // It's still overridden.
|
||||
|
||||
XCTAssertTrue(localAltered!.isSignificantlyDifferentFrom(localUsed!))
|
||||
XCTAssertEqual(localAltered!.password, "yupyup")
|
||||
XCTAssertEqual(localAltered!.formSubmitURL!, "http://example.com/form2/")
|
||||
XCTAssertTrue(localAltered!.localModified >= preUpdate)
|
||||
XCTAssertEqual(localAltered!.syncStatus, SyncStatus.changed) // Changes are enough to warrant upload.
|
||||
XCTAssertEqual(localAltered!.timesUsed, 6)
|
||||
XCTAssertEqual(mirrorAltered!.timesUsed, 4)
|
||||
}
|
||||
|
||||
func testDeltas() {
|
||||
// Shared.
|
||||
let guidA = "abcdabcdabcd"
|
||||
let loginA1 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "password", modified: 1234)
|
||||
loginA1.timeCreated = 1200
|
||||
loginA1.timeLastUsed = 1234
|
||||
loginA1.timePasswordChanged = 1200
|
||||
loginA1.formSubmitURL = "http://example.com/form/"
|
||||
loginA1.timesUsed = 3
|
||||
|
||||
let a1a1 = loginA1.deltas(from: loginA1)
|
||||
XCTAssertEqual(0, a1a1.nonCommutative.count)
|
||||
XCTAssertEqual(0, a1a1.nonConflicting.count)
|
||||
XCTAssertEqual(0, a1a1.commutative.count)
|
||||
|
||||
let loginA2 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "password", modified: 1235)
|
||||
loginA2.timeCreated = 1200
|
||||
loginA2.timeLastUsed = 1235
|
||||
loginA2.timePasswordChanged = 1200
|
||||
loginA2.timesUsed = 4
|
||||
|
||||
let a1a2 = loginA2.deltas(from: loginA1)
|
||||
|
||||
XCTAssertEqual(2, a1a2.nonCommutative.count)
|
||||
XCTAssertEqual(0, a1a2.nonConflicting.count)
|
||||
XCTAssertEqual(1, a1a2.commutative.count)
|
||||
|
||||
switch a1a2.commutative[0] {
|
||||
case let .timesUsed(increment):
|
||||
XCTAssertEqual(increment, 1)
|
||||
break
|
||||
}
|
||||
switch a1a2.nonCommutative[0] {
|
||||
case let .formSubmitURL(to):
|
||||
XCTAssertNil(to)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
switch a1a2.nonCommutative[1] {
|
||||
case let .timeLastUsed(to):
|
||||
XCTAssertEqual(to, 1235)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
|
||||
let loginA3 = ServerLogin(guid: guidA, hostname: "http://example.com", username: "username", password: "something else", modified: 1280)
|
||||
loginA3.timeCreated = 1200
|
||||
loginA3.timeLastUsed = 1250
|
||||
loginA3.timePasswordChanged = 1250
|
||||
loginA3.formSubmitURL = "http://example.com/form/"
|
||||
loginA3.timesUsed = 5
|
||||
|
||||
let a1a3 = loginA3.deltas(from: loginA1)
|
||||
|
||||
XCTAssertEqual(3, a1a3.nonCommutative.count)
|
||||
XCTAssertEqual(0, a1a3.nonConflicting.count)
|
||||
XCTAssertEqual(1, a1a3.commutative.count)
|
||||
|
||||
switch a1a3.commutative[0] {
|
||||
case let .timesUsed(increment):
|
||||
XCTAssertEqual(increment, 2)
|
||||
break
|
||||
}
|
||||
|
||||
switch a1a3.nonCommutative[0] {
|
||||
case let .password(to):
|
||||
XCTAssertEqual("something else", to)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
switch a1a3.nonCommutative[1] {
|
||||
case let .timeLastUsed(to):
|
||||
XCTAssertEqual(to, 1250)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
switch a1a3.nonCommutative[2] {
|
||||
case let .timePasswordChanged(to):
|
||||
XCTAssertEqual(to, 1250)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
|
||||
// Now apply the deltas to the original record and check that they match!
|
||||
XCTAssertFalse(loginA1.applyDeltas(a1a2).isSignificantlyDifferentFrom(loginA2))
|
||||
XCTAssertFalse(loginA1.applyDeltas(a1a3).isSignificantlyDifferentFrom(loginA3))
|
||||
|
||||
let merged = Login.mergeDeltas(a: (loginA2.serverModified, a1a2), b: (loginA3.serverModified, a1a3))
|
||||
let mCCount = merged.commutative.count
|
||||
let a2CCount = a1a2.commutative.count
|
||||
let a3CCount = a1a3.commutative.count
|
||||
XCTAssertEqual(mCCount, a2CCount + a3CCount)
|
||||
|
||||
let mNCount = merged.nonCommutative.count
|
||||
let a2NCount = a1a2.nonCommutative.count
|
||||
let a3NCount = a1a3.nonCommutative.count
|
||||
XCTAssertLessThanOrEqual(mNCount, a2NCount + a3NCount)
|
||||
XCTAssertGreaterThanOrEqual(mNCount, max(a2NCount, a3NCount))
|
||||
|
||||
let mFCount = merged.nonConflicting.count
|
||||
let a2FCount = a1a2.nonConflicting.count
|
||||
let a3FCount = a1a3.nonConflicting.count
|
||||
XCTAssertLessThanOrEqual(mFCount, a2FCount + a3FCount)
|
||||
XCTAssertGreaterThanOrEqual(mFCount, max(a2FCount, a3FCount))
|
||||
|
||||
switch merged.commutative[0] {
|
||||
case let .timesUsed(increment):
|
||||
XCTAssertEqual(1, increment)
|
||||
}
|
||||
switch merged.commutative[1] {
|
||||
case let .timesUsed(increment):
|
||||
XCTAssertEqual(2, increment)
|
||||
}
|
||||
|
||||
switch merged.nonCommutative[0] {
|
||||
case let .password(to):
|
||||
XCTAssertEqual("something else", to)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
switch merged.nonCommutative[1] {
|
||||
case let .formSubmitURL(to):
|
||||
XCTAssertNil(to)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
switch merged.nonCommutative[2] {
|
||||
case let .timeLastUsed(to):
|
||||
XCTAssertEqual(to, 1250)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
switch merged.nonCommutative[3] {
|
||||
case let .timePasswordChanged(to):
|
||||
XCTAssertEqual(to, 1250)
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected non-commutative login field.")
|
||||
}
|
||||
|
||||
// Applying the merged deltas gives us the expected login.
|
||||
let expected = Login(guid: guidA, hostname: "http://example.com", username: "username", password: "something else")
|
||||
expected.timeCreated = 1200
|
||||
expected.timeLastUsed = 1250
|
||||
expected.timePasswordChanged = 1250
|
||||
expected.formSubmitURL = nil
|
||||
expected.timesUsed = 6
|
||||
|
||||
let applied = loginA1.applyDeltas(merged)
|
||||
XCTAssertFalse(applied.isSignificantlyDifferentFrom(expected))
|
||||
XCTAssertFalse(expected.isSignificantlyDifferentFrom(applied))
|
||||
}
|
||||
|
||||
func testLoginsIsSynced() {
|
||||
let loginA = Login.createWithHostname("alphabet.com", username: "username1", password: "password1")
|
||||
let serverLoginA = ServerLogin(guid: loginA.guid, hostname: "alpha.com", username: "username1", password: "password1", modified: Date.now())
|
||||
|
||||
XCTAssertFalse(logins.hasSyncedLogins().value.successValue ?? true)
|
||||
let _ = logins.addLogin(loginA).value
|
||||
XCTAssertFalse(logins.hasSyncedLogins().value.successValue ?? false)
|
||||
let _ = logins.applyChangedLogin(serverLoginA).value
|
||||
XCTAssertTrue(logins.hasSyncedLogins().value.successValue ?? false)
|
||||
}
|
||||
}
|
||||
1150
mobile/ios/StorageTests/TestSQLiteBookmarks.swift
Normal file
1150
mobile/ios/StorageTests/TestSQLiteBookmarks.swift
Normal file
File diff suppressed because it is too large
Load diff
1625
mobile/ios/StorageTests/TestSQLiteHistory.swift
Normal file
1625
mobile/ios/StorageTests/TestSQLiteHistory.swift
Normal file
File diff suppressed because it is too large
Load diff
333
mobile/ios/StorageTests/TestSQLiteHistoryRecommendations.swift
Normal file
333
mobile/ios/StorageTests/TestSQLiteHistoryRecommendations.swift
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
import Deferred
|
||||
import SDWebImage
|
||||
|
||||
import XCTest
|
||||
|
||||
private let microsecondsPerMinute: UInt64 = 60_000_000 // 1000 * 1000 * 60
|
||||
private let oneHourInMicroseconds: UInt64 = 60 * microsecondsPerMinute
|
||||
private let oneDayInMicroseconds: UInt64 = 24 * oneHourInMicroseconds
|
||||
|
||||
class TestSQLiteHistoryRecommendations: XCTestCase {
|
||||
let files = MockFiles()
|
||||
|
||||
var db: BrowserDB!
|
||||
var prefs: MockProfilePrefs!
|
||||
var history: SQLiteHistory!
|
||||
var bookmarks: MergedSQLiteBookmarks!
|
||||
var metadata: SQLiteMetadata!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files)
|
||||
metadata = SQLiteMetadata(db: db)
|
||||
prefs = MockProfilePrefs()
|
||||
history = SQLiteHistory(db: db, prefs: prefs)
|
||||
bookmarks = MergedSQLiteBookmarks(db: db)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Clear out anything we might have changed on disk
|
||||
history.clearHistory().succeeded()
|
||||
db.run("DELETE FROM \(TablePageMetadata)").succeeded()
|
||||
db.run("DELETE FROM \(TableHighlights)").succeeded()
|
||||
db.run("DELETE FROM \(TableActivityStreamBlocklist)").succeeded()
|
||||
|
||||
SDWebImageManager.shared().imageCache?.clearDisk()
|
||||
SDWebImageManager.shared().imageCache?.clearMemory()
|
||||
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify that we return a non-recent history highlight if:
|
||||
*
|
||||
* 1. We haven't visited the site in the last 30 minutes
|
||||
* 2. We've only visited the site less than or equal to 3 times
|
||||
* 3. The site we visited has a non-empty title
|
||||
*
|
||||
*/
|
||||
func testHistoryHighlights() {
|
||||
let startTime = Date.nowMicroseconds()
|
||||
let oneHourAgo = startTime - oneHourInMicroseconds
|
||||
let fifteenMinutesAgo = startTime - 15 * microsecondsPerMinute
|
||||
|
||||
/*
|
||||
* Site A: 1 visit, 1 hour ago = highlight
|
||||
* Site B: 1 visits, 15 minutes ago = non-highlight
|
||||
* Site C: 3 visits, 1 hour ago = highlight
|
||||
* Site D: 4 visits, 1 hour ago = non-highlight
|
||||
*/
|
||||
let siteA = Site(url: "http://siteA/", title: "A")
|
||||
let siteB = Site(url: "http://siteB/", title: "B")
|
||||
let siteC = Site(url: "http://siteC/", title: "C")
|
||||
let siteD = Site(url: "http://siteD/", title: "D")
|
||||
|
||||
let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link)
|
||||
let siteVisitB1 = SiteVisit(site: siteB, date: fifteenMinutesAgo, type: .link)
|
||||
|
||||
let siteVisitC1 = SiteVisit(site: siteC, date: oneHourAgo + 1, type: .link)
|
||||
let siteVisitC2 = SiteVisit(site: siteC, date: oneHourAgo + 1000, type: .link)
|
||||
let siteVisitC3 = SiteVisit(site: siteC, date: oneHourAgo + 2000, type: .link)
|
||||
|
||||
let siteVisitD1 = SiteVisit(site: siteD, date: oneHourAgo, type: .link)
|
||||
let siteVisitD2 = SiteVisit(site: siteD, date: oneHourAgo + 1000, type: .link)
|
||||
let siteVisitD3 = SiteVisit(site: siteD, date: oneHourAgo + 2000, type: .link)
|
||||
let siteVisitD4 = SiteVisit(site: siteD, date: oneHourAgo + 3000, type: .link)
|
||||
|
||||
history.clearHistory().succeeded()
|
||||
history.addLocalVisit(siteVisitA1).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitB1).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitC1).succeeded()
|
||||
history.addLocalVisit(siteVisitC2).succeeded()
|
||||
history.addLocalVisit(siteVisitC3).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitD1).succeeded()
|
||||
history.addLocalVisit(siteVisitD2).succeeded()
|
||||
history.addLocalVisit(siteVisitD3).succeeded()
|
||||
history.addLocalVisit(siteVisitD4).succeeded()
|
||||
history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded()
|
||||
let highlights = history.getHighlights().value.successValue!
|
||||
XCTAssertEqual(highlights.count, 2)
|
||||
XCTAssertEqual(highlights[0]!.title, "A")
|
||||
XCTAssertEqual(highlights[1]!.title, "C")
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify that we do not return a highlight if
|
||||
* its domain is in the blacklist
|
||||
*
|
||||
*/
|
||||
func testBlacklistHighlights() {
|
||||
let startTime = Date.nowMicroseconds()
|
||||
let oneHourAgo = startTime - oneHourInMicroseconds
|
||||
let fifteenMinutesAgo = startTime - 15 * microsecondsPerMinute
|
||||
|
||||
/*
|
||||
* Site A: 1 visit, 1 hour ago = highlight that is on the blacklist
|
||||
* Site B: 1 visits, 15 minutes ago = non-highlight
|
||||
* Site C: 3 visits, 1 hour ago = highlight that is on the blacklist
|
||||
* Site D: 4 visits, 1 hour ago = non-highlight
|
||||
*/
|
||||
let siteA = Site(url: "http://www.google.com", title: "A")
|
||||
let siteB = Site(url: "http://siteB/", title: "B")
|
||||
let siteC = Site(url: "http://www.search.yahoo.com/", title: "C")
|
||||
let siteD = Site(url: "http://siteD/", title: "D")
|
||||
|
||||
let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link)
|
||||
let siteVisitB1 = SiteVisit(site: siteB, date: fifteenMinutesAgo, type: .link)
|
||||
|
||||
let siteVisitC1 = SiteVisit(site: siteC, date: oneHourAgo, type: .link)
|
||||
let siteVisitC2 = SiteVisit(site: siteC, date: oneHourAgo + 1000, type: .link)
|
||||
let siteVisitC3 = SiteVisit(site: siteC, date: oneHourAgo + 2000, type: .link)
|
||||
|
||||
let siteVisitD1 = SiteVisit(site: siteD, date: oneHourAgo, type: .link)
|
||||
let siteVisitD2 = SiteVisit(site: siteD, date: oneHourAgo + 1000, type: .link)
|
||||
let siteVisitD3 = SiteVisit(site: siteD, date: oneHourAgo + 2000, type: .link)
|
||||
let siteVisitD4 = SiteVisit(site: siteD, date: oneHourAgo + 3000, type: .link)
|
||||
|
||||
history.clearHistory().succeeded()
|
||||
history.addLocalVisit(siteVisitA1).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitB1).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitC1).succeeded()
|
||||
history.addLocalVisit(siteVisitC2).succeeded()
|
||||
history.addLocalVisit(siteVisitC3).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitD1).succeeded()
|
||||
history.addLocalVisit(siteVisitD2).succeeded()
|
||||
history.addLocalVisit(siteVisitD3).succeeded()
|
||||
history.addLocalVisit(siteVisitD4).succeeded()
|
||||
|
||||
history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded()
|
||||
let highlights = history.getHighlights().value.successValue!
|
||||
XCTAssertEqual(highlights.count, 0)
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify that we return the most recent highlight per domain
|
||||
*/
|
||||
func testMostRecentUniqueDomainReturnedInHighlights() {
|
||||
let startTime = Date.nowMicroseconds()
|
||||
let oneHourAgo = startTime - oneHourInMicroseconds
|
||||
let twoHoursAgo = startTime - 2 * oneHourInMicroseconds
|
||||
|
||||
/*
|
||||
* Site A: 1 visit, 1 hour ago = highlight
|
||||
* Site C: 2 visits, 2 hours ago = highlight with the same domain
|
||||
*/
|
||||
let siteA = Site(url: "http://www.foo.com/", title: "A")
|
||||
let siteC = Site(url: "http://m.foo.com/", title: "C")
|
||||
|
||||
let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link)
|
||||
|
||||
let siteVisitC1 = SiteVisit(site: siteC, date: twoHoursAgo, type: .link)
|
||||
let siteVisitC2 = SiteVisit(site: siteC, date: twoHoursAgo + 1000, type: .link)
|
||||
|
||||
history.clearHistory().succeeded()
|
||||
history.addLocalVisit(siteVisitA1).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitC1).succeeded()
|
||||
history.addLocalVisit(siteVisitC2).succeeded()
|
||||
|
||||
history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded()
|
||||
let highlights = history.getHighlights().value.successValue!
|
||||
XCTAssertEqual(highlights.count, 1)
|
||||
XCTAssertEqual(highlights[0]!.title, "A")
|
||||
}
|
||||
|
||||
func testBookmarkHighlights() {
|
||||
history.clearHistory().succeeded()
|
||||
populateForRecommendationCalculations(history, bookmarks: bookmarks, metadata: metadata, historyCount: 10, bookmarkCount: 10)
|
||||
|
||||
let sites = history.getRecentBookmarks(5).value.successValue?.asArray()
|
||||
XCTAssertEqual(sites!.count, 5, "5 bookmarks should have been fetched")
|
||||
sites!.forEach { XCTAssertEqual($0.guid, "bookmark-\(sites!.index(of: $0)!)"); XCTAssertEqual($0.metadata?.description, "Test Description") }
|
||||
}
|
||||
|
||||
func testMetadataReturnedInHighlights() {
|
||||
let startTime = Date.nowMicroseconds()
|
||||
let oneHourAgo = startTime - oneHourInMicroseconds
|
||||
|
||||
let siteA = Site(url: "http://siteA.com", title: "Site A")
|
||||
let siteB = Site(url: "http://siteB.com/", title: "Site B")
|
||||
let siteC = Site(url: "http://siteC.com/", title: "Site C")
|
||||
|
||||
let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link)
|
||||
let siteVisitB1 = SiteVisit(site: siteB, date: oneHourAgo + 1000, type: .link)
|
||||
|
||||
let siteVisitC1 = SiteVisit(site: siteC, date: oneHourAgo, type: .link)
|
||||
let siteVisitC2 = SiteVisit(site: siteC, date: oneHourAgo + 1000, type: .link)
|
||||
let siteVisitC3 = SiteVisit(site: siteC, date: oneHourAgo + 2000, type: .link)
|
||||
|
||||
history.clearHistory().succeeded()
|
||||
history.addLocalVisit(siteVisitA1).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitB1).succeeded()
|
||||
|
||||
history.addLocalVisit(siteVisitC1).succeeded()
|
||||
history.addLocalVisit(siteVisitC2).succeeded()
|
||||
history.addLocalVisit(siteVisitC3).succeeded()
|
||||
|
||||
// add metadata for 2 of the sites
|
||||
let metadata = SQLiteMetadata(db: db)
|
||||
let pageA = PageMetadata(id: nil, siteURL: siteA.url, mediaURL: "http://image.com",
|
||||
title: siteA.title, description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
metadata.storeMetadata(pageA, forPageURL: siteA.url.asURL!, expireAt: Date.now() + 3000).succeeded()
|
||||
let pageB = PageMetadata(id: nil, siteURL: siteB.url, mediaURL: "http://image.com",
|
||||
title: siteB.title, description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
metadata.storeMetadata(pageB, forPageURL: siteB.url.asURL!, expireAt: Date.now() + 3000).succeeded()
|
||||
let pageC = PageMetadata(id: nil, siteURL: siteC.url, mediaURL: "http://image.com",
|
||||
title: siteC.title, description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
metadata.storeMetadata(pageC, forPageURL: siteC.url.asURL!, expireAt: Date.now() + 3000).succeeded()
|
||||
|
||||
history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded()
|
||||
let highlights = history.getHighlights().value.successValue!
|
||||
XCTAssertEqual(highlights.count, 3)
|
||||
|
||||
for highlight in highlights {
|
||||
XCTAssertNotNil(highlight?.metadata)
|
||||
XCTAssertNotNil(highlight?.metadata?.mediaURL)
|
||||
}
|
||||
}
|
||||
|
||||
func testRemoveHighlightForURL() {
|
||||
let startTime = Date.nowMicroseconds()
|
||||
let oneHourAgo = startTime - oneHourInMicroseconds
|
||||
|
||||
let siteA = Site(url: "http://siteA/", title: "A")
|
||||
let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link)
|
||||
|
||||
history.clearHistory().succeeded()
|
||||
history.addLocalVisit(siteVisitA1).succeeded()
|
||||
|
||||
history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded()
|
||||
var highlights = history.getHighlights().value.successValue!
|
||||
XCTAssertEqual(highlights.count, 1)
|
||||
XCTAssertEqual(highlights[0]!.title, "A")
|
||||
|
||||
history.removeHighlightForURL(siteA.url).succeeded()
|
||||
history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded()
|
||||
highlights = history.getHighlights().value.successValue!
|
||||
XCTAssertEqual(highlights.count, 0)
|
||||
}
|
||||
|
||||
func testClearHighlightsCache() {
|
||||
let startTime = Date.nowMicroseconds()
|
||||
let oneHourAgo = startTime - oneHourInMicroseconds
|
||||
|
||||
let siteA = Site(url: "http://siteA/", title: "A")
|
||||
let siteVisitA1 = SiteVisit(site: siteA, date: oneHourAgo, type: .link)
|
||||
|
||||
history.clearHistory().succeeded()
|
||||
history.addLocalVisit(siteVisitA1).succeeded()
|
||||
|
||||
history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded()
|
||||
let highlights = history.getHighlights().value.successValue!
|
||||
XCTAssertEqual(highlights.count, 1)
|
||||
XCTAssertEqual(highlights[0]!.title, "A")
|
||||
}
|
||||
}
|
||||
|
||||
class TestSQLiteHistoryRecommendationsPerf: XCTestCase {
|
||||
func testRecommendationPref() {
|
||||
let files = MockFiles()
|
||||
let db = BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files)
|
||||
let metadata = SQLiteMetadata(db: db)
|
||||
let prefs = MockProfilePrefs()
|
||||
let history = SQLiteHistory(db: db, prefs: prefs)
|
||||
let bookmarks = MergedSQLiteBookmarks(db: db)
|
||||
|
||||
let count = 500
|
||||
|
||||
history.clearHistory().succeeded()
|
||||
populateForRecommendationCalculations(history, bookmarks: bookmarks, metadata: metadata, historyCount: count, bookmarkCount: count)
|
||||
self.measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: true) {
|
||||
for _ in 0...5 {
|
||||
history.repopulate(invalidateTopSites: true, invalidateHighlights: true).succeeded()
|
||||
}
|
||||
self.stopMeasuring()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func populateForRecommendationCalculations(_ history: SQLiteHistory, bookmarks: MergedSQLiteBookmarks, metadata: SQLiteMetadata, historyCount: Int, bookmarkCount: Int) {
|
||||
let baseMillis: UInt64 = baseInstantInMillis - 20000
|
||||
|
||||
for i in 0..<historyCount {
|
||||
let site = Site(url: "http://s\(i)ite\(i)/foo", title: "A \(i)")
|
||||
site.guid = "abc\(i)def"
|
||||
|
||||
history.insertOrUpdatePlace(site.asPlace(), modified: baseMillis).succeeded()
|
||||
for j in 0...20 {
|
||||
let visitTime = advanceMicrosecondTimestamp(baseInstantInMicros, by: (1000000 * i) + (1000 * j))
|
||||
addVisitForSite(site, intoHistory: history, from: .local, atTime: visitTime)
|
||||
addVisitForSite(site, intoHistory: history, from: .remote, atTime: visitTime)
|
||||
}
|
||||
}
|
||||
|
||||
(0..<bookmarkCount).forEach { i in
|
||||
let modifiedTime = advanceMicrosecondTimestamp(baseInstantInMicros, by: (1000000 * i))
|
||||
let bookmarkSite = Site(url: "http://bookmark-\(i)/", title: "\(i) Bookmark")
|
||||
bookmarkSite.guid = "bookmark-\(i)"
|
||||
|
||||
addVisitForSite(bookmarkSite, intoHistory: history, from: .local, atTime: modifiedTime)
|
||||
addVisitForSite(bookmarkSite, intoHistory: history, from: .remote, atTime: modifiedTime)
|
||||
addVisitForSite(bookmarkSite, intoHistory: history, from: .local, atTime: modifiedTime)
|
||||
addVisitForSite(bookmarkSite, intoHistory: history, from: .remote, atTime: modifiedTime)
|
||||
let pageA = PageMetadata(id: nil, siteURL: bookmarkSite.url, mediaURL: "http://image.com",
|
||||
title: bookmarkSite.title, description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
metadata.storeMetadata(pageA, forPageURL: bookmarkSite.url.asURL!, expireAt: Date.now() + 3000).succeeded()
|
||||
bookmarks.local.addToMobileBookmarks(URL(string:"http://bookmark-\(i)/")!, title: "\(i) Bookmark", favicon: nil).succeeded()
|
||||
}
|
||||
}
|
||||
115
mobile/ios/StorageTests/TestSQLiteMetadata.swift
Normal file
115
mobile/ios/StorageTests/TestSQLiteMetadata.swift
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
import Deferred
|
||||
|
||||
import XCTest
|
||||
|
||||
class TestSQLiteMetadata: XCTestCase {
|
||||
let files = MockFiles()
|
||||
var db: BrowserDB!
|
||||
var metadata: SQLiteMetadata!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
self.db = BrowserDB(filename: "foo.db", schema: BrowserSchema(), files: self.files)
|
||||
self.metadata = SQLiteMetadata(db: db)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
|
||||
removeAllMetadata(self.db).succeeded()
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testInsertMetadata() {
|
||||
let site = "http://test.com"
|
||||
|
||||
let page = PageMetadata(id: nil, siteURL: site, mediaURL: "http://image.com",
|
||||
title: "Test", description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
self.metadata.storeMetadata(page, forPageURL: site.asURL!, expireAt: Date.now() + 3000).succeeded()
|
||||
let results = metadataFromDB(self.db).value.successValue!
|
||||
XCTAssertEqual(results.count, 1)
|
||||
let metadata = results[0]!
|
||||
XCTAssertEqual(metadata.siteURL, site)
|
||||
}
|
||||
|
||||
func testDuplicateCacheKeyInsert() {
|
||||
let siteA = "http://test.com/site/A"
|
||||
let siteB = "http://test.com/site/B"
|
||||
|
||||
let metadataA1 = PageMetadata(id: nil, siteURL: siteA, mediaURL: nil,
|
||||
title: "First Visit", description: "", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
let metadataB = PageMetadata(id: nil, siteURL: siteB, mediaURL: "http://image.com",
|
||||
title: "Test", description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
let metadataA2 = PageMetadata(id: nil, siteURL: siteA, mediaURL: "http://image.com",
|
||||
title: "Second Visit", description: "A new description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
|
||||
self.metadata.storeMetadata(metadataA1, forPageURL: siteA.asURL!, expireAt: Date.now() + 3000).succeeded()
|
||||
|
||||
let initialResults = metadataFromDB(self.db).value.successValue!
|
||||
XCTAssertEqual(initialResults.count, 1)
|
||||
|
||||
let initialA = initialResults[0]!
|
||||
XCTAssertEqual(initialA.siteURL, siteA)
|
||||
XCTAssertEqual(initialA.title, "First Visit")
|
||||
XCTAssertEqual(initialA.description, "")
|
||||
XCTAssertNil(initialA.mediaURL)
|
||||
|
||||
self.metadata.storeMetadata(metadataB, forPageURL: siteB.asURL!, expireAt: Date.now() + 3000).succeeded()
|
||||
self.metadata.storeMetadata(metadataA2, forPageURL: siteA.asURL!, expireAt: Date.now() + 3000).succeeded()
|
||||
|
||||
let results = metadataFromDB(self.db).value.successValue!
|
||||
|
||||
// Should only have 2 since we upsert
|
||||
XCTAssertEqual(results.count, 2)
|
||||
|
||||
let resultA = results[1]!
|
||||
let resultB = results[0]!
|
||||
|
||||
XCTAssertEqual(resultA.siteURL, siteA)
|
||||
XCTAssertEqual(resultB.siteURL, siteB)
|
||||
|
||||
XCTAssertEqual(resultA.title, "Second Visit")
|
||||
XCTAssertEqual(resultA.description, "A new description")
|
||||
XCTAssertEqual(resultA.mediaURL!, "http://image.com")
|
||||
}
|
||||
|
||||
func testExpirationPurging() {
|
||||
let baseTime = Date.now()
|
||||
let siteA = "http://test.com/site/A"
|
||||
let metadataA = PageMetadata(id: nil, siteURL: siteA, mediaURL: nil,
|
||||
title: "Test", description: "Test Description", type: nil, providerName: nil, mediaDataURI: nil, cacheImages: false)
|
||||
// Set expiration to base
|
||||
self.metadata.storeMetadata(metadataA, forPageURL: siteA.asURL!, expireAt: baseTime - 1000).succeeded()
|
||||
self.metadata.deleteExpiredMetadata().succeeded()
|
||||
|
||||
let results = metadataFromDB(self.db).value.successValue!
|
||||
XCTAssertEqual(results.count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private func metadataFromDB(_ db: BrowserDB) -> Deferred<Maybe<Cursor<PageMetadata>>> {
|
||||
let sql = "SELECT * FROM \(TablePageMetadata)"
|
||||
return db.runQuery(sql, args: nil, factory: pageMetadataFactory)
|
||||
}
|
||||
|
||||
private func removeAllMetadata(_ db: BrowserDB) -> Success {
|
||||
return db.run("DELETE FROM \(TablePageMetadata)")
|
||||
}
|
||||
|
||||
private func pageMetadataFactory(_ row: SDRow) -> PageMetadata {
|
||||
let id = row["id"] as! Int
|
||||
let siteURL = row["site_url"] as! String
|
||||
let mediaURL = row["media_url"] as? String
|
||||
let title = row["title"] as? String
|
||||
let description = row["description"] as? String
|
||||
let type = row["type"] as? String
|
||||
let providerName = row["provider_name"] as? String
|
||||
return PageMetadata(id: id, siteURL: siteURL, mediaURL: mediaURL, title: title,
|
||||
description: description, type: type, providerName: providerName, mediaDataURI: nil, cacheImages: false)
|
||||
}
|
||||
303
mobile/ios/StorageTests/TestSQLiteRemoteClientsAndTabs.swift
Normal file
303
mobile/ios/StorageTests/TestSQLiteRemoteClientsAndTabs.swift
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
import Deferred
|
||||
|
||||
import XCTest
|
||||
|
||||
open class MockRemoteClientsAndTabs: RemoteClientsAndTabs {
|
||||
open let clientsAndTabs: [ClientAndTabs]
|
||||
|
||||
public init() {
|
||||
let now = Date.now()
|
||||
let client1GUID = Bytes.generateGUID()
|
||||
let client2GUID = Bytes.generateGUID()
|
||||
let u11 = URL(string: "http://test.com/test1")!
|
||||
let tab11 = RemoteTab(clientGUID: client1GUID, URL: u11, title: "Test 1", history: [ ], lastUsed: (now - OneMinuteInMilliseconds), icon: nil)
|
||||
|
||||
let u12 = URL(string: "http://test.com/test2")!
|
||||
let tab12 = RemoteTab(clientGUID: client1GUID, URL: u12, title: "Test 2", history: [], lastUsed: (now - OneHourInMilliseconds), icon: nil)
|
||||
|
||||
let tab21 = RemoteTab(clientGUID: client2GUID, URL: u11, title: "Test 1", history: [], lastUsed: (now - OneDayInMilliseconds), icon: nil)
|
||||
|
||||
let u22 = URL(string: "http://different.com/test2")!
|
||||
let tab22 = RemoteTab(clientGUID: client2GUID, URL: u22, title: "Different Test 2", history: [], lastUsed: now + OneHourInMilliseconds, icon: nil)
|
||||
|
||||
let client1 = RemoteClient(guid: client1GUID, name: "Test client 1", modified: (now - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: "55.0.1", fxaDeviceId: "fxa1")
|
||||
let client2 = RemoteClient(guid: client2GUID, name: "Test client 2", modified: (now - OneHourInMilliseconds), type: "desktop", formfactor: "laptop", os: "Darwin", version: "55.0.1", fxaDeviceId: "fxa2")
|
||||
|
||||
let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (now - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: "55.0.1", fxaDeviceId: "fxa3")
|
||||
let localUrl1 = URL(string: "http://test.com/testlocal1")!
|
||||
let localTab1 = RemoteTab(clientGUID: nil, URL: localUrl1, title: "Local test 1", history: [], lastUsed: (now - OneMinuteInMilliseconds), icon: nil)
|
||||
let localUrl2 = URL(string: "http://test.com/testlocal2")!
|
||||
let localTab2 = RemoteTab(clientGUID: nil, URL: localUrl2, title: "Local test 2", history: [], lastUsed: (now - OneMinuteInMilliseconds), icon: nil)
|
||||
|
||||
// Tabs are ordered most-recent-first.
|
||||
self.clientsAndTabs = [ClientAndTabs(client: client1, tabs: [tab11, tab12]),
|
||||
ClientAndTabs(client: client2, tabs: [tab22, tab21]),
|
||||
ClientAndTabs(client: localClient, tabs: [localTab1, localTab2])]
|
||||
}
|
||||
|
||||
open func onRemovedAccount() -> Success {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
open func wipeClients() -> Success {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
open func wipeRemoteTabs() -> Deferred<Maybe<()>> {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
open func wipeTabs() -> Success {
|
||||
return succeed()
|
||||
}
|
||||
|
||||
open func insertOrUpdateClients(_ clients: [RemoteClient]) -> Deferred<Maybe<Int>> {
|
||||
return deferMaybe(0)
|
||||
}
|
||||
|
||||
open func insertOrUpdateClient(_ client: RemoteClient) -> Deferred<Maybe<Int>> {
|
||||
return deferMaybe(0)
|
||||
}
|
||||
|
||||
open func insertOrUpdateTabs(_ tabs: [RemoteTab]) -> Deferred<Maybe<Int>> {
|
||||
return insertOrUpdateTabsForClientGUID(nil, tabs: [RemoteTab]())
|
||||
}
|
||||
|
||||
open func insertOrUpdateTabsForClientGUID(_ clientGUID: String?, tabs: [RemoteTab]) -> Deferred<Maybe<Int>> {
|
||||
return deferMaybe(-1)
|
||||
}
|
||||
|
||||
open func getClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>> {
|
||||
return deferMaybe(self.clientsAndTabs)
|
||||
}
|
||||
|
||||
open func getClients() -> Deferred<Maybe<[RemoteClient]>> {
|
||||
return deferMaybe(self.clientsAndTabs.map { $0.client })
|
||||
}
|
||||
|
||||
public func getClient(guid: GUID) -> Deferred<Maybe<RemoteClient?>> {
|
||||
return deferMaybe(self.clientsAndTabs.find { clientAndTabs in
|
||||
return clientAndTabs.client.guid == guid
|
||||
}?.client)
|
||||
}
|
||||
|
||||
public func getClient(fxaDeviceId: GUID) -> Deferred<Maybe<RemoteClient?>> {
|
||||
return deferMaybe(self.clientsAndTabs.find { clientAndTabs in
|
||||
return clientAndTabs.client.fxaDeviceId == fxaDeviceId
|
||||
}?.client)
|
||||
}
|
||||
|
||||
open func getClientWithId(_ clientID: GUID) -> Deferred<Maybe<RemoteClient?>> {
|
||||
return getClient(guid: clientID)
|
||||
}
|
||||
|
||||
open func getClientGUIDs() -> Deferred<Maybe<Set<GUID>>> {
|
||||
return deferMaybe(Set<GUID>(optFilter(self.clientsAndTabs.map { $0.client.guid })))
|
||||
}
|
||||
|
||||
open func getTabsForClientWithGUID(_ guid: GUID?) -> Deferred<Maybe<[RemoteTab]>> {
|
||||
return deferMaybe(optFilter(self.clientsAndTabs.map { $0.client.guid == guid ? $0.tabs : nil })[0])
|
||||
}
|
||||
|
||||
open func deleteClient(guid: GUID) -> Success { return succeed() }
|
||||
|
||||
open func deleteCommands() -> Success { return succeed() }
|
||||
open func deleteCommands(_ clientGUID: GUID) -> Success { return succeed() }
|
||||
|
||||
open func getCommands() -> Deferred<Maybe<[GUID: [SyncCommand]]>> { return deferMaybe([GUID: [SyncCommand]]()) }
|
||||
|
||||
open func insertCommand(_ command: SyncCommand, forClients clients: [RemoteClient]) -> Deferred<Maybe<Int>> { return deferMaybe(0) }
|
||||
open func insertCommands(_ commands: [SyncCommand], forClients clients: [RemoteClient]) -> Deferred<Maybe<Int>> { return deferMaybe(0) }
|
||||
}
|
||||
|
||||
func removeLocalClient(_ a: ClientAndTabs) -> Bool {
|
||||
return a.client.guid != nil
|
||||
}
|
||||
|
||||
func byGUID(_ a: ClientAndTabs, b: ClientAndTabs) -> Bool {
|
||||
guard let aGUID = a.client.guid, let bGUID = b.client.guid else {
|
||||
return false
|
||||
}
|
||||
return aGUID < bGUID
|
||||
}
|
||||
|
||||
func byURL(_ a: RemoteTab, b: RemoteTab) -> Bool {
|
||||
return a.URL.absoluteString < b.URL.absoluteString
|
||||
}
|
||||
|
||||
class SQLRemoteClientsAndTabsTests: XCTestCase {
|
||||
var clientsAndTabs: SQLiteRemoteClientsAndTabs!
|
||||
|
||||
lazy var clients: [ClientAndTabs] = MockRemoteClientsAndTabs().clientsAndTabs
|
||||
|
||||
override func setUp() {
|
||||
let files = MockFiles()
|
||||
do {
|
||||
try files.remove("browser.db")
|
||||
} catch _ {
|
||||
}
|
||||
clientsAndTabs = SQLiteRemoteClientsAndTabs(db: BrowserDB(filename: "browser.db", schema: BrowserSchema(), files: files))
|
||||
}
|
||||
|
||||
func testInsertGetClear() {
|
||||
// Insert some test data.
|
||||
var remoteDevicesToInsert: [RemoteDevice] = []
|
||||
// Filter the local client from mock test data.
|
||||
let remoteClients = clients.filter(removeLocalClient)
|
||||
for c in remoteClients {
|
||||
let e = self.expectation(description: "Insert.")
|
||||
clientsAndTabs.insertOrUpdateClient(c.client).upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
e.fulfill()
|
||||
}
|
||||
let remoteDevice = RemoteDevice(id: c.client.fxaDeviceId!, name: "FxA Device", type: "desktop", isCurrentDevice: false, lastAccessTime: 12345678)
|
||||
remoteDevicesToInsert.append(remoteDevice)
|
||||
clientsAndTabs.insertOrUpdateTabsForClientGUID(c.client.guid, tabs: c.tabs).succeeded()
|
||||
}
|
||||
_ = clientsAndTabs.replaceRemoteDevices(remoteDevicesToInsert).succeeded()
|
||||
|
||||
let f = self.expectation(description: "Get after insert.")
|
||||
clientsAndTabs.getClientsAndTabs().upon {
|
||||
if let got = $0.successValue {
|
||||
let expected = remoteClients.sorted(by: byGUID)
|
||||
let actual = got.sorted(by: byGUID)
|
||||
|
||||
// This comparison will fail if the order of the tabs changes. We sort the result
|
||||
// as part of the DB query, so it's not actively sorted in Swift.
|
||||
XCTAssertEqual(expected, actual)
|
||||
} else {
|
||||
XCTFail("Expected clients!")
|
||||
}
|
||||
f.fulfill()
|
||||
}
|
||||
|
||||
// Update the test data with a client with new tabs, and one with no tabs.
|
||||
let client0NewTabs = clients[1].tabs.map { $0.withClientGUID(self.clients[0].client.guid) }
|
||||
let client1NewTabs: [RemoteTab] = []
|
||||
let expected = [
|
||||
ClientAndTabs(client: clients[0].client, tabs: client0NewTabs),
|
||||
ClientAndTabs(client: clients[1].client, tabs: client1NewTabs),
|
||||
].sorted(by: byGUID)
|
||||
|
||||
func doUpdate(_ guid: String?, tabs: [RemoteTab]) {
|
||||
let g0 = self.expectation(description: "Update client: \(guid ?? "nil").")
|
||||
clientsAndTabs.insertOrUpdateTabsForClientGUID(guid, tabs: tabs).upon {
|
||||
if let rowID = $0.successValue {
|
||||
XCTAssertTrue(rowID > -1)
|
||||
} else {
|
||||
XCTFail("Didn't successfully update.")
|
||||
}
|
||||
g0.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
doUpdate(clients[0].client.guid, tabs: client0NewTabs)
|
||||
doUpdate(clients[1].client.guid, tabs: client1NewTabs)
|
||||
// Also update the local tabs list. It should still not appear in the expected tabs below.
|
||||
doUpdate(clients[2].client.guid, tabs: client1NewTabs)
|
||||
|
||||
let h = self.expectation(description: "Get after update.")
|
||||
clientsAndTabs.getClientsAndTabs().upon {
|
||||
if let clients = $0.successValue {
|
||||
XCTAssertEqual(expected, clients.sorted(by: byGUID))
|
||||
} else {
|
||||
XCTFail("Expected clients!")
|
||||
}
|
||||
h.fulfill()
|
||||
}
|
||||
|
||||
// Now clear everything, and verify we have no clients or tabs whatsoever.
|
||||
let i = self.expectation(description: "Clear.")
|
||||
clientsAndTabs.clear().upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
i.fulfill()
|
||||
}
|
||||
|
||||
let j = self.expectation(description: "Get after clear.")
|
||||
clientsAndTabs.getClientsAndTabs().upon {
|
||||
if let clients = $0.successValue {
|
||||
XCTAssertEqual(0, clients.count)
|
||||
} else {
|
||||
XCTFail("Expected clients!")
|
||||
}
|
||||
j.fulfill()
|
||||
}
|
||||
|
||||
self.waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
|
||||
func testGetTabsForClient() {
|
||||
for c in clients {
|
||||
let e = self.expectation(description: "Insert.")
|
||||
clientsAndTabs.insertOrUpdateClient(c.client).upon {
|
||||
XCTAssertTrue($0.isSuccess)
|
||||
e.fulfill()
|
||||
}
|
||||
clientsAndTabs.insertOrUpdateTabsForClientGUID(c.client.guid, tabs: c.tabs).succeeded()
|
||||
}
|
||||
|
||||
let e = self.expectation(description: "Get after insert.")
|
||||
let ct = clients[0]
|
||||
clientsAndTabs.getTabsForClientWithGUID(ct.client.guid).upon {
|
||||
if let got = $0.successValue {
|
||||
// This comparison will fail if the order of the tabs changes. We sort the result
|
||||
// as part of the DB query, so it's not actively sorted in Swift.
|
||||
XCTAssertEqual(ct.tabs.count, got.count)
|
||||
XCTAssertEqual(ct.tabs.sorted(by: byURL), got.sorted(by: byURL))
|
||||
} else {
|
||||
XCTFail("Expected tabs!")
|
||||
}
|
||||
e.fulfill()
|
||||
}
|
||||
|
||||
let f = self.expectation(description: "Get after insert.")
|
||||
let localClient = clients[0]
|
||||
clientsAndTabs.getTabsForClientWithGUID(localClient.client.guid).upon {
|
||||
if let got = $0.successValue {
|
||||
// This comparison will fail if the order of the tabs changes. We sort the result
|
||||
// as part of the DB query, so it's not actively sorted in Swift.
|
||||
XCTAssertEqual(localClient.tabs.count, got.count)
|
||||
XCTAssertEqual(localClient.tabs.sorted(by: byURL), got.sorted(by: byURL))
|
||||
} else {
|
||||
XCTFail("Expected tabs!")
|
||||
}
|
||||
f.fulfill()
|
||||
}
|
||||
|
||||
self.waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
|
||||
func remoteDeviceFactory(_ row: SDRow) -> RemoteDevice {
|
||||
return RemoteDevice(
|
||||
id: row["guid"] as? String,
|
||||
name: row["name"] as! String,
|
||||
type: row["type"] as? String,
|
||||
isCurrentDevice: row["is_current_device"] as! Int > 0,
|
||||
lastAccessTime: row["last_access_time"] as? Timestamp)
|
||||
}
|
||||
|
||||
func testReplaceRemoteDevices() {
|
||||
let device1 = RemoteDevice(id: "fx1", name: "Device 1", type: "mobile", isCurrentDevice: false, lastAccessTime: 12345678)
|
||||
let device2 = RemoteDevice(id: "fx2", name: "Device 2 (local)", type: "desktop", isCurrentDevice: true, lastAccessTime: nil)
|
||||
let device3 = RemoteDevice(id: nil, name: "Device 3 (fauly)", type: "desktop", isCurrentDevice: false, lastAccessTime: 12345678)
|
||||
let device4 = RemoteDevice(id: "fx4", name: "Device 4 (fauly)", type: nil, isCurrentDevice: false, lastAccessTime: 12345678)
|
||||
|
||||
_ = clientsAndTabs.replaceRemoteDevices([device1, device2, device3, device4]).succeeded()
|
||||
|
||||
let devices = clientsAndTabs.db.runQuery("SELECT * FROM \(TableRemoteDevices)", args: nil, factory: remoteDeviceFactory).value.successValue!.asArray()
|
||||
XCTAssertEqual(devices.count, 1) // Fauly devices + local device were not inserted.
|
||||
|
||||
let device5 = RemoteDevice(id: "fx5", name: "Device 5", type: "mobile", isCurrentDevice: false, lastAccessTime: 12345678)
|
||||
_ = clientsAndTabs.replaceRemoteDevices([device5]).succeeded()
|
||||
|
||||
let newDevices = clientsAndTabs.db.runQuery("SELECT * FROM \(TableRemoteDevices)", args: nil, factory: remoteDeviceFactory).value.successValue!.asArray()
|
||||
XCTAssertEqual(newDevices.count, 1) // replaceRemoteDevices wipes the whole list before inserting.
|
||||
}
|
||||
}
|
||||
220
mobile/ios/StorageTests/TestSwiftData.swift
Normal file
220
mobile/ios/StorageTests/TestSwiftData.swift
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
/* 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
|
||||
@testable import Storage
|
||||
|
||||
import XCTest
|
||||
|
||||
// TODO: rewrite this test to not use BrowserSchema. It used to use HistoryTable…
|
||||
class TestSwiftData: XCTestCase {
|
||||
var swiftData: SwiftData?
|
||||
var urlCounter = 1
|
||||
var testDB: String!
|
||||
|
||||
override func setUp() {
|
||||
let files = MockFiles()
|
||||
do {
|
||||
try files.remove("testSwiftData.db")
|
||||
} catch _ {
|
||||
}
|
||||
testDB = (try! (files.getAndEnsureDirectory() as NSString)).appendingPathComponent("testSwiftData.db")
|
||||
swiftData = SwiftData(filename: testDB, schema: BrowserSchema(), files: files)
|
||||
let table = BrowserSchema()
|
||||
|
||||
// Ensure static flags match expected values.
|
||||
XCTAssert(SwiftData.ReuseConnections, "Reusing database connections")
|
||||
XCTAssert(SwiftData.EnableWAL, "WAL enabled")
|
||||
|
||||
XCTAssertNil(addSite(table, url: "http://url0", title: "title0"), "Added url0.")
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Restore static flags to their default values.
|
||||
SwiftData.ReuseConnections = true
|
||||
SwiftData.EnableWAL = true
|
||||
}
|
||||
|
||||
/*
|
||||
// These two tests broke after pull #427.
|
||||
func testNoWALOrConnectionReuse() {
|
||||
SwiftData.EnableWAL = false
|
||||
SwiftData.ReuseConnections = false
|
||||
var error = writeDuringRead()
|
||||
XCTAssertEqual(error!.code, 5, "Got 'database is locked' error")
|
||||
}
|
||||
|
||||
func testNoConnectionReuse() {
|
||||
SwiftData.EnableWAL = true
|
||||
SwiftData.ReuseConnections = false
|
||||
var error = writeDuringRead()
|
||||
XCTAssertNotNil(error, "Expected error during write.")
|
||||
XCTAssertEqual(error?.code ?? 0, 8, "Got 'attempt to write to read-only database' error")
|
||||
}
|
||||
*/
|
||||
|
||||
func testNoWAL() {
|
||||
SwiftData.EnableWAL = false
|
||||
SwiftData.ReuseConnections = true
|
||||
let error = writeDuringRead()
|
||||
XCTAssertNil(error, "Insertion succeeded")
|
||||
}
|
||||
|
||||
func testDefaultSettings() {
|
||||
SwiftData.EnableWAL = true
|
||||
SwiftData.ReuseConnections = true
|
||||
let error = writeDuringRead()
|
||||
XCTAssertNil(error, "Insertion succeeded")
|
||||
}
|
||||
|
||||
func testBusyTimeout() {
|
||||
SwiftData.EnableWAL = false
|
||||
SwiftData.ReuseConnections = false
|
||||
let error = writeDuringRead(closeTimeout: 1)
|
||||
XCTAssertNil(error, "Insertion succeeded")
|
||||
}
|
||||
|
||||
func testFilledCursor() {
|
||||
SwiftData.ReuseConnections = false
|
||||
SwiftData.EnableWAL = false
|
||||
XCTAssertNil(writeDuringRead(true), "Insertion succeeded")
|
||||
}
|
||||
|
||||
fileprivate func writeDuringRead(_ safeQuery: Bool = false, closeTimeout: UInt64? = nil) -> MaybeErrorType? {
|
||||
|
||||
// Query the database and hold the cursor.
|
||||
var c: Cursor<SDRow>!
|
||||
let result = swiftData!.withConnection(SwiftData.Flags.readOnly) { db -> Void in
|
||||
if safeQuery {
|
||||
c = db.executeQuery("SELECT * FROM history", factory: { $0 })
|
||||
} else {
|
||||
c = db.executeQueryUnsafe("SELECT * FROM history", factory: { $0 }, withArgs: nil)
|
||||
}
|
||||
return ()
|
||||
}
|
||||
|
||||
XCTAssertNil(result.value.failureValue, "Queried database")
|
||||
|
||||
// If we have a live cursor, this will step to the first result.
|
||||
// Stepping through a prepared statement without resetting it will lock the connection.
|
||||
let _ = c[0]
|
||||
|
||||
// Close the cursor after a delay if there's a close timeout set.
|
||||
if let closeTimeout = closeTimeout {
|
||||
let queue = DispatchQueue(label: "cursor timeout queue", attributes: [])
|
||||
queue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(closeTimeout * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {
|
||||
c.close()
|
||||
}
|
||||
}
|
||||
|
||||
defer { urlCounter += 1 }
|
||||
return addSite(BrowserSchema(), url: "http://url/\(urlCounter)", title: "title\(urlCounter)")
|
||||
}
|
||||
|
||||
fileprivate func addSite(_ table: BrowserSchema, url: String, title: String) -> MaybeErrorType? {
|
||||
let result = swiftData!.withConnection(SwiftData.Flags.readWrite) { connection -> Void in
|
||||
let args: Args = [Bytes.generateGUID(), url, title]
|
||||
try connection.executeChange("INSERT INTO history (guid, url, title, is_deleted, should_upload) VALUES (?, ?, ?, 0, 0)", withArgs: args)
|
||||
}
|
||||
|
||||
return result.value.failureValue
|
||||
}
|
||||
|
||||
func testEncrypt() {
|
||||
// XXX: Something is holding an open connection to the normal database, making it impossible
|
||||
// to change its encryption. This kills it so that we can move on.
|
||||
let files = MockFiles()
|
||||
do {
|
||||
try files.remove("testSwiftData.db")
|
||||
} catch _ {
|
||||
}
|
||||
let path = testDB
|
||||
func verifyData(_ swiftData: SwiftData) -> MaybeErrorType? {
|
||||
let resultDeferred = swiftData.withConnection(SwiftData.Flags.readOnly) { db -> Void in
|
||||
return ()
|
||||
}
|
||||
return resultDeferred.value.failureValue
|
||||
}
|
||||
|
||||
XCTAssertNotNil(SwiftData(filename: path!, schema: BrowserSchema(), files: files), "Connected to unencrypted database")
|
||||
|
||||
// Encrypt the database.
|
||||
XCTAssertNil(verifyData(SwiftData(filename: path!, key: "Secret", schema: BrowserSchema(), files: files)), "Encrypted database")
|
||||
|
||||
// Now change the encryption key.
|
||||
XCTAssertNil(verifyData(SwiftData(filename: path!, key: "Secret2", prevKey: "Secret", schema: BrowserSchema(), files: files)), "Re-encrypted database")
|
||||
|
||||
// Changing the encryption without the prevKey should fail.
|
||||
XCTAssertNotNil(verifyData(SwiftData(filename: path!, schema: BrowserSchema(), files: files)), "Failed decrypting database")
|
||||
|
||||
// Now remove the encryption key.
|
||||
XCTAssertNil(verifyData(SwiftData(filename: path!, prevKey: "Secret2", schema: BrowserSchema(), files: files)), "Decrypted database")
|
||||
}
|
||||
|
||||
func testNulls() {
|
||||
guard let db = swiftData else {
|
||||
XCTFail("DB not open")
|
||||
return
|
||||
}
|
||||
db.withConnection(SwiftData.Flags.readWriteCreate) { db in
|
||||
try! db.executeChange("CREATE TABLE foo ( bar TEXT, baz INTEGER )")
|
||||
try! db.executeChange("INSERT INTO foo VALUES (NULL, 1), ('here', 2)")
|
||||
let shouldBeString = db.executeQuery("SELECT bar FROM foo WHERE baz = 2", factory: { (row) in row["bar"] }).asArray()[0]
|
||||
guard let s = shouldBeString as? String else {
|
||||
XCTFail("Couldn't cast.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(s, "here")
|
||||
|
||||
let shouldBeNull = db.executeQuery("SELECT bar FROM foo WHERE baz = 1", factory: { (row) in row["bar"] }).asArray()[0]
|
||||
XCTAssertNil(shouldBeNull as? String)
|
||||
XCTAssertNil(shouldBeNull)
|
||||
}.succeeded()
|
||||
}
|
||||
|
||||
func testArrayCursor() {
|
||||
let data = ["One", "Two", "Three"]
|
||||
let t = ArrayCursor<String>(data: data)
|
||||
|
||||
// Test subscript access
|
||||
XCTAssertNil(t[-1], "Subscript -1 returns nil")
|
||||
XCTAssertEqual(t[0]!, "One", "Subscript zero returns the correct data")
|
||||
XCTAssertEqual(t[1]!, "Two", "Subscript one returns the correct data")
|
||||
XCTAssertEqual(t[2]!, "Three", "Subscript two returns the correct data")
|
||||
XCTAssertNil(t[3], "Subscript three returns nil")
|
||||
|
||||
// Test status data with default initializer
|
||||
XCTAssertEqual(t.status, CursorStatus.success, "Cursor as correct status")
|
||||
XCTAssertEqual(t.statusMessage, "Success", "Cursor as correct status message")
|
||||
XCTAssertEqual(t.count, 3, "Cursor as correct size")
|
||||
|
||||
// Test generator access
|
||||
var i = 0
|
||||
for s in t {
|
||||
XCTAssertEqual(s!, data[i], "Subscript zero returns the correct data")
|
||||
i += 1
|
||||
}
|
||||
|
||||
// Test creating a failed cursor
|
||||
let t2 = ArrayCursor<String>(data: data, status: CursorStatus.failure, statusMessage: "Custom status message")
|
||||
XCTAssertEqual(t2.status, CursorStatus.failure, "Cursor as correct status")
|
||||
XCTAssertEqual(t2.statusMessage, "Custom status message", "Cursor as correct status message")
|
||||
XCTAssertEqual(t2.count, 0, "Cursor as correct size")
|
||||
|
||||
// Test subscript access return nil for a failed cursor
|
||||
XCTAssertNil(t2[0], "Subscript zero returns nil if failure")
|
||||
XCTAssertNil(t2[1], "Subscript one returns nil if failure")
|
||||
XCTAssertNil(t2[2], "Subscript two returns nil if failure")
|
||||
XCTAssertNil(t2[3], "Subscript three returns nil if failure")
|
||||
|
||||
// Test that generator doesn't work with failed cursors
|
||||
var ran = false
|
||||
for s in t2 {
|
||||
print("Got \(s ?? "nil")", terminator: "\n")
|
||||
ran = true
|
||||
}
|
||||
XCTAssertFalse(ran, "for...in didn't run for failed cursor")
|
||||
}
|
||||
}
|
||||
BIN
mobile/ios/StorageTests/fixtures/v33.db
Normal file
BIN
mobile/ios/StorageTests/fixtures/v33.db
Normal file
Binary file not shown.
BIN
mobile/ios/StorageTests/testcert1.pem
Normal file
BIN
mobile/ios/StorageTests/testcert1.pem
Normal file
Binary file not shown.
BIN
mobile/ios/StorageTests/testcert2.pem
Normal file
BIN
mobile/ios/StorageTests/testcert2.pem
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue