diff --git a/Package.swift b/Package.swift index 4f3fe068..fbaee8c0 100644 --- a/Package.swift +++ b/Package.swift @@ -72,7 +72,7 @@ let package = Package( plugins: [.plugin(name: "TISwiftLintPlugin")]), .target(name: "TIFoundationUtils", - dependencies: ["TISwiftUtils"], + dependencies: ["TISwiftUtils", "TILogging"], path: "TIFoundationUtils", exclude: ["TIFoundationUtils.app"], plugins: [.plugin(name: "TISwiftLintPlugin")]), @@ -148,7 +148,7 @@ let package = Package( path: "Tests/TITextProcessingTests"), .testTarget( name: "TIFoundationUtilsTests", - dependencies: ["TIFoundationUtils", "TISwiftUtils"], + dependencies: ["TIFoundationUtils", "TISwiftUtils", "TILogging"], path: "Tests/TIFoundationUtilsTests") ] ) diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift index 7620f8cf..fc095f2f 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift @@ -32,7 +32,14 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS where StoreContent == Value? { let getClosure: GetClosure = { container in - try? container.codableObject(forKey: key, decoder: decoder).get() + // If source storage has value and target doesn't we can't migrate it from storate container + try? container.codableObject(forKey: key, decoder: decoder) + .flatMap { + let _ = container.set(encodableObject: $0, forKey: key, encoder: encoder) + + return .success($0) + } + .get() } let setClosure: SetClosure = { container, value in @@ -50,7 +57,18 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS where StoreContent == Value? { let getClosure: GetClosure = { container in - container.codableObject(forKey: key, defaultValue: wrappedValue, decoder: decoder) + // If source storage has value and target doesn't we can't migrate it from storate container + try? container.codableObject(forKey: key, decoder: decoder) + .flatMap { + let _ = container.set(encodableObject: $0, forKey: key, encoder: encoder) + + return .success($0) + } + .flatMapError { _ in + container.set(encodableObject: wrappedValue, forKey: key, encoder: encoder) + .flatMap { _ in .success(wrappedValue) } + } + .get() } let setClosure: SetClosure = { container, value in diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift index 4603f93e..73a2119d 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift @@ -33,8 +33,6 @@ open class BaseSingleValueStorage: SingleValueStorage { public let getValueClosure: GetValueClosure public let storeValueClosure: StoreValueClosure - public var migrationStorage: StorageType? - public init(storage: StorageType, storageKey: StorageKey, hasValueClosure: @escaping HasValueClosure, @@ -53,43 +51,18 @@ open class BaseSingleValueStorage: SingleValueStorage { // MARK: - SingleValueStorage open func hasStoredValue() -> Bool { - if let migrationStorage, (try? hasValueClosure(migrationStorage, storageKey).get()) == true { - let _ = deleteValueClosure(storage, storageKey) - return true - } - - return (try? hasValueClosure(storage, storageKey).get()) ?? false + (try? hasValueClosure(storage, storageKey).get()) ?? false } open func store(value: ValueType) -> Result { - let targetStorate = migrationStorage ?? storage - - return storeValueClosure(targetStorate, value, storageKey) + storeValueClosure(storage, value, storageKey) } open func getValue() -> Result { - guard let migrationStorage else { - return getValueClosure(storage, storageKey) - } - - if let value = try? getValueClosure(migrationStorage, storageKey).get() { - return .success(value) - } - - let result = getValueClosure(storage, storageKey) - - if case let .success(value) = result, case .success = store(value: value) { - let _ = deleteValueClosure(storage, storageKey) - } - - return result + getValueClosure(storage, storageKey) } open func deleteValue() -> Result { - if let migrationStorage { - return deleteValueClosure(migrationStorage, storageKey) - } - - return deleteValueClosure(storage, storageKey) + deleteValueClosure(storage, storageKey) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift index a1a45a32..f658f27c 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift @@ -20,6 +20,8 @@ // THE SOFTWARE. // +import TILogging + open class BaseMigratingSingleValueStorage: SingleValueStorage where SourceStorage.ErrorType == TargetStorage.ErrorType, @@ -30,12 +32,15 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, public let sourceStorage: SourceStorage public let targetStorage: TargetStorage + public let errorLogger: ErrorLogger public init(sourceStorage: SourceStorage, - targetStorage: TargetStorage) { + targetStorage: TargetStorage, + errorLogger: ErrorLogger = TIFoundationLogger(category: "BaseMigratingSingleValueStorage")) { self.sourceStorage = sourceStorage self.targetStorage = targetStorage + self.errorLogger = errorLogger } // MARK: - SingleValueStorage @@ -46,6 +51,11 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, open func store(value: ValueType) -> Result { let _ = sourceStorage.deleteValue() + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return .failure($0) + } return targetStorage.store(value: value) } @@ -54,6 +64,11 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, targetStorage.getValue() .flatMap { let _ = sourceStorage.deleteValue() + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return .failure($0) + } return .success($0) } @@ -79,5 +94,20 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, return .success($0) } + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return sourceStorage.deleteValue() + } + } +} + +public extension SingleValueStorage where ErrorType == StorageError { + + func migrating(to targetStorage: S) -> BaseMigratingSingleValueStorage + where ErrorType == S.ErrorType, + ValueType == S.ValueType { + + BaseMigratingSingleValueStorage(sourceStorage: self, targetStorage: targetStorage) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift index 766f5d47..4986607e 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift @@ -21,18 +21,39 @@ // import TISwiftUtils +import TILogging open class BaseCodableMigratingStorageContainer: MigratingStorageContainer, CodableKeyValueStorage where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStorage { + public enum CodableTypeError: Error { + case unableToConvertToEncodable + } + + public let errorLogger: ErrorLogger + + public init(sourceStorage: SourceStorage, + targetStorage: TargetStorage, + errorLogger: ErrorLogger = TIFoundationLogger(category: "BaseCodableMigratingStorageContainer")) { + + self.errorLogger = errorLogger + + super.init(sourceStorage: sourceStorage, targetStorage: targetStorage) + } + open func codableObject(forKey key: StorageKey, decoder: CodableKeyValueDecoder) -> Result { targetStorage.codableObject(forKey: key, decoder: decoder) .flatMap { - removeSourceValue(forKey: key) + let _ = removeSourceValue(forKey: key) + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return .failure($0) + } return .success($0) } @@ -45,19 +66,26 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora forKey key: StorageKey, encoder: CodableKeyValueEncoder) -> Result { - removeSourceValue(forKey: key) + let _ = removeSourceValue(forKey: key) + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return .failure($0) + } return targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) } open func removeCodableValue(forKey key: StorageKey) -> Result { targetStorage.removeCodableValue(forKey: key) - .flatMap { - if let value = try? sourceStorage.hasCodableValue(forKey: key).get(), value { - return sourceStorage.removeCodableValue(forKey: key) - } + .flatMap { value in + sourceStorage.removeCodableValue(forKey: key) + .flatMapError { _ in .success(value) } + } + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) - return .success($0) + return removeSourceValue(forKey: key) } } @@ -70,7 +98,8 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora // MARK: - Open methods - open func removeSourceValue(forKey key: StorageKey) { + open func removeSourceValue(forKey key: StorageKey) -> Result { // override in subclasses + .failure(.valueNotFound) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift index 97da6482..9a5e02f3 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift @@ -24,7 +24,7 @@ import Foundation public final class DefaultsMigratingStorageContainer: BaseCodableMigratingStorageContainer { - public override func removeSourceValue(forKey key: StorageKey) { - let _ = sourceStorage.removeValue(forKey: key) + public override func removeSourceValue(forKey key: StorageKey) -> Result { + sourceStorage.removeValue(forKey: key) } } diff --git a/TIFoundationUtils/PlaygroundPodfile b/TIFoundationUtils/PlaygroundPodfile index 95c2fd95..8a8cb08a 100644 --- a/TIFoundationUtils/PlaygroundPodfile +++ b/TIFoundationUtils/PlaygroundPodfile @@ -5,5 +5,6 @@ target 'TIFoundationUtils' do use_frameworks! pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec' pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' end diff --git a/TIFoundationUtils/TIFoundationLogger.swift b/TIFoundationUtils/TIFoundationLogger.swift new file mode 100644 index 00000000..dbfeb1e2 --- /dev/null +++ b/TIFoundationUtils/TIFoundationLogger.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TILogging +import os + +public final class TIFoundationLogger: DefaultOSLogErrorLogger { + public init(category: String) { + super.init(log: OSLog(subsystem: "TIFoundationUtils", category: category)) + } +} diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index a8f783b2..9e0f609b 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -20,5 +20,6 @@ Pod::Spec.new do |s| end s.dependency 'TISwiftUtils', s.version.to_s + s.dependency 'TILogging', s.version.to_s s.framework = 'Foundation' end diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift index 0a8ff79e..f10fc43e 100644 --- a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift @@ -25,7 +25,7 @@ import KeychainAccess public final class KeychainMigratingStorageContainer: BaseCodableMigratingStorageContainer { - public override func removeSourceValue(forKey key: StorageKey) { - let _ = sourceStorage.removeValue(forKey: key) + public override func removeSourceValue(forKey key: StorageKey) -> Result { + sourceStorage.removeValue(forKey: key) } } diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift index 845945ed..2648322f 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift @@ -127,10 +127,6 @@ case let .failure(storageError): let groupKeychain = Keychain(service: "app.group.identifier") let targetApiTokenKeychainStorage = StringValueKeychainStorage(keychain: groupKeychain, storageKey: .apiToken) -let migratingApiTokenKeychainStorage = BaseMigratingSingleValueStorage(sourceStorage: apiTokenKeychainStorage, - targetStorage: targetApiTokenKeychainStorage) +let migratingApiTokenKeychainStorage = apiTokenKeychainStorage.migrating(to: targetApiTokenKeychainStorage) let token = migratingApiTokenKeychainStorage.getValue() - -//: Если нет необходимости в создании target storage, можно воспользоваться переменной `migrationStorage`, которая есть у каждого хранилища, наследованного от `BaseSingleValueStorage` -apiTokenKeychainStorage.migrationStorage = groupKeychain diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index db3631a9..ba01a447 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/Sources/**/*' + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end s.dependency 'TIUIElements', s.version.to_s s.dependency 'TISwiftUtils', s.version.to_s diff --git a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift index 7621ab11..bf368892 100644 --- a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift @@ -33,61 +33,192 @@ final class MigratingBackingStoreTests: XCTestCase { override func setUp() { profile = nil + let _ = MockMigratingStorageContainer.defaultContainer.sourceStorage.removeValue(forKey: .profile) + let _ = MockMigratingStorageContainer.defaultContainer.targetStorage.removeValue(forKey: .profile) } - // MARK: - Tests + // MARK: - Read Tests - func testMigrationOnGetValue() throws { - let oldProfile = Profile(name: "some", age: 0) + // Precondition - source: ✅ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testGetValue() throws { + let oldProfile = Profile(name: "old", age: 0) + let newProfile = Profile(name: "new", age: 0) let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) - XCTAssertEqual(profile, oldProfile) + XCTAssertEqual(profile, newProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) } - func testGetValueFromTarget() throws { - let newProfile = Profile(name: "any", age: 1) + // Precondition - source: ❌ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testGetValueWithNoSource() throws { + let newProfile = Profile(name: "new", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) XCTAssertEqual(profile, newProfile) - XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) } - func testGetValue() throws { - let newProfile = Profile(name: "any", age: 1) - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage - - profile = newProfile - - XCTAssertEqual(profile, newProfile) - XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) - } - - func testMigrationOnStoreValue() throws { - let oldProfile = Profile(name: "some", age: 0) - let newProfile = Profile(name: "any", age: 1) + // Precondition - source: ✅ target: ❌ + // PostCondition - source: ❌ target: ✅ + func testGetValueWithNoTarget() throws { + let oldProfile = Profile(name: "old", age: 0) let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) - profile = newProfile - XCTAssertEqual(profile, newProfile) - XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + XCTAssertEqual(profile, oldProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) } - func testStoreValueFromTarget() throws { - let oldProfile = Profile(name: "some", age: 0) - let newProfile = Profile(name: "any", age: 1) + // Precondition - source: ❌ target: ❌ + // PostCondition - source: ❌ target: ❌ + func testGetValueWithNoValues() throws { + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage - let _ = targetStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) - profile = newProfile + XCTAssertEqual(profile, nil) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) + } - XCTAssertEqual(profile, newProfile) - XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + // MARK: - Write Tests + + // Precondition - source: ✅ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testStoreValue() throws { + let oldProfile = Profile(name: "old", age: 0) + let newProfile = Profile(name: "new", age: 0) + let currentProfile = Profile(name: "name", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) + profile = currentProfile + + XCTAssertEqual(profile, currentProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertEqual(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), currentProfile) + } + + // Precondition - source: ❌ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testStoreValueWithNoSource() throws { + let newProfile = Profile(name: "new", age: 0) + let currentProfile = Profile(name: "name", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) + profile = currentProfile + + XCTAssertEqual(profile, currentProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertEqual(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), currentProfile) + } + + // Precondition - source: ✅ target: ❌ + // PostCondition - source: ❌ target: ✅ + func testStoreValueWithNoTarget() throws { + let oldProfile = Profile(name: "old", age: 0) + let currentProfile = Profile(name: "name", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + profile = currentProfile + + XCTAssertEqual(profile, currentProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertEqual(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), currentProfile) + } + + // Precondition - source: ❌ target: ❌ + // PostCondition - source: ❌ target: ✅ + func testStoreValueWithNoValues() throws { + let currentProfile = Profile(name: "name", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + profile = currentProfile + + XCTAssertEqual(profile, currentProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertEqual(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), currentProfile) + } + + // MARK: - Delete Tests + + // Precondition - source: ✅ target: ✅ + // PostCondition - source: ❌ target: ❌ + func testDeleteValue() throws { + let oldProfile = Profile(name: "old", age: 0) + let newProfile = Profile(name: "new", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) + profile = nil + + XCTAssertEqual(profile, nil) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) + } + + // Precondition - source: ❌ target: ✅ + // PostCondition - source: ❌ target: ❌ + func testDeleteValueWithNoSource() throws { + let newProfile = Profile(name: "new", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) + profile = nil + + XCTAssertEqual(profile, nil) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) + } + + // Precondition - source: ✅ target: ❌ + // PostCondition - source: ❌ target: ❌ + func testDeleteValueWithNoTarget() throws { + let oldProfile = Profile(name: "old", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + profile = nil + + XCTAssertEqual(profile, nil) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) + } + + // Precondition - source: ❌ target: ❌ + // PostCondition - source: ❌ target: ❌ + func testDeleteValueWithNoValues() throws { + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + profile = nil + + XCTAssertEqual(profile, nil) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) } } diff --git a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift index 5b00dcba..3f3ceab5 100644 --- a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift @@ -32,136 +32,171 @@ final class MigratingSingleValueStorageTests: XCTestCase { storageKey: .refreshToken) public lazy var refreshToken = BaseMigratingSingleValueStorage(sourceStorage: sourceRefreshTokenStorage, - targetStorage: targetRefreshTokenStorage) + targetStorage: targetRefreshTokenStorage, + errorLogger: MockStorageLogger.defaultLogger) override func setUp() { let _ = sourceRefreshTokenStorage.deleteValue() let _ = targetRefreshTokenStorage.deleteValue() + let _ = MockStorageLogger.defaultLogger.getError() } - // MARK: - Tests - - func testMigrationOnGetValue() throws { - let oldToken = "old-token" - - let _ = sourceRefreshTokenStorage.store(value: oldToken) - - XCTAssertEqual(try? refreshToken.getValue().get(), oldToken) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), oldToken) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testGetValueFromTarget() throws { - let token = "new-token" - - let _ = targetRefreshTokenStorage.store(value: token) - - XCTAssertEqual(try? refreshToken.getValue().get(), token) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } + // MARK: - Read Tests + // Precondition - source: ✅ target: ✅ + // PostCondition - source: ❌ target: ✅ func testGetValue() throws { - let token = "token" - - let _ = refreshToken.store(value: token) - - XCTAssertEqual(try? refreshToken.getValue().get(), token) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testMigrationOnHasValue() throws { - let oldToken = "old-token" - - let _ = sourceRefreshTokenStorage.store(value: oldToken) - - XCTAssertTrue(refreshToken.hasStoredValue()) - XCTAssertFalse(targetRefreshTokenStorage.hasStoredValue()) - XCTAssertTrue(sourceRefreshTokenStorage.hasStoredValue()) - } - - func testHasValueFromTarget() throws { - let token = "new-token" - - let _ = targetRefreshTokenStorage.store(value: token) - - XCTAssertTrue(refreshToken.hasStoredValue()) - XCTAssertTrue(targetRefreshTokenStorage.hasStoredValue()) - XCTAssertFalse(sourceRefreshTokenStorage.hasStoredValue()) - } - - func testHasValue() throws { - let token = "token" - - let _ = refreshToken.store(value: token) - - XCTAssertTrue(refreshToken.hasStoredValue()) - XCTAssertTrue(targetRefreshTokenStorage.hasStoredValue()) - XCTAssertFalse(sourceRefreshTokenStorage.hasStoredValue()) - } - - func testMigrationOnDeleteValue() throws { - let oldToken = "old-token" - - let _ = sourceRefreshTokenStorage.store(value: oldToken) - - XCTAssertNoThrow(try refreshToken.deleteValue().get()) - XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testDeleteValueFromTarget() throws { - let token = "new-token" - - let _ = targetRefreshTokenStorage.store(value: token) - - XCTAssertNoThrow(try refreshToken.deleteValue().get()) - XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testDeleteValue() throws { - let token = "token" - - let _ = refreshToken.store(value: token) - - XCTAssertNoThrow(try refreshToken.deleteValue().get()) - XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testMigrationOnStoreValue() throws { let oldToken = "old-token" let newToken = "new-token" let _ = sourceRefreshTokenStorage.store(value: oldToken) - let _ = refreshToken.store(value: newToken) + let _ = targetRefreshTokenStorage.store(value: newToken) + XCTAssertEqual(try refreshToken.getValue().get(), newToken) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - XCTAssertEqual(try? refreshToken.getValue().get(), newToken) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), newToken) + XCTAssertNoThrow(try targetRefreshTokenStorage.getValue().get()) } - func testStoreValueFromTarget() throws { - let oldToken = "old-token" + // Precondition - source: ❌ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testGetValueWithNoSource() throws { let newToken = "new-token" - let _ = targetRefreshTokenStorage.store(value: oldToken) - let _ = refreshToken.store(value: newToken) + let _ = targetRefreshTokenStorage.store(value: newToken) + XCTAssertEqual(try refreshToken.getValue().get(), newToken) + XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - XCTAssertEqual(try? refreshToken.getValue().get(), newToken) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), newToken) + XCTAssertNoThrow(try targetRefreshTokenStorage.getValue().get()) } + // Precondition - source: ✅ target: ❌ + // PostCondition - source: ❌ target: ✅ + func testGetValueWithNoTarget() throws { + let oldToken = "old-token" + + let _ = sourceRefreshTokenStorage.store(value: oldToken) + + XCTAssertEqual(try refreshToken.getValue().get(), oldToken) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertNoThrow(try targetRefreshTokenStorage.getValue().get()) + } + + // Precondition - source: ❌ target: ❌ + // PostCondition - source: ❌ target: ❌ + func testGetValueWithNoValues() throws { + XCTAssertThrowsError(try refreshToken.getValue().get()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) + } + + // MARK: - Write Tests + + // Precondition - source: ✅ target: ✅ + // PostCondition - source: ❌ target: ✅ func testStoreValue() throws { - let token = "token" + let oldToken = "old-token" + let newToken = "new-token" + let currentToken = "token" - let _ = refreshToken.store(value: token) + let _ = sourceRefreshTokenStorage.store(value: oldToken) + let _ = targetRefreshTokenStorage.store(value: newToken) - XCTAssertEqual(try? refreshToken.getValue().get(), token) + XCTAssertNoThrow(try refreshToken.store(value: currentToken).get()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) + XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken) + } + + // Precondition - source: ❌ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testStoreValueWithNoSource() throws { + let newToken = "new-token" + let currentToken = "token" + + let _ = targetRefreshTokenStorage.store(value: newToken) + + XCTAssertNoThrow(try refreshToken.store(value: currentToken).get()) + XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken) + } + + // Precondition - source: ✅ target: ❌ + // PostCondition - source: ❌ target: ✅ + func testStoreValueWithNoTarget() throws { + let oldToken = "old-token" + let currentToken = "token" + + let _ = sourceRefreshTokenStorage.store(value: oldToken) + + XCTAssertNoThrow(try refreshToken.store(value: currentToken).get()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken) + } + + // Precondition - source: ❌ target: ❌ + // PostCondition - source: ❌ target: ✅ + func testStoreValueWithNoValues() throws { + let currentToken = "token" + + XCTAssertNoThrow(try refreshToken.store(value: currentToken).get()) + XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken) + } + + // MARK: - Delete Tests + + // Precondition - source: ✅ target: ✅ + // PostCondition - source: ❌ target: ❌ + func testDeleteValue() throws { + let oldToken = "old-token" + let newToken = "new-token" + + let _ = sourceRefreshTokenStorage.store(value: oldToken) + let _ = targetRefreshTokenStorage.store(value: newToken) + + XCTAssertNoThrow(try refreshToken.deleteValue().get()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) + } + + // Precondition - source: ❌ target: ✅ + // PostCondition - source: ❌ target: ❌ + func testDeleteValueWithNoSource() throws { + let newToken = "new-token" + + let _ = targetRefreshTokenStorage.store(value: newToken) + + XCTAssertNoThrow(try refreshToken.deleteValue().get()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) + } + + // Precondition - source: ✅ target: ❌ + // PostCondition - source: ❌ target: ❌ + func testDeleteValueWithNoTarget() throws { + let oldToken = "old-token" + + let _ = sourceRefreshTokenStorage.store(value: oldToken) + + XCTAssertNoThrow(try refreshToken.deleteValue().get()) + XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) + } + + // Precondition - source: ❌ target: ❌ + // PostCondition - source: ❌ target: ❌ + func testDeleteValueWithNoValues() throws { + XCTAssertThrowsError(try refreshToken.deleteValue().get()) + XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) } } diff --git a/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift b/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift index 9bd84c5a..cbb15236 100644 --- a/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift +++ b/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift @@ -33,7 +33,11 @@ class BaseSingleValueMockStorage: BaseSingleValueStorage(forKey key: StorageKey) { - let _ = sourceStorage.removeValue(forKey: key) + override func removeSourceValue(forKey key: StorageKey) -> Result { + sourceStorage.removeValue(forKey: key) } } diff --git a/Tests/TIFoundationUtilsTests/Mocks/MockStorageLogger.swift b/Tests/TIFoundationUtilsTests/Mocks/MockStorageLogger.swift new file mode 100644 index 00000000..bffc12c0 --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Mocks/MockStorageLogger.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIFoundationUtils +import TILogging +import os + +final class MockStorageLogger: DefaultOSLogErrorLogger { + + static let defaultLogger = MockStorageLogger(category: "MockStorageLogger") + + private var recentError: StorageError? + + init(category: String) { + super.init(log: OSLog(subsystem: "TIFoundationUtilsTests", category: category)) + } + + func getError() -> StorageError? { + defer { recentError = nil } + + return recentError + } + + override func log(error: Error, file: StaticString, line: Int) { + recentError = error as? StorageError + } +} diff --git a/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift b/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift index 8251f1fd..cbfcda33 100644 --- a/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift +++ b/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift @@ -34,7 +34,7 @@ final class StringSingleValueMockStorage: BaseSingleValueMockStorage { } let storeValueClosure: StoreValueClosure = { storage, value, storageKey in - .success(storage.store(value: value, forKey: storageKey)) + .success(storage.store(value: value, forKey: storageKey)) } super.init(storage: storage, diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md index 9a8dcc87..2a5aed06 100644 --- a/docs/tikeychainutils/singlevaluestorage.md +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -125,14 +125,7 @@ case let .failure(storageError): let groupKeychain = Keychain(service: "app.group.identifier") let targetApiTokenKeychainStorage = StringValueKeychainStorage(keychain: groupKeychain, storageKey: .apiToken) -let migratingApiTokenKeychainStorage = BaseMigratingSingleValueStorage(sourceStorage: apiTokenKeychainStorage, - targetStorage: targetApiTokenKeychainStorage) +let migratingApiTokenKeychainStorage = apiTokenKeychainStorage.migrating(to: targetApiTokenKeychainStorage) let token = migratingApiTokenKeychainStorage.getValue() ``` - -Если нет необходимости в создании target storage, можно воспользоваться переменной `migrationStorage`, которая есть у каждого хранилища, наследованного от `BaseSingleValueStorage` - -```swift -apiTokenKeychainStorage.migrationStorage = groupKeychain -```