/* 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> { return succeed() } open func wipeTabs() -> Success { return succeed() } open func insertOrUpdateClients(_ clients: [RemoteClient]) -> Deferred> { return deferMaybe(0) } open func insertOrUpdateClient(_ client: RemoteClient) -> Deferred> { return deferMaybe(0) } open func insertOrUpdateTabs(_ tabs: [RemoteTab]) -> Deferred> { return insertOrUpdateTabsForClientGUID(nil, tabs: [RemoteTab]()) } open func insertOrUpdateTabsForClientGUID(_ clientGUID: String?, tabs: [RemoteTab]) -> Deferred> { return deferMaybe(-1) } open func getClientsAndTabs() -> Deferred> { return deferMaybe(self.clientsAndTabs) } open func getClients() -> Deferred> { return deferMaybe(self.clientsAndTabs.map { $0.client }) } public func getClient(guid: GUID) -> Deferred> { return deferMaybe(self.clientsAndTabs.find { clientAndTabs in return clientAndTabs.client.guid == guid }?.client) } public func getClient(fxaDeviceId: GUID) -> Deferred> { return deferMaybe(self.clientsAndTabs.find { clientAndTabs in return clientAndTabs.client.fxaDeviceId == fxaDeviceId }?.client) } open func getClientWithId(_ clientID: GUID) -> Deferred> { return getClient(guid: clientID) } open func getClientGUIDs() -> Deferred>> { return deferMaybe(Set(optFilter(self.clientsAndTabs.map { $0.client.guid }))) } open func getTabsForClientWithGUID(_ guid: GUID?) -> Deferred> { 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> { return deferMaybe([GUID: [SyncCommand]]()) } open func insertCommand(_ command: SyncCommand, forClients clients: [RemoteClient]) -> Deferred> { return deferMaybe(0) } open func insertCommands(_ commands: [SyncCommand], forClients clients: [RemoteClient]) -> Deferred> { 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. } }