/* 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 Deferred import Foundation import Shared @testable import Storage @testable import Sync @testable import SyncTelemetry import SwiftyJSON import XCTest class TestBookmarksRepairRequestor: XCTestCase { private let MockHashedDeviceID = "4b66918e184c0a9a49c4a9dc7468d3495642141a08419e69c6cb107367366176" private func buildMockScratchpad(prefs: Prefs) -> Scratchpad { var scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) let b = Scratchpad.Builder(p: scratchpad) b.hashedUID = "1234" scratchpad = b.build() XCTAssertEqual(scratchpad.fxaDeviceId, "unknown_fxaDeviceId") XCTAssertEqual(scratchpad.hashedDeviceID!, MockHashedDeviceID) return scratchpad } func testNoClients() { let expectation = self.expectation(description: #function) let prefs = MockProfilePrefs() let scratchpad = buildMockScratchpad(prefs: prefs) let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: localClient, tabs: [])]) let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) requestor.startRepairs(validationInfo: validationInfo) >>== { result in XCTAssertTrue(result) XCTAssertEqual(remoteClients.commands.count, 1) XCTAssertEqual(remoteClients.commands["localID"]!.count, 0) expectation.fulfill() } waitForExpectations(timeout: 10, handler: nil) } func testOneClientNoResponse() { let expectation = self.expectation(description: #function) let prefs = MockProfilePrefs() let scratchpad = buildMockScratchpad(prefs: prefs) let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) let remoteClient = RemoteClient(guid: "client-a", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClient, tabs: [])]) let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] let mockFlowID = Bytes.generateGUID() // Mock telemetry events let startedEvent = makeRepairEvent(["started", nil, ["flowID": mockFlowID, "numIDs": "2"]]) let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": mockFlowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) let finishedEvent = makeRepairEvent(["finished", nil, ["flowID": mockFlowID, "numIDs": "2"]]) let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) requestor.startRepairs(validationInfo: validationInfo, flowID: mockFlowID) >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") // asking it to continue stays in that state until we timeout or the command // is removed. return requestor.continueRepairs() } >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) // now pretend that client synced. let _ = remoteClients.deleteCommands("client-a") return requestor.continueRepairs() } >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent-again") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, uploadEvent) // the command should be outgoing again. checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") // pretend that client synced again without writing a command. let _ = remoteClients.deleteCommands("client-a") return requestor.continueRepairs() } >>== { result in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, uploadEvent, finishedEvent) expectation.fulfill() } waitForExpectations(timeout: 10, handler: nil) } func testOneClientTimeout() { let expectation = self.expectation(description: #function) let prefs = MockProfilePrefs() let scratchpad = buildMockScratchpad(prefs: prefs) let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) let remoteClient = RemoteClient(guid: "client-a", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClient, tabs: [])]) let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] // Mock telemetry events let mockFlowID = Bytes.generateGUID() let startedEvent = makeRepairEvent(["started", nil, ["flowID": mockFlowID, "numIDs": "2"]]) let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": mockFlowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) let abandonEvent = makeRepairEvent(["abandon", "silent", ["flowID": mockFlowID, "deviceID": MockHashedDeviceID]]) let finishedEvent = makeRepairEvent(["finished", nil, ["flowID": mockFlowID, "numIDs": "2"]]) let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) requestor.startRepairs(validationInfo: validationInfo, flowID: mockFlowID) >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") // pretend we are now in the future (well actually, that the request was made a long time ago) prefs.setTimestamp(0, forKey: "repairs.bookmark.when") return requestor.continueRepairs() } >>== { result in // We should be finished as we gave up in disgust. XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, abandonEvent, finishedEvent) expectation.fulfill() } waitForExpectations(timeout: 10, handler: nil) } func testLatestClientUsed() { let expectation = self.expectation(description: #function) let prefs = MockProfilePrefs() let scratchpad = buildMockScratchpad(prefs: prefs) let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) let clientEarly = RemoteClient(guid: "client-early", name: "Test remote client", modified: (Date.now() - OneWeekInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let clientLate = RemoteClient(guid: "client-late", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: clientEarly, tabs: []), ClientAndTabs(client: clientLate, tabs: [])]) let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] // Mock telemetry events let mockFlowID = Bytes.generateGUID() let startedEvent = makeRepairEvent(["started", nil, ["flowID": mockFlowID, "numIDs": "2"]]) let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": mockFlowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) requestor.startRepairs(validationInfo: validationInfo, flowID: mockFlowID) >>== { result in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) // the repair command should be outgoing to the most-recent client. checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-late") expectation.fulfill() } waitForExpectations(timeout: 10, handler: nil) } func testClientVanishes() { let expectation = self.expectation(description: #function) let prefs = MockProfilePrefs() let scratchpad = buildMockScratchpad(prefs: prefs) let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) let remoteClientA = RemoteClient(guid: "client-a", name: "Test remote client", modified: Date.now(), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClientB = RemoteClient(guid: "client-b", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: remoteClientA, tabs: []), ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClientB, tabs: [])]) let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2"]] // Mock telemetry events let flowID = Bytes.generateGUID() let startedEvent = makeRepairEvent(["started", nil, ["flowID": flowID, "numIDs": "2"]]) let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) let missingEvent = makeRepairEvent(["abandon", "missing", ["flowID": flowID, "deviceID": MockHashedDeviceID]]) let responseEvent = makeRepairEvent(["response", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) let finishedEvent = makeRepairEvent(["finished", nil, ["flowID": flowID, "numIDs": "0"]]) let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) requestor.startRepairs(validationInfo: validationInfo, flowID: flowID) >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") // asking it to continue stays in that state until we timeout or the command // is removed. return requestor.continueRepairs() } >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) // the command should now be outgoing. checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") let _ = remoteClients.deleteCommands("client-a") // Now let's pretend the client vanished. remoteClients.clientsAndTabs.removeFirst() return requestor.continueRepairs() } >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, missingEvent, uploadEvent) // We should have moved on to client-b. checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-b") // Now let's pretend client B wrote all missing IDs. let repairResponse = RepairResponse(collection: "bookmarks", request: "upload", flowID: flowID, clientID: "client-b", ids: ["mock-guid1", "mock-guid2"]) return requestor.continueRepairs(response: repairResponse) } >>== { result in XCTAssertTrue(result) checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, missingEvent, uploadEvent, responseEvent, finishedEvent) // We should be finished as we got all our IDs. XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) expectation.fulfill() } waitForExpectations(timeout: 10, handler: nil) } func testMultiClients() { let expectation = self.expectation(description: #function) let prefs = MockProfilePrefs() let scratchpad = buildMockScratchpad(prefs: prefs) let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) let remoteClientA = RemoteClient(guid: "client-a", name: "Test remote client", modified: Date.now(), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClientB = RemoteClient(guid: "client-b", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: remoteClientA, tabs: []), ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClientB, tabs: [])]) let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2", "mock-guid3"]] let flowID = Bytes.generateGUID() // Mock telemetry events let startedEvent = makeRepairEvent(["started", nil, ["flowID": flowID, "numIDs": "3"]]) let firstUploadEvent = makeRepairEvent(["request", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "3"]]) let secondUploadEvent = makeRepairEvent(["request", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "1"]]) let firstResponseEvent = makeRepairEvent(["response", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "2"]]) let secondResponseEvent = makeRepairEvent(["response", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "1"]]) let finishedEvent = makeRepairEvent(["finished", nil, ["flowID": flowID, "numIDs": "0"]]) let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) requestor.startRepairs(validationInfo: validationInfo, flowID: flowID) >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") // asking it to continue stays in that state until we timeout or the command // is removed. return requestor.continueRepairs() } >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, firstUploadEvent) // the command should now be outgoing. checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") let _ = remoteClients.deleteCommands("client-a") // Now let's pretend the client wrote a response. let repairResponse = RepairResponse(collection: "bookmarks", request: "upload", flowID: flowID, clientID: "client-a", ids: ["mock-guid1", "mock-guid2"]) return requestor.continueRepairs(response: repairResponse) } >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, firstUploadEvent, firstResponseEvent, secondUploadEvent) // We should have moved on to client-b. checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-b") let _ = remoteClients.deleteCommands("client-b") // Now let's pretend client B write the missing ID. let repairResponse = RepairResponse(collection: "bookmarks", request: "upload", flowID: flowID, clientID: "client-b", ids: ["mock-guid3"]) return requestor.continueRepairs(response: repairResponse) } >>== { result in XCTAssertTrue(result) checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, firstUploadEvent, firstResponseEvent, secondUploadEvent, secondResponseEvent, finishedEvent) // We should be finished as we got all our IDs. XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) expectation.fulfill() } waitForExpectations(timeout: 10, handler: nil) } func testAlreadyRepairingContinue() { let expectation = self.expectation(description: #function) let prefs = MockProfilePrefs() let scratchpad = buildMockScratchpad(prefs: prefs) let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) let remoteClientA = RemoteClient(guid: "client-a", name: "Test remote client", modified: Date.now(), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClientB = RemoteClient(guid: "client-b", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: remoteClientA, tabs: []), ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClientB, tabs: [])]) let validationInfo = [BufferInconsistency.missingValues: ["mock-guid1", "mock-guid2", "mock-guid3"]] // Mock telemetry events let flowID = Bytes.generateGUID() let startedEvent = makeRepairEvent(["started", nil, ["flowID": flowID, "numIDs": "3"]]) let uploadEvent = makeRepairEvent(["request", "upload", ["flowID": flowID, "deviceID": MockHashedDeviceID, "numIDs": "3"]]) let abortedEvent = makeRepairEvent(["aborted", nil, ["flowID": flowID, "reason": "other clients repairing", "numIDs": "3"]]) let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) requestor.startRepairs(validationInfo: validationInfo, flowID: flowID) >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") // asking it to continue stays in that state until we timeout or the command // is removed. return requestor.continueRepairs() } >>== { result -> Deferred> in XCTAssertTrue(result) XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), "repair.sent") checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent) // the command should now be outgoing. checkOutgoingCommand(remoteClients: remoteClients, clientID: "client-a") let _ = remoteClients.deleteCommands("client-a") // Now let's pretend the client wrote a response (it doesn't matter what's in here) let repairResponse = RepairResponse(collection: "bookmarks", request: "upload", flowID: flowID, clientID: "client-a", ids: ["mock-guid1", "mock-guid2"]) // and another client also started a request let otherRequest = RepairRequest(collection: "bookmarks", request: "upload", flowID: "abdc", requestor: "client-c", ids: ["bogusid"]) let _ = remoteClients.insertCommand(otherRequest.toSyncCommand(), forClients: [remoteClientB]) return requestor.continueRepairs(response: repairResponse) } >>== { result in XCTAssertTrue(result) // We should have aborted now XCTAssertEqual(prefs.stringForKey("repairs.bookmark.state"), nil) checkRecordedEvents(fromPrefs: prefs, expected: startedEvent, uploadEvent, abortedEvent) expectation.fulfill() } waitForExpectations(timeout: 10, handler: nil) } func testSyncEventsPickledInPrefs() { let prefs = MockProfilePrefs() let scratchpad = Scratchpad(b: KeyBundle.random(), persistingTo: prefs) let localClient = RemoteClient(guid: nil, name: "Test local client", modified: (Date.now() - OneMinuteInMilliseconds), type: "mobile", formfactor: "largetablet", os: "iOS", version: nil, fxaDeviceId: nil) let remoteClientA = RemoteClient(guid: "client-a", name: "Test remote client", modified: Date.now(), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClientB = RemoteClient(guid: "client-b", name: "Test remote client", modified: (Date.now() - OneMinuteInMilliseconds), type: "desktop", formfactor: nil, os: nil, version: "55.0.1", fxaDeviceId: nil) let remoteClients = MockRemoteClientsAndTabs([ClientAndTabs(client: remoteClientA, tabs: []), ClientAndTabs(client: localClient, tabs: []), ClientAndTabs(client: remoteClientB, tabs: [])]) let requestor = BookmarksRepairRequestor(scratchpad: scratchpad, basePrefs: prefs, remoteClients: remoteClients) let mockTimestamp = Date.now() let mockEvent = Event(timestamp: mockTimestamp, category: "test", method: "method", object: "object", value: "value", extra: ["test": "value"]) requestor.recordTelemetry(event: mockEvent) let events = requestor.basePrefs.arrayForKey(PrefKeySyncEvents) as! [Data] XCTAssertEqual(events.count, 1) let pickledEvent = Event.unpickle(events[0]) XCTAssertEqual(pickledEvent!.category, mockEvent.category) } } func checkOutgoingCommand(remoteClients: MockRemoteClientsAndTabs, clientID: GUID) { let outgoingCmds = remoteClients.commands[clientID]! XCTAssertEqual(outgoingCmds.count, 1) XCTAssertEqual(JSON.parse(outgoingCmds.first!.value)["command"].stringValue, "repairRequest") } func checkRecordedEvents(fromPrefs prefs: Prefs, expected: Event...) { let eventData = prefs.arrayForKey(PrefKeySyncEvents) as? [Data] ?? [] let actualEvents = eventData.map(Event.unpickle) XCTAssertEqual(actualEvents.count, expected.count) expected.enumerated().forEach { offset, expected in XCTAssertEqual(expected, actualEvents[offset]) } } func makeRepairEvent(_ values: [Any?]) -> Event { return Event(timestamp: Date.now(), category: "sync", method: "repair", object: values[0] as! String, value: values[1] as? String, extra: values[2] as? [String: String] ) } // Checks equivalence while ignoring the timestamp extension Event: Equatable { public static func ==(left: Event, right: Event) -> Bool { let propsAreEqual = (left.category == right.category) && (left.method == right.method) && (left.object == right.object) && (left.value ?? "" == right.value ?? "") && (left.extra ?? [:] == right.extra ?? [:]) return propsAreEqual } } open class MockRemoteClientsAndTabs: RemoteClientsAndTabs { open var clientsAndTabs: [ClientAndTabs] open var commands: [GUID: [SyncCommand]] public init(_ clientsAndTabs: [ClientAndTabs]) { self.clientsAndTabs = clientsAndTabs self.commands = clientsAndTabs.map { $0.client.guid ?? "localID" }.reduce([String: [SyncCommand]]()) { (dict, clientId) -> [String: [SyncCommand]] in var dict = dict dict[clientId] = [SyncCommand]() return dict } } 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 }) } open func getClientWithId(_ clientID: GUID) -> Deferred> { return self.getClient(guid: clientID) } open func getClient(fxaDeviceId: String) -> Deferred> { return deferMaybe(self.clientsAndTabs.find { clientAndTabs in return clientAndTabs.client.fxaDeviceId == fxaDeviceId }?.client) } open func getClient(guid: GUID) -> Deferred> { return deferMaybe(self.clientsAndTabs.find { clientAndTabs in return clientAndTabs.client.guid == guid }?.client) } open func deleteClient(guid: GUID) -> Success { clientsAndTabs = clientsAndTabs.filter { $0.client.guid != guid } return succeed() } 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 deleteCommands() -> Success { self.commands = [GUID: [SyncCommand]]() return succeed() } open func deleteCommands(_ clientGUID: GUID) -> Success { self.commands[clientGUID] = [SyncCommand]() return succeed() } open func getCommands() -> Deferred> { return deferMaybe(self.commands) } open func insertCommand(_ command: SyncCommand, forClients clients: [RemoteClient]) -> Deferred> { return self.insertCommands([command], forClients: clients) } open func insertCommands(_ commands: [SyncCommand], forClients clients: [RemoteClient]) -> Deferred> { var numInserts = 0 for client in clients where self.commands[client.guid ?? "localID"] != nil { self.commands[client.guid ?? "localID"]! += commands numInserts += commands.count } return deferMaybe(numInserts) } }