Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
2.0.0
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/* 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<Maybe<Bool>> 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<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 })
}
open func getClient(fxaDeviceId: String) -> Deferred<Maybe<RemoteClient?>> {
return deferMaybe(self.clientsAndTabs.find { clientAndTabs in
return clientAndTabs.client.fxaDeviceId == fxaDeviceId
}?.client)
}
open func getClient(guid: GUID) -> Deferred<Maybe<RemoteClient?>> {
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<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 deleteCommands() -> Success {
self.commands = [GUID: [SyncCommand]]()
return succeed()
}
open func deleteCommands(_ clientGUID: GUID) -> Success {
self.commands[clientGUID] = [SyncCommand]()
return succeed()
}
open func getCommands() -> Deferred<Maybe<[GUID: [SyncCommand]]>> {
return deferMaybe(self.commands)
}
open func insertCommand(_ command: SyncCommand, forClients clients: [RemoteClient]) -> Deferred<Maybe<Int>> {
return self.insertCommands([command], forClients: clients)
}
open func insertCommands(_ commands: [SyncCommand], forClients clients: [RemoteClient]) -> Deferred<Maybe<Int>> {
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)
}
}