Dactyloidae iOS initial commit

This commit is contained in:
wuggy 2026-06-26 21:04:09 -07:00
commit 7154a0497e
2123 changed files with 197052 additions and 0 deletions

View 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)!
}
}

View 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
}
}

View 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>

View 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)
}
}

View 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()
}
}

View 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)
}
}

View 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)
}
}

View 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))
}
}

View 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)
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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()
}
}

View 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)
}

View 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.
}
}

View 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")
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.