Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
firefox-merge-…
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
executable file 627 lines (521 sloc) 31.3 KB
/* 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/. */
@testable import Account
import Foundation
import Shared
import Storage
@testable import Sync
import XCGLogger
import Deferred
import XCTest
import SwiftyJSON
private let log = Logger.syncLogger
class MockSyncAuthState: SyncAuthState {
var clientName: String?
var enginesEnablements: [String : Bool]?
let serverRoot: String
let kSync: Data
var deviceID: String? {
return "mock_device_id"
}
init(serverRoot: String, kSync: Data) {
self.serverRoot = serverRoot
self.kSync = kSync
}
func invalidate() {
}
func token(_ now: Timestamp, canBeExpired: Bool) -> Deferred<Maybe<(token: TokenServerToken, forKey: Data)>> {
let token = TokenServerToken(id: "id", key: "key", api_endpoint: serverRoot, uid: UInt64(0), hashedFxAUID: "",
durationInSeconds: UInt64(5 * 60), remoteTimestamp: Timestamp(now - 1))
return deferMaybe((token, self.kSync))
}
}
class MetaGlobalTests: XCTestCase {
var server: MockSyncServer!
var serverRoot: String!
var kSync: Data!
var syncPrefs: Prefs!
var authState: SyncAuthState!
var stateMachine: SyncStateMachine!
override func setUp() {
kSync = Data.randomOfLength(64)!
server = MockSyncServer(username: "1234567")
server.start()
serverRoot = server.baseURL
syncPrefs = MockProfilePrefs()
authState = MockSyncAuthState(serverRoot: serverRoot, kSync: kSync)
stateMachine = SyncStateMachine(prefs: syncPrefs)
}
func storeMetaGlobal(metaGlobal: MetaGlobal) {
let envelope = EnvelopeJSON(JSON(object: [
"id": "global",
"collection": "meta",
"payload": metaGlobal.asPayload().json.stringValue()!,
"modified": Double(Date.now())/1000]))
server.storeRecords(records: [envelope], inCollection: "meta")
}
func storeCryptoKeys(keys: Keys) {
let keyBundle = KeyBundle.fromKSync(kSync)
let record = Record(id: "keys", payload: keys.asPayload())
let envelope = EnvelopeJSON(keyBundle.serializer({ $0.json })(record)!)
server.storeRecords(records: [envelope], inCollection: "crypto")
}
func assertFreshStart(ready: Ready?, after: Timestamp) {
XCTAssertNotNil(ready)
guard let ready = ready else {
return
}
// We should have wiped.
// We should have uploaded new meta/global and crypto/keys.
XCTAssertGreaterThan(server.collections["meta"]?.records["global"]?.modified ?? 0, after)
XCTAssertGreaterThan(server.collections["meta"]?.modified ?? 0, after)
XCTAssertGreaterThan(server.collections["crypto"]?.records["keys"]?.modified ?? 0, after)
XCTAssertGreaterThan(server.collections["crypto"]?.modified ?? 0, after)
// And we should have downloaded meta/global and crypto/keys.
XCTAssertNotNil(ready.scratchpad.global)
XCTAssertNotNil(ready.scratchpad.keys)
// We should have the default engine configuration.
XCTAssertNotNil(ready.scratchpad.engineConfiguration)
guard let engineConfiguration = ready.scratchpad.engineConfiguration else {
return
}
XCTAssertEqual(engineConfiguration.enabled.sorted(), ["addons", "bookmarks", "clients", "forms", "history", "passwords", "prefs", "tabs"])
XCTAssertEqual(engineConfiguration.declined, [])
// Basic verifications.
XCTAssertEqual(ready.collectionKeys.defaultBundle.encKey.count, 32)
if let clients = ready.scratchpad.global?.value.engines["clients"] {
XCTAssertTrue(clients.syncID.count == 12)
}
}
func testMetaGlobalVersionTooNew() {
// There's no recovery from a meta/global version "in the future": just bail out with an UpgradeRequiredError.
storeMetaGlobal(metaGlobal: MetaGlobal(syncID: "id", storageVersion: 6, engines: [String: EngineMeta](), declined: []))
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "clientUpgradeRequired"])
XCTAssertNotNil(result.failureValue as? ClientUpgradeRequiredError)
XCTAssertNil(result.successValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
func testMetaGlobalVersionTooOld() {
// To recover from a meta/global version "in the past", fresh start.
storeMetaGlobal(metaGlobal: MetaGlobal(syncID: "id", storageVersion: 4, engines: [String: EngineMeta](), declined: []))
let afterStores = Date.now()
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "remoteUpgradeRequired",
"freshStartRequired", "serverConfigurationRequired", "initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
self.assertFreshStart(ready: result.successValue, after: afterStores)
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
func testMetaGlobalMissing() {
// To recover from a missing meta/global, fresh start.
let afterStores = Date.now()
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "missingMetaGlobal",
"freshStartRequired", "serverConfigurationRequired", "initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
self.assertFreshStart(ready: result.successValue, after: afterStores)
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
func testCryptoKeysMissing() {
// To recover from a missing crypto/keys, fresh start.
storeMetaGlobal(metaGlobal: createMetaGlobal(enginesEnablements: nil))
let afterStores = Date.now()
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "missingCryptoKeys", "freshStartRequired", "serverConfigurationRequired", "initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
self.assertFreshStart(ready: result.successValue, after: afterStores)
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
func testMetaGlobalAndCryptoKeysFresh() {
// When encountering a valid meta/global and crypto/keys, advance smoothly.
let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, engines: [String: EngineMeta](), declined: [])
let cryptoKeys = Keys.random()
storeMetaGlobal(metaGlobal: metaGlobal)
storeCryptoKeys(keys: cryptoKeys)
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have downloaded meta/global and crypto/keys.
XCTAssertEqual(ready.scratchpad.global?.value, metaGlobal)
XCTAssertEqual(ready.scratchpad.keys?.value, cryptoKeys)
// We should have marked all local engines for reset.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"])
ready.clearLocalCommands()
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
let afterFirstSync = Date.now()
// Now, run through the state machine again. Nothing's changed remotely, so we should advance quickly.
let secondExpectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have not downloaded a fresh meta/global or crypto/keys.
XCTAssertLessThan(ready.scratchpad.global?.timestamp ?? Timestamp.max, afterFirstSync)
XCTAssertLessThan(ready.scratchpad.keys?.timestamp ?? Timestamp.max, afterFirstSync)
// We should not have marked any local engines for reset.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), [])
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
secondExpectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
func testFailingOptimisticStateMachine() {
// We test only the optimistic state machine, knowing it will need to go through
// needsFreshMetaGlobal, and fail.
let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, engines: [String: EngineMeta](), declined: [])
let cryptoKeys = Keys.random()
storeMetaGlobal(metaGlobal: metaGlobal)
storeCryptoKeys(keys: cryptoKeys)
stateMachine = SyncStateMachine(prefs: syncPrefs, allowingStates: SyncStateMachine.OptimisticStates)
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal"])
XCTAssertNotNil(result.failureValue)
if let failure = result.failureValue as? DisallowedStateError {
XCTAssertEqual(failure.state, SyncStateLabel.NeedsFreshMetaGlobal)
} else {
XCTFail("SyncStatus failed, but with a different error")
}
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
func testHappyOptimisticStateMachine() {
// We should be able to quickly progress through a constrained (a.k.a. optimistic) state machine
let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, engines: [String: EngineMeta](), declined: [])
let cryptoKeys = Keys.random()
storeMetaGlobal(metaGlobal: metaGlobal)
storeCryptoKeys(keys: cryptoKeys)
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
// Now, run through the state machine again. Nothing's changed remotely, so we should advance quickly.
// We should be able to use this 'optimistic' path in an extension.
stateMachine = SyncStateMachine(prefs: syncPrefs, allowingStates: SyncStateMachine.OptimisticStates)
let secondExpectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
secondExpectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
func testUpdatedCryptoKeys() {
// When encountering a valid meta/global and crypto/keys, advance smoothly.
let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5, engines: [String: EngineMeta](), declined: [])
let cryptoKeys = Keys.random()
cryptoKeys.collectionKeys.updateValue(KeyBundle.random(), forKey: "bookmarks")
cryptoKeys.collectionKeys.updateValue(KeyBundle.random(), forKey: "clients")
storeMetaGlobal(metaGlobal: metaGlobal)
storeCryptoKeys(keys: cryptoKeys)
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have downloaded meta/global and crypto/keys.
XCTAssertEqual(ready.scratchpad.global?.value, metaGlobal)
XCTAssertEqual(ready.scratchpad.keys?.value, cryptoKeys)
// We should have marked all local engines for reset.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"])
ready.clearLocalCommands()
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
let afterFirstSync = Date.now()
// Store a fresh crypto/keys, with the same default key, one identical collection key, and one changed collection key.
let freshCryptoKeys = Keys(defaultBundle: cryptoKeys.defaultBundle)
freshCryptoKeys.collectionKeys.updateValue(cryptoKeys.forCollection("bookmarks"), forKey: "bookmarks")
freshCryptoKeys.collectionKeys.updateValue(KeyBundle.random(), forKey: "clients")
storeCryptoKeys(keys: freshCryptoKeys)
// Now, run through the state machine again.
let secondExpectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have not downloaded a fresh meta/global ...
XCTAssertLessThan(ready.scratchpad.global?.timestamp ?? Timestamp.max, afterFirstSync)
// ... but we should have downloaded a fresh crypto/keys.
XCTAssertGreaterThanOrEqual(ready.scratchpad.keys?.timestamp ?? Timestamp.min, afterFirstSync)
// We should have marked only the local engine with a changed key for reset.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["clients"])
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
secondExpectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
let afterSecondSync = Date.now()
// Store a fresh crypto/keys, with a changed default key and one identical collection key, and one changed collection key.
let freshCryptoKeys2 = Keys.random()
freshCryptoKeys2.collectionKeys.updateValue(freshCryptoKeys.forCollection("bookmarks"), forKey: "bookmarks")
freshCryptoKeys2.collectionKeys.updateValue(KeyBundle.random(), forKey: "clients")
storeCryptoKeys(keys: freshCryptoKeys2)
// Now, run through the state machine again.
let thirdExpectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have not downloaded a fresh meta/global ...
XCTAssertLessThan(ready.scratchpad.global?.timestamp ?? Timestamp.max, afterSecondSync)
// ... but we should have downloaded a fresh crypto/keys.
XCTAssertGreaterThanOrEqual(ready.scratchpad.keys?.timestamp ?? Timestamp.min, afterSecondSync)
// We should have marked all local engines as needing reset, except for the engine whose key remained constant.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["clients", "history", "passwords", "tabs"])
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
thirdExpectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
let afterThirdSync = Date.now()
// Now store a random crypto/keys, with a different default key (and no bulk keys).
let randomCryptoKeys = Keys.random()
storeCryptoKeys(keys: randomCryptoKeys)
// Now, run through the state machine again.
let fourthExpectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have not downloaded a fresh meta/global ...
XCTAssertLessThan(ready.scratchpad.global?.timestamp ?? Timestamp.max, afterThirdSync)
// ... but we should have downloaded a fresh crypto/keys.
XCTAssertGreaterThanOrEqual(ready.scratchpad.keys?.timestamp ?? Timestamp.min, afterThirdSync)
// We should have marked all local engines for reset.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"])
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
fourthExpectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
private func createUnusualMetaGlobal() -> MetaGlobal {
let metaGlobal = MetaGlobal(syncID: "id", storageVersion: 5,
engines: ["bookmarks": EngineMeta(version: 1, syncID: "bookmarks"), "unknownEngine1": EngineMeta(version: 2, syncID: "engineId1")],
declined: ["clients", "forms", "unknownEngine2"])
return metaGlobal
}
func testEngineConfigurations() {
// When encountering a valid meta/global and crypto/keys, advance smoothly. Keep the engine configuration for re-upload.
let metaGlobal = createUnusualMetaGlobal()
let cryptoKeys = Keys.random()
storeMetaGlobal(metaGlobal: metaGlobal)
storeCryptoKeys(keys: cryptoKeys)
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// We should have saved the engine configuration.
XCTAssertNotNil(ready.scratchpad.engineConfiguration)
guard let engineConfiguration = ready.scratchpad.engineConfiguration else {
return
}
XCTAssertEqual(engineConfiguration, metaGlobal.engineConfiguration())
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
// Wipe meta/global.
server.removeAllItemsFromCollection(collection: "meta", atTime: Date.now())
// Now, run through the state machine again. We should produce and upload a meta/global reflecting our engine configuration.
let secondExpectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "missingMetaGlobal", "freshStartRequired", "serverConfigurationRequired", "initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// The downloaded meta/global should reflect our local engine configuration.
XCTAssertNotNil(ready.scratchpad.global)
guard let global = ready.scratchpad.global?.value else {
return
}
XCTAssertEqual(global.engineConfiguration(), metaGlobal.engineConfiguration())
// We should have the same cached engine configuration.
XCTAssertNotNil(ready.scratchpad.engineConfiguration)
guard let engineConfiguration = ready.scratchpad.engineConfiguration else {
return
}
XCTAssertEqual(engineConfiguration, metaGlobal.engineConfiguration())
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
secondExpectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
func testMetaGlobalModified() {
// When encountering a valid meta/global and crypto/keys, advance smoothly.
let metaGlobal = createUnusualMetaGlobal()
let cryptoKeys = Keys.random()
storeMetaGlobal(metaGlobal: metaGlobal)
storeCryptoKeys(keys: cryptoKeys)
let expectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have downloaded meta/global and crypto/keys.
XCTAssertEqual(ready.scratchpad.global?.value, metaGlobal)
XCTAssertEqual(ready.scratchpad.keys?.value, cryptoKeys)
// We should have marked all local engines for reset.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"])
XCTAssertEqual(ready.enginesEnabled(), [])
XCTAssertEqual(ready.enginesDisabled(), [])
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
expectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
let afterFirstSync = Date.now()
// Store a meta/global with a new global syncID.
let newMetaGlobal = metaGlobal.withSyncID("newID")
storeMetaGlobal(metaGlobal: newMetaGlobal)
// Now, run through the state machine again.
let secondExpectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "needsFreshCryptoKeys", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have downloaded a fresh meta/global ...
XCTAssertGreaterThanOrEqual(ready.scratchpad.global?.timestamp ?? Timestamp.min, afterFirstSync)
// ... and we should have downloaded a fresh crypto/keys -- but its timestamp is identical to the old one!
// Therefore, the "needsFreshCryptoKeys" stage above is our test that we re-downloaded crypto/keys.
// We should have marked all local engines for reset.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks", "clients", "history", "passwords", "tabs"])
// And our engine configuration should be unchanged.
XCTAssertNotNil(ready.scratchpad.global)
guard let global = ready.scratchpad.global?.value else {
return
}
XCTAssertEqual(global.engineConfiguration(), metaGlobal.engineConfiguration())
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
secondExpectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
// Now store a meta/global with a changed engine syncID, a new engine, and a new declined entry.
var engines = newMetaGlobal.engines
engines.updateValue(EngineMeta(version: 1, syncID: Bytes.generateGUID()), forKey: "bookmarks")
engines.updateValue(EngineMeta(version: 1, syncID: Bytes.generateGUID()), forKey: "forms")
engines.removeValue(forKey: "unknownEngine1")
var declined = newMetaGlobal.declined.filter({ $0 != "forms" })
declined.append("unknownEngine1")
let secondMetaGlobal = MetaGlobal(syncID: newMetaGlobal.syncID, storageVersion: 5, engines: engines, declined: declined)
storeMetaGlobal(metaGlobal: secondMetaGlobal)
syncPrefs.removeObjectForKey("scratchpad.localCommands")
// Now, run through the state machine again.
let thirdExpectation = self.expectation(description: "Waiting on value.")
stateMachine.toReady(authState).upon { result in
XCTAssertEqual(self.stateMachine.stateLabelSequence.map { $0.rawValue }, ["initialWithLiveToken", "initialWithLiveTokenAndInfo", "needsFreshMetaGlobal", "resolveMetaGlobalVersion", "resolveMetaGlobalContent", "hasMetaGlobal", "hasFreshCryptoKeys", "ready"])
XCTAssertNotNil(result.successValue)
guard let ready = result.successValue else {
return
}
// And we should have downloaded a fresh meta/global ...
XCTAssertGreaterThanOrEqual(ready.scratchpad.global?.timestamp ?? Timestamp.min, afterFirstSync)
// ... and we should have downloaded a fresh crypto/keys -- but its timestamp is identical to the old one!
// Therefore, the "needsFreshCryptoKeys" stage above is our test that we re-downloaded crypto/keys.
// We should have marked the changed engine for local reset, and identified the enabled and disabled engines.
XCTAssertEqual(ready.collectionsThatNeedLocalReset(), ["bookmarks"])
XCTAssertEqual(ready.enginesEnabled(), ["forms"])
XCTAssertEqual(ready.enginesDisabled(), ["unknownEngine1"])
// And our engine configuration should reflect the new meta/global on the server.
XCTAssertNotNil(ready.scratchpad.global)
guard let global = ready.scratchpad.global?.value else {
return
}
XCTAssertEqual(global.engineConfiguration(), secondMetaGlobal.engineConfiguration())
XCTAssertTrue(result.isSuccess)
XCTAssertNil(result.failureValue)
thirdExpectation.fulfill()
}
waitForExpectations(timeout: 2000) { (error) in
XCTAssertNil(error, "Error: \(error ??? "nil")")
}
}
}