From b7d51d22b63178ece9d5bc95ea19ab6f161571a8 Mon Sep 17 00:00:00 2001 From: sashei Date: Wed, 14 Mar 2018 16:43:02 -0600 Subject: [PATCH] touch item wip --- Cartfile | 1 + Cartfile.resolved | 1 + Lockbox.xcodeproj/project.pbxproj | 10 + lockbox-ios/Action/DataStoreAction.swift | 86 +++++++- lockbox-ios/Common/Extensions/Date+.swift | 11 + lockbox-ios/Model/Item.swift | 15 +- .../Presenter/ItemDetailPresenter.swift | 4 + lockbox-ios/Store/DataStore.swift | 28 ++- lockbox-ios/lockbox-datastore/dswrapper.js | 10 + .../DataStoreActionIntegrationSpec.swift | 33 ++- lockbox-iosTests/DataStoreActionSpec.swift | 206 +++++++++++++++++- lockbox-iosTests/DataStoreSpec.swift | 23 +- lockbox-iosTests/FxAStoreSpec.swift | 2 +- 13 files changed, 390 insertions(+), 40 deletions(-) create mode 100644 lockbox-ios/Common/Extensions/Date+.swift diff --git a/Cartfile b/Cartfile index fa14f2f4b..ebe499d8d 100644 --- a/Cartfile +++ b/Cartfile @@ -2,4 +2,5 @@ github "Quick/Quick" github "Quick/Nimble" github "ReactiveX/RxSwift" github "RxSwiftCommunity/RxDataSources" ~> 3.0 +github "RxSwiftCommunity/RxOptional" ~> 3.1.3 github "mozilla-mobile/telemetry-ios" "v1.0.4" diff --git a/Cartfile.resolved b/Cartfile.resolved index bd463012b..ae1342fa9 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -2,4 +2,5 @@ github "Quick/Nimble" "v7.0.3" github "Quick/Quick" "v1.2.0" github "ReactiveX/RxSwift" "4.1.2" github "RxSwiftCommunity/RxDataSources" "3.0.2" +github "RxSwiftCommunity/RxOptional" "3.3.0" github "mozilla-mobile/telemetry-ios" "v1.0.4" diff --git a/Lockbox.xcodeproj/project.pbxproj b/Lockbox.xcodeproj/project.pbxproj index f619d6925..fe3751009 100644 --- a/Lockbox.xcodeproj/project.pbxproj +++ b/Lockbox.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 7AA541B3BC9C4F5E6A725222 /* FxAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA54C12A1BC616CA2344584 /* FxAView.swift */; }; 7AA541BD28063282A0B8B352 /* Observable+Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA547CE3404CC8123CA455D /* Observable+Spec.swift */; }; 7AA5435895EE131D6B5BD4ED /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA5450E45A4DB17626F4684 /* WebView.swift */; }; + 7AA5436C61447D25F2167706 /* Date+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA54DEAC9AF2E17ED0FADC3 /* Date+.swift */; }; 7AA5439445A0DE14E73A038A /* DataStoreSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA5439EEDE4AB222842E7C8 /* DataStoreSpec.swift */; }; 7AA543A00DEEDF6947B578EC /* UserInfoStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA54146E0C9269A0CE8D53A /* UserInfoStore.swift */; }; 7AA543D2B55F2968B976F8EF /* OAuthInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA544EC2E10034239FCB256 /* OAuthInfoSpec.swift */; }; @@ -109,6 +110,8 @@ 7D7AD94B204EFBA4002D35EE /* StatusAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7D7AD94A204EFBA4002D35EE /* StatusAlert.xib */; }; 7D8296591F9FA4E800ED1ADD /* RxSwift.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7DF469C31F9F8F8000375C74 /* RxSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7D88BD461F9A6F5B0082A838 /* ItemEntrySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D88BD451F9A6F5B0082A838 /* ItemEntrySpec.swift */; }; + 7D92DAF320586FBA00195A1B /* RxOptional.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D92DAF220586FBA00195A1B /* RxOptional.framework */; }; + 7D92DAF420586FC200195A1B /* RxOptional.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7D92DAF220586FBA00195A1B /* RxOptional.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7DA4C6832028F84800B61DD8 /* ItemListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA4C6802028F84800B61DD8 /* ItemListView.swift */; }; 7DA4C6862028F85A00B61DD8 /* ItemListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA4C6852028F85A00B61DD8 /* ItemListPresenter.swift */; }; 7DA4C68D2028F99200B61DD8 /* ItemListViewSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA4C6872028F86E00B61DD8 /* ItemListViewSpec.swift */; }; @@ -160,6 +163,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 7D92DAF420586FC200195A1B /* RxOptional.framework in CopyFiles */, 7D302BEB1FD7284600D2FD4B /* RxCocoa.framework in CopyFiles */, 7DEA1C4420362E18008AD7C4 /* Differentiator.framework in CopyFiles */, 7D8296591F9FA4E800ED1ADD /* RxSwift.framework in CopyFiles */, @@ -234,6 +238,7 @@ 7AA54CDEA56C47263160AD0E /* Dispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatcher.swift; sourceTree = ""; }; 7AA54D4248CF43B76BACDBD3 /* FxAStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAStore.swift; sourceTree = ""; }; 7AA54D4E439D76B910034E2A /* ErrorAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorAction.swift; sourceTree = ""; }; + 7AA54DEAC9AF2E17ED0FADC3 /* Date+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+.swift"; sourceTree = ""; }; 7AA54E126924935F1ADF4E8E /* ItemDetailStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemDetailStore.swift; sourceTree = ""; }; 7AA54E38ACC2FDBBF700D5E1 /* RouteAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteAction.swift; sourceTree = ""; }; 7AA54E826632AFFBA1677E5E /* UserInfoStoreSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoStoreSpec.swift; sourceTree = ""; }; @@ -287,6 +292,7 @@ 7D6FB87A204F00E2005E23AA /* StatusAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAlert.swift; sourceTree = ""; }; 7D7AD94A204EFBA4002D35EE /* StatusAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusAlert.xib; sourceTree = ""; }; 7D88BD451F9A6F5B0082A838 /* ItemEntrySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEntrySpec.swift; sourceTree = ""; }; + 7D92DAF220586FBA00195A1B /* RxOptional.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxOptional.framework; path = Carthage/Build/iOS/RxOptional.framework; sourceTree = ""; }; 7D9D64921F9F8EE300279C39 /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/iOS/RxCocoa.framework; sourceTree = ""; }; 7DA4C6802028F84800B61DD8 /* ItemListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListView.swift; sourceTree = ""; }; 7DA4C6852028F85A00B61DD8 /* ItemListPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPresenter.swift; sourceTree = ""; }; @@ -326,6 +332,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7D92DAF320586FBA00195A1B /* RxOptional.framework in Frameworks */, 7D4D2B3A20362DC500238143 /* RxDataSources.framework in Frameworks */, 7DEA1C4320362E18008AD7C4 /* Differentiator.framework in Frameworks */, 7DF469C41F9F8F8000375C74 /* RxSwift.framework in Frameworks */, @@ -468,6 +475,7 @@ 7AA5450E45A4DB17626F4684 /* WebView.swift */, 7AA54FE4F9D6DAEF984F63E6 /* Observable+.swift */, 7AA542A3362BE85B313A88B8 /* UIImage+.swift */, + 7AA54DEAC9AF2E17ED0FADC3 /* Date+.swift */, ); path = Extensions; sourceTree = ""; @@ -499,6 +507,7 @@ 7D1B1A8A1F98F86400C1F5FF /* Frameworks */ = { isa = PBXGroup; children = ( + 7D92DAF220586FBA00195A1B /* RxOptional.framework */, 7DEA1C4120362E09008AD7C4 /* Differentiator.framework */, 7D320B3420360F9000891F73 /* RxDataSources.framework */, D83C9FBD1FAD3D5800D08AAE /* Telemetry.framework */, @@ -835,6 +844,7 @@ 7AA54EC7A9CA9BBD57AFEAF9 /* FilterCell.swift in Sources */, 7AA544AA5AE3DE25FE0B0B74 /* CopyAction.swift in Sources */, 7AA545D6182E36466BC8256A /* CopyConfirmationDisplayStore.swift in Sources */, + 7AA5436C61447D25F2167706 /* Date+.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/lockbox-ios/Action/DataStoreAction.swift b/lockbox-ios/Action/DataStoreAction.swift index efac54e2f..2648d5399 100644 --- a/lockbox-ios/Action/DataStoreAction.swift +++ b/lockbox-ios/Action/DataStoreAction.swift @@ -13,19 +13,21 @@ enum DataStoreError: Error { } enum JSCallbackFunction: String { - case OpenComplete, InitializeComplete, UnlockComplete, LockComplete, ListComplete + case OpenComplete, InitializeComplete, UnlockComplete, LockComplete, ListComplete, UpdateComplete static let allValues: [JSCallbackFunction] = [ .OpenComplete, .InitializeComplete, .UnlockComplete, .LockComplete, - .ListComplete + .ListComplete, + .UpdateComplete ] } enum DataStoreAction: Action { - case list(list: [Item]) + case list(list: [String: Item]) + case updated(item: Item) case locked(locked: Bool) case initialized(initialized: Bool) case opened(opened: Bool) @@ -34,8 +36,10 @@ enum DataStoreAction: Action { extension DataStoreAction: Equatable { static func ==(lhs: DataStoreAction, rhs: DataStoreAction) -> Bool { switch (lhs, rhs) { - case (.list(let lhList), .list(let rhList)): - return lhList.elementsEqual(rhList) + case (.list, .list): + return true + case (.updated(let lhItem), .updated(let rhItem)): + return lhItem == rhItem case (.locked(let lhLocked), .locked(let rhLocked)): return lhLocked == rhLocked case (.initialized(let lhInitialized), .initialized(let rhInitialized)): @@ -63,7 +67,8 @@ class DataStoreActionHandler: NSObject, ActionHandler { private var initializeSubject = PublishSubject() private var unlockSubject = PublishSubject() private var lockSubject = PublishSubject() - private var listSubject = PublishSubject<[Item]>() + private var listSubject = PublishSubject<[String: Item]>() + private var updateSubject = PublishSubject() internal var webViewConfiguration: WKWebViewConfiguration { let webConfig = WKWebViewConfiguration() @@ -182,12 +187,26 @@ class DataStoreActionHandler: NSObject, ActionHandler { self?.dispatcher.dispatch(action: DataStoreAction.list(list: itemList)) }, onError: { [weak self] error in self?.dispatcher.dispatch(action: ErrorAction(error: error)) - self?.listSubject = PublishSubject<[Item]>() + self?.listSubject = PublishSubject<[String: Item]>() }) .disposed(by: self.disposeBag) self._list() } + + public func touch(_ item: Item) { + self.updateSubject + .take(1) + .subscribe(onNext: { [weak self] item in + self?.dispatcher.dispatch(action: DataStoreAction.updated(item: item)) + }, onError: { [weak self] error in + self?.dispatcher.dispatch(action: ErrorAction(error: error)) + self?.updateSubject = PublishSubject() + }) + .disposed(by: self.disposeBag) + + self._touch(item) + } } // javascript interaction @@ -273,6 +292,26 @@ extension DataStoreActionHandler { .disposed(by: self.disposeBag) } + private func _touch(_ item: Item) { + self.openSubject.take(1) + .flatMap { _ in + self.checkState() + } + .flatMap { _ -> Single in + if item.id == nil { + throw DataStoreError.NoIDPassed + } + + let jsonItem = try self.parser.jsonStringFromItem(item) + + return self.webView.evaluateJavaScript("\(self.dataStoreName).touch(\(jsonItem))") + } + .subscribe(onError: { error in + self.updateSubject.onError(error) + }) + .disposed(by: self.disposeBag) + } + private func checkState() -> Single { return _initialized().asObservable() .flatMap { initialized -> Observable in @@ -291,6 +330,23 @@ extension DataStoreActionHandler { } .asSingle() } + + private func completeSubjectWithBody(messageBody: Any, subject: PublishSubject) { + guard let itemDictionary = messageBody as? [String: Any] else { + subject.onError(DataStoreError.UnexpectedType) + return + } + + var item: Item + do { + item = try self.parser.itemFromDictionary(itemDictionary) + } catch { + subject.onError(error) + return + } + + subject.onNext(item) + } } extension DataStoreActionHandler: WKScriptMessageHandler, WKNavigationDelegate { @@ -313,22 +369,28 @@ extension DataStoreActionHandler: WKScriptMessageHandler, WKNavigationDelegate { self.unlockSubject.onNext(()) case .LockComplete: self.lockSubject.onNext(()) + case .UpdateComplete: + self.completeSubjectWithBody(messageBody: message.body, subject: self.updateSubject) case .ListComplete: guard let listBody = message.body as? [[Any]] else { self.dispatcher.dispatch(action: ErrorAction(error: DataStoreError.UnexpectedType)) break } - let itemList = listBody.flatMap { (anyList: [Any]) -> Item? in - guard let itemDictionary = anyList[1] as? [String: Any], + let itemDictionary = listBody.reduce([:]) { dict, anyList -> [String: Item] in + guard let itemId = anyList[0] as? String, + let itemDictionary = anyList[1] as? [String: Any], let item = try? self.parser.itemFromDictionary(itemDictionary) else { - return nil + return dict } - return item + var updatedDict = dict + updatedDict[itemId] = item + + return updatedDict } - self.listSubject.onNext(itemList) + self.listSubject.onNext(itemDictionary) } } } diff --git a/lockbox-ios/Common/Extensions/Date+.swift b/lockbox-ios/Common/Extensions/Date+.swift new file mode 100644 index 000000000..7787454da --- /dev/null +++ b/lockbox-ios/Common/Extensions/Date+.swift @@ -0,0 +1,11 @@ +/* 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 + +extension Date { + var iso8601: String { + return ISO8601DateFormatter().string(from: self) + } +} diff --git a/lockbox-ios/Model/Item.swift b/lockbox-ios/Model/Item.swift index 0b3d30f11..9825c7187 100644 --- a/lockbox-ios/Model/Item.swift +++ b/lockbox-ios/Model/Item.swift @@ -15,6 +15,18 @@ class Item: Codable, Equatable { var lastUsed: String? var entry: ItemEntry + enum CodingKeys: String, CodingKey { + case id = "id" + case disabled = "disabled" + case title = "title" + case origins = "origins" + case tags = "tags" + case created = "created" + case modified = "modified" + case lastUsed = "last_used" + case entry = "entry" + } + init(origins: [String], entry: ItemEntry) { self.origins = origins self.entry = entry @@ -23,7 +35,8 @@ class Item: Codable, Equatable { static func ==(lhs: Item, rhs: Item) -> Bool { return lhs.id == rhs.id && lhs.entry == rhs.entry && - lhs.origins.elementsEqual(rhs.origins) + lhs.origins.elementsEqual(rhs.origins) && + lhs.modified == rhs.modified } diff --git a/lockbox-ios/Presenter/ItemDetailPresenter.swift b/lockbox-ios/Presenter/ItemDetailPresenter.swift index aac0447f0..136efe4f7 100644 --- a/lockbox-ios/Presenter/ItemDetailPresenter.swift +++ b/lockbox-ios/Presenter/ItemDetailPresenter.swift @@ -21,6 +21,7 @@ class ItemDetailPresenter { private var itemDetailStore: ItemDetailStore private var copyDisplayStore: CopyConfirmationDisplayStore private var routeActionHandler: RouteActionHandler + private var dataStoreActionHandler: DataStoreActionHandler private var copyActionHandler: CopyActionHandler private var itemDetailActionHandler: ItemDetailActionHandler private var disposeBag = DisposeBag() @@ -54,6 +55,7 @@ class ItemDetailPresenter { text = item.entry.password ?? "" } + target.dataStoreActionHandler.touch(item) target.copyActionHandler.invoke(CopyAction(text: text, fieldName: value)) }) .disposed(by: target.disposeBag) @@ -66,6 +68,7 @@ class ItemDetailPresenter { itemDetailStore: ItemDetailStore = ItemDetailStore.shared, copyDisplayStore: CopyConfirmationDisplayStore = CopyConfirmationDisplayStore.shared, routeActionHandler: RouteActionHandler = RouteActionHandler.shared, + dataStoreActionHandler: DataStoreActionHandler = DataStoreActionHandler.shared, copyActionHandler: CopyActionHandler = CopyActionHandler.shared, itemDetailActionHandler: ItemDetailActionHandler = ItemDetailActionHandler.shared) { self.view = view @@ -73,6 +76,7 @@ class ItemDetailPresenter { self.itemDetailStore = itemDetailStore self.copyDisplayStore = copyDisplayStore self.routeActionHandler = routeActionHandler + self.dataStoreActionHandler = dataStoreActionHandler self.copyActionHandler = copyActionHandler self.itemDetailActionHandler = itemDetailActionHandler diff --git a/lockbox-ios/Store/DataStore.swift b/lockbox-ios/Store/DataStore.swift index 4870d8cd8..6182dc5be 100644 --- a/lockbox-ios/Store/DataStore.swift +++ b/lockbox-ios/Store/DataStore.swift @@ -5,18 +5,22 @@ import Foundation import RxSwift import RxCocoa +import RxOptional class DataStore { public static let shared = DataStore() fileprivate let disposeBag = DisposeBag() - fileprivate var itemList = ReplaySubject<[Item]>.create(bufferSize: 1) + fileprivate var itemList = ReplaySubject<[String: Item]>.create(bufferSize: 1) fileprivate var initialized = ReplaySubject.create(bufferSize: 1) fileprivate var opened = ReplaySubject.create(bufferSize: 1) fileprivate var locked = ReplaySubject.create(bufferSize: 1) public var onItemList: Observable<[Item]> { return self.itemList.asObservable() + .map { itemDictionary -> [Item] in + return Array(itemDictionary.values) + } .distinctUntilChanged { lhList, rhList in return lhList.elementsEqual(rhList) } @@ -41,6 +45,19 @@ class DataStore { switch action { case .list(let list): self.itemList.onNext(list) + case .updated(let item): + self.itemList.take(1) + .map { items in + guard let id = item.id else { + return items + } + + var updatedItems = items + updatedItems[id] = item + return updatedItems + } + .bind(to: self.itemList) + .disposed(by: self.disposeBag) case .locked(let locked): self.locked.onNext(locked) case .initialized(let initialized): @@ -54,13 +71,10 @@ class DataStore { public func onItem(_ itemId: String) -> Observable { return self.itemList.asObservable() - .flatMap { list in - Observable.from(list) - } - .filterByType(class: Item.self) - .filter { item in - return item.id == itemId + .map { items -> Item? in + return items[itemId] } + .filterNil() .distinctUntilChanged() } } diff --git a/lockbox-ios/lockbox-datastore/dswrapper.js b/lockbox-ios/lockbox-datastore/dswrapper.js index 877ba2638..2db3da79e 100644 --- a/lockbox-ios/lockbox-datastore/dswrapper.js +++ b/lockbox-ios/lockbox-datastore/dswrapper.js @@ -68,4 +68,14 @@ class SwiftInteropDataStore extends DataStoreModule.DataStore { } }) } + + async touch(item) { + return super.touch(item).then( function(updatedItem) { + try { + webkit.messageHandlers.UpdateComplete.postMessage(updatedItem) + } catch (err) { + console.log("callback function not available") + } + }) + } } diff --git a/lockbox-iosTests/DataStoreActionIntegrationSpec.swift b/lockbox-iosTests/DataStoreActionIntegrationSpec.swift index 81bae43a0..5cf9a1552 100644 --- a/lockbox-iosTests/DataStoreActionIntegrationSpec.swift +++ b/lockbox-iosTests/DataStoreActionIntegrationSpec.swift @@ -44,6 +44,14 @@ class DataStoreHandlerIntegrationSpec: QuickSpec { xdescribe("DataStore with JavaScript integration") { var unlockValue: DataStoreAction? + let item = Item.Builder() + .title("Wordpress") + .entry(ItemEntry.Builder() + .kind("login") + .username("tjacobson@yahoo.com") + .password("iLUVdawgz") + .build()) + .build() beforeEach { DataStoreActionHandler.shared.unlock(scopedKey: self.scopedKey) @@ -65,8 +73,31 @@ class DataStoreHandlerIntegrationSpec: QuickSpec { .filterByType(class: DataStoreAction.self) .toBlocking().first() - expect(listValue).to(equal(DataStoreAction.list(list: []))) + expect(listValue).to(equal(DataStoreAction.list(list: [:]))) } +// +// it("pushes the updated value when touching an item") { +// let encodedItem = try! encoder.encode(item) +// let jsonString = String(data: encodedItem, encoding: .utf8) +// +// DataStoreActionHandler.shared.webView.evaluateJavaScript("ds.add(\(jsonString))").toBlocking() +// +// DataStoreActionHandler.shared.list() +// +// let listValue = try! Dispatcher.shared.register +// .filterByType(class: DataStoreAction.self) +// .toBlocking().first() as! DataStoreAction +// +// let fullItem = listValue.first +// +// DataStoreActionHandler.shared.touch(fullItem) +// +// let updated = try! Dispatcher.shared.register +// .filterByType(class: DataStoreAction.self) +// .toBlocking().first() +// +// expect(updated.lastUsed = ) +// } it("calls back from javascript after locking & unlocking") { DataStoreActionHandler.shared.lock() diff --git a/lockbox-iosTests/DataStoreActionSpec.swift b/lockbox-iosTests/DataStoreActionSpec.swift index 88c7bca07..ccc873a09 100644 --- a/lockbox-iosTests/DataStoreActionSpec.swift +++ b/lockbox-iosTests/DataStoreActionSpec.swift @@ -732,7 +732,7 @@ class DataStoreActionSpec: QuickSpec { it("pushes an empty list") { expect(self.dispatcher.actionTypeArguments).notTo(beNil()) let arguments = self.dispatcher.actionTypeArguments as! [DataStoreAction] - expect(arguments).to(contain(DataStoreAction.list(list: []))) + expect(arguments).to(contain(DataStoreAction.list(list: [:]))) } } @@ -748,7 +748,7 @@ class DataStoreActionSpec: QuickSpec { it("pushes a list with the valid items") { expect(self.dispatcher.actionTypeArguments).notTo(beNil()) let arguments = self.dispatcher.actionTypeArguments as! [DataStoreAction] - expect(arguments).to(contain(DataStoreAction.list(list: []))) + expect(arguments).to(contain(DataStoreAction.list(list: [:]))) } } @@ -761,14 +761,199 @@ class DataStoreActionSpec: QuickSpec { .entry(ItemEntry.Builder().kind("login").build()) .build() - let message = FakeWKScriptMessage(name: JSCallbackFunction.ListComplete.rawValue, body: [["idvalue", ["foo": 5, "bar": 1]], ["idvalue1", ["foo": 3, "bar": 7]]]) + let message = FakeWKScriptMessage(name: JSCallbackFunction.ListComplete.rawValue, body: [["idvalue1", ["foo": 3, "bar": 7]]]) self.subject.userContentController(self.webView.configuration.userContentController, didReceive: message) } it("pushes the items") { expect(self.dispatcher.actionTypeArguments).notTo(beNil()) let arguments = self.dispatcher.actionTypeArguments as! [DataStoreAction] - expect(arguments).to(contain(DataStoreAction.list(list: [self.parser.item, self.parser.item]))) + expect(arguments).to(contain(DataStoreAction.list(list: [self.parser.item.id!: self.parser.item]))) + } + } + } + } + } + } + } + + describe(".touch()") { + let item = Item.Builder().id("jlkfsdlkjsfd").build() + let encodedItem = try! JSONEncoder().encode(item) + let itemJSONString = String(data: encodedItem, encoding: .utf8) + + describe("when the datastore is not open") { + it("does nothing") { + self.subject.touch(item) + expect(self.dispatcher.actionTypeArguments.count).to(beLessThanOrEqualTo(2)) + expect(self.webView.evaluateJSArgument).to(beNil()) + } + } + + describe("when the datastore is open") { + beforeEach { + let message = FakeWKScriptMessage(name: JSCallbackFunction.OpenComplete.rawValue, body: "opened") + self.subject.userContentController(self.webView.configuration.userContentController, didReceive: message) + } + + describe("when the datastore is not initialized") { + beforeEach { + self.webView.firstBoolSingle = self.scheduler.createHotObservable([next(100, false)]) + .take(1) + .asSingle() + self.subject.touch(item) + self.scheduler.start() + } + + it("pushes the DataStoreNotInitialized error") { + expect(self.dispatcher.actionTypeArguments).notTo(beNil()) + let argument = self.dispatcher.actionTypeArguments.last as! ErrorAction + expect(argument).to(matchErrorAction(ErrorAction(error: DataStoreError.NotInitialized))) + } + } + + describe("when the datastore is initialized but locked") { + beforeEach { + self.webView.firstBoolSingle = self.scheduler.createColdObservable([next(100, true)]) + .take(1) + .asSingle() + self.webView.secondBoolSingle = self.scheduler.createColdObservable([next(100, true)]) + .take(1) + .asSingle() + self.subject.touch(item) + self.scheduler.start() + } + + it("pushes the DataStoreLocked error") { + expect(self.dispatcher.actionTypeArguments).notTo(beNil()) + let argument = self.dispatcher.actionTypeArguments.last as! ErrorAction + expect(argument).to(matchErrorAction(ErrorAction(error: DataStoreError.Locked))) + } + } + + fdescribe("when the datastore is initialized & unlocked") { + beforeEach { + self.webView.firstBoolSingle = self.scheduler.createColdObservable([next(100, true)]) + .take(1) + .asSingle() + self.webView.secondBoolSingle = self.scheduler.createColdObservable([next(100, false)]) + .take(1) + .asSingle() + } + + describe("when the item does not have an ID") { + beforeEach { + self.subject.touch(Item.Builder().build()) + } + + it("dispatches the NoIDPassed error") { + expect(self.webView.evaluateJSArgument).to(beNil()) + expect(self.dispatcher.actionTypeArguments).notTo(beNil()) +// let argument = self.dispatcher.actionTypeArguments.last as! ErrorAction +// expect(argument).to(matchErrorAction(ErrorAction(error: DataStoreError.NoIDPassed))) + } + } + + describe("when the item has an ID") { + describe("when the javascript call results in an error") { + let err = NSError(domain: "badness", code: -1) + + beforeEach { + self.webView.anySingle = self.scheduler.createColdObservable([error(100, err)]) + .take(1) + .asSingle() + + self.subject.touch(item) + self.scheduler.start() + } + + it("evaluates .touch() on the webview datastore") { + expect(self.webView.evaluateJSArgument).notTo(beNil()) + expect(self.webView.evaluateJSArgument).to(equal("\(self.dataStoreName).touch(\(itemJSONString!))")) + } + + it("dispatches the error") { + expect(self.dispatcher.actionTypeArguments).notTo(beNil()) + let argument = self.dispatcher.actionTypeArguments.last as! ErrorAction + expect(argument).to(matchErrorAction(ErrorAction(error: err))) + } + } + + describe("when the javascript call proceeds normally") { + beforeEach { + self.webView.anySingle = self.scheduler.createColdObservable([next(200, "initial success")]) + .take(1) + .asSingle() + + self.subject.touch(item) + self.scheduler.start() + } + + it("evaluates .touch() on the webview datastore") { + expect(self.webView.evaluateJSArgument).notTo(beNil()) + expect(self.webView.evaluateJSArgument).to(equal("\(self.dataStoreName).touch(\(itemJSONString!))")) + } + + describe("getting an unknown callback from javascript") { + beforeEach { + let message = FakeWKScriptMessage(name: "gibberish", body: "something") + self.subject.userContentController(self.webView.configuration.userContentController, didReceive: message) + } + + it("pushes the UnexpectedJavaScriptMethod to the dispatcher") { + expect(self.dispatcher.actionTypeArguments).notTo(beNil()) + let argument = self.dispatcher.actionTypeArguments.last as! ErrorAction + expect(argument).to(matchErrorAction(ErrorAction(error: DataStoreError.UnexpectedJavaScriptMethod))) + } + } + + describe("when the webview does not call back with a dictionary") { + beforeEach { + let message = FakeWKScriptMessage(name: JSCallbackFunction.UpdateComplete.rawValue, body: [1, 2, 3]) + self.subject.userContentController(self.webView.configuration.userContentController, didReceive: message) + } + + it("pushes the UnexpectedType error") { + expect(self.dispatcher.actionTypeArguments).notTo(beNil()) + let argument = self.dispatcher.actionTypeArguments.last as! ErrorAction + expect(argument).to(matchErrorAction(ErrorAction(error: DataStoreError.UnexpectedType))) + } + } + + describe("when the webview calls back with a dictionary") { + describe("when the parser is unable to parse an item from the dictionary") { + beforeEach { + self.parser.itemFromDictionaryShouldThrow = true + let message = FakeWKScriptMessage(name: JSCallbackFunction.UpdateComplete.rawValue, body: ["foo": 5, "bar": 1]) + + self.subject.userContentController(self.webView.configuration.userContentController, didReceive: message) + } + + it("pushes a list with the valid items") { + expect(self.dispatcher.actionTypeArguments).notTo(beNil()) + let arguments = self.dispatcher.actionTypeArguments as! [DataStoreAction] + expect(arguments).to(contain(DataStoreAction.list(list: [:]))) + } + } + + describe("when the parser is able to parse an item from the dictionary") { + beforeEach { + self.parser.itemFromDictionaryShouldThrow = false + self.parser.item = Item.Builder() + .origins(["www.blah.com"]) + .id("kdkjdsfsdf") + .entry(ItemEntry.Builder().kind("login").build()) + .build() + + let message = FakeWKScriptMessage(name: JSCallbackFunction.UpdateComplete.rawValue, body: ["foo": 5, "bar": 1]) + self.subject.userContentController(self.webView.configuration.userContentController, didReceive: message) + } + + it("pushes the item") { + expect(self.dispatcher.actionTypeArguments).notTo(beNil()) + let arguments = self.dispatcher.actionTypeArguments as! [DataStoreAction] + expect(arguments).to(contain(DataStoreAction.updated(item: self.parser.item))) + } } } } @@ -781,10 +966,15 @@ class DataStoreActionSpec: QuickSpec { let itemA = Item.Builder().id("something").build() let itemB = Item.Builder().id("something else").build() - it("updateList is equal based on the contained list") { - expect(DataStoreAction.list(list: [itemA])).to(equal(DataStoreAction.list(list: [itemA]))) - expect(DataStoreAction.list(list: [itemA])).notTo(equal(DataStoreAction.list(list: [itemA, itemA]))) - expect(DataStoreAction.list(list: [itemA])).notTo(equal(DataStoreAction.list(list: [itemB]))) + it("updateList is always equal") { + expect(DataStoreAction.list(list: [itemA.id!: itemA])).to(equal(DataStoreAction.list(list: [itemA.id!: itemA]))) + expect(DataStoreAction.list(list: [itemA.id!: itemA])).to(equal(DataStoreAction.list(list: [itemA.id!: itemA, itemA.id!: itemA]))) + expect(DataStoreAction.list(list: [itemA.id!: itemA])).to(equal(DataStoreAction.list(list: [itemB.id!: itemB]))) + } + + it("update is equal based on the contained item") { + expect(DataStoreAction.updated(item: itemA)).to(equal(DataStoreAction.updated(item: itemA))) + expect(DataStoreAction.updated(item: itemA)).notTo(equal(DataStoreAction.updated(item: itemB))) } it("initialize is equal based on the contained boolean") { diff --git a/lockbox-iosTests/DataStoreSpec.swift b/lockbox-iosTests/DataStoreSpec.swift index 9de1a5b23..5a2b4544c 100644 --- a/lockbox-iosTests/DataStoreSpec.swift +++ b/lockbox-iosTests/DataStoreSpec.swift @@ -35,13 +35,15 @@ class DataStoreSpec: QuickSpec { describe("onItemList") { var itemListObserver = self.scheduler.createObserver([Item].self) + let itemAid = "kljsdflkjsd" + let itemBid = "dqwkldsfkj" let itemList = [ - Item.Builder() - .id("kljsdflkjsd") + itemAid: Item.Builder() + .id(itemAid) .origins(["bowl.com"]) .build(), - Item.Builder() - .id("dqwkldsfkj") + itemBid: Item.Builder() + .id(itemBid) .origins(["plate.com"]) .build() ] @@ -58,7 +60,7 @@ class DataStoreSpec: QuickSpec { it("pushes dispatched lists to observers") { expect(itemListObserver.events.last).notTo(beNil()) - expect(itemListObserver.events.last!.value.element).to(equal(itemList)) + expect(itemListObserver.events.last!.value.element).to(equal(Array(itemList.values))) } it("doesn't push the same list twice in a row") { @@ -67,7 +69,8 @@ class DataStoreSpec: QuickSpec { } it("pushes subsequent different lists") { - let newItemList = [Item.Builder().id("wwkjlkjm").build()] + itemList + var newItemList = itemList + newItemList["wwkjlkjm"] = Item.Builder().id("wwkjlkjm").build() self.dispatcher.fakeRegistration.onNext(DataStoreAction.list(list: newItemList)) expect(itemListObserver.events.count).to(equal(2)) @@ -87,8 +90,8 @@ class DataStoreSpec: QuickSpec { .origins(["bowl.com"]) .build() let itemList = [ - item, - Item.Builder() + itemId: item, + "dqwkldsfkj": Item.Builder() .id("dqwkldsfkj") .origins(["plate.com"]) .build() @@ -110,14 +113,14 @@ class DataStoreSpec: QuickSpec { } it("doesn't push the item if it is unchanged") { - self.dispatcher.fakeRegistration.onNext(DataStoreAction.list(list: [item])) + self.dispatcher.fakeRegistration.onNext(DataStoreAction.list(list: [itemId: item])) expect(itemObserver.events.count).to(equal(1)) } it("pushes the item again if it is changed") { let changedItem = Item.Builder().id(itemId) .origins(["cat.com"]).build() - let newItemList = [changedItem, Item.Builder().id("fdssdf").build()] + let newItemList = [itemId: changedItem, "fdssdf": Item.Builder().id("fdssdf").build()] self.dispatcher.fakeRegistration.onNext(DataStoreAction.list(list: newItemList)) expect(itemObserver.events.count).to(equal(2)) diff --git a/lockbox-iosTests/FxAStoreSpec.swift b/lockbox-iosTests/FxAStoreSpec.swift index fb61ea4ae..765c9f629 100644 --- a/lockbox-iosTests/FxAStoreSpec.swift +++ b/lockbox-iosTests/FxAStoreSpec.swift @@ -57,7 +57,7 @@ class FxAStoreSpec: QuickSpec { } it("does not push non-FxADisplayAction actions") { - self.dispatcher.fakeRegistration.onNext(DataStoreAction.list(list: [])) + self.dispatcher.fakeRegistration.onNext(DataStoreAction.list(list: [:])) expect(displayObserver.events.count).to(be(1)) } }