diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 7b80b42c13..1a66a873df 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -268,6 +268,7 @@ 4FBBC5682A61E42F0077281F /* NonEmptyStringDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */; }; 4FC083292A4A35FB00A97089 /* Integer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC083282A4A35FB00A97089 /* Integer+Extensions.swift */; }; 4FC0832B2A4A361700A97089 /* IntegerExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC0832A2A4A361700A97089 /* IntegerExtensionsTests.swift */; }; + 4FC6F8892A73E445002139B2 /* PostedTransactionCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC6F8882A73E445002139B2 /* PostedTransactionCacheTests.swift */; }; 4FC972172A712DCC008593DE /* CachingProductsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC972162A712DCC008593DE /* CachingProductsManagerTests.swift */; }; 4FCBA84F2A15391B004134BD /* SnapshotTesting+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576C8A9127D27DDD0058FA6E /* SnapshotTesting+Extensions.swift */; }; 4FCBA8512A153940004134BD /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 4FCBA8502A153940004134BD /* SnapshotTesting */; }; @@ -981,6 +982,7 @@ 4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonEmptyStringDecodable.swift; sourceTree = ""; }; 4FC083282A4A35FB00A97089 /* Integer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Integer+Extensions.swift"; sourceTree = ""; }; 4FC0832A2A4A361700A97089 /* IntegerExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerExtensionsTests.swift; sourceTree = ""; }; + 4FC6F8882A73E445002139B2 /* PostedTransactionCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostedTransactionCacheTests.swift; sourceTree = ""; }; 4FC972162A712DCC008593DE /* CachingProductsManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachingProductsManagerTests.swift; sourceTree = ""; }; 4FCBA8522A1539D0004134BD /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; }; 4FCEEA5D2A379B80002C2112 /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = ""; }; @@ -2145,6 +2147,7 @@ children = ( 37E35D87B7E6F91E27E98F42 /* DeviceCacheTests.swift */, 37E35E3250FBBB03D92E06EC /* InMemoryCachedObjectTests.swift */, + 4FC6F8882A73E445002139B2 /* PostedTransactionCacheTests.swift */, ); path = Caching; sourceTree = ""; @@ -3454,6 +3457,7 @@ 5766AA5A283D4CAB00FA6091 /* IgnoreHashableTests.swift in Sources */, B36824BF268FBC8700957E4C /* SubscriberAttributeTests.swift in Sources */, 351B51BC26D450E800BD2BD7 /* OfferingsTests.swift in Sources */, + 4FC6F8892A73E445002139B2 /* PostedTransactionCacheTests.swift in Sources */, 5796A38827D6B85900653165 /* BackendPostReceiptDataTests.swift in Sources */, 5752E8482892DC500069281E /* ErrorUtilsTests.swift in Sources */, 57488BF229CB84D40000EE7E /* BackendOfflineEntitlementsTests.swift in Sources */, diff --git a/Sources/Caching/DeviceCache.swift b/Sources/Caching/DeviceCache.swift index 5cb5d98bbf..e58897718c 100644 --- a/Sources/Caching/DeviceCache.swift +++ b/Sources/Caching/DeviceCache.swift @@ -56,7 +56,7 @@ class DeviceCache { self.userDefaults.write { var value: Value = $0.value(forKey: key) ?? defaultValue updater(&value) - $0.set(value, forKey: key) + $0.set(codable: value, forKey: key) } } @@ -163,11 +163,8 @@ class DeviceCache { func cache(offerings: Offerings, appUserID: String) { self.cacheInMemory(offerings: offerings) - - if let jsonData = try? JSONEncoder.default.encode(value: offerings.response, logErrors: true) { - self.userDefaults.write { - $0.set(jsonData, forKey: CacheKey.offerings(appUserID)) - } + self.userDefaults.write { + $0.set(codable: offerings.response, forKey: CacheKey.offerings(appUserID)) } } @@ -572,18 +569,27 @@ private extension DeviceCache { _ userDefaults: UserDefaults, productEntitlementMapping mapping: ProductEntitlementMapping ) { - guard let data = try? JSONEncoder.default.encode(value: mapping, logErrors: true) else { - return + if userDefaults.set(codable: mapping, + forKey: CacheKeys.productEntitlementMapping) { + userDefaults.set(Date(), forKey: CacheKeys.productEntitlementMappingLastUpdated) } - - userDefaults.set(data, forKey: CacheKeys.productEntitlementMapping) - userDefaults.set(Date(), forKey: CacheKeys.productEntitlementMappingLastUpdated) } } fileprivate extension UserDefaults { + /// - Returns: whether the value could be saved + @discardableResult + func set(codable: T, forKey key: DeviceCacheKeyType) -> Bool { + guard let data = try? JSONEncoder.default.encode(value: codable, logErrors: true) else { + return false + } + + self.set(data, forKey: key) + return true + } + func value(forKey key: DeviceCacheKeyType) -> T? { guard let data = self.data(forKey: key) else { return nil diff --git a/Sources/Caching/PostedTransactionCache.swift b/Sources/Caching/PostedTransactionCache.swift index 195b3ab23c..f2c48b8108 100644 --- a/Sources/Caching/PostedTransactionCache.swift +++ b/Sources/Caching/PostedTransactionCache.swift @@ -20,9 +20,6 @@ protocol PostedTransactionCacheType { } -// TODO: tests -// TODO: integrate - final class PostedTransactionCache: PostedTransactionCacheType { private typealias StoredTransactions = Set diff --git a/Tests/UnitTests/Caching/PostedTransactionCacheTests.swift b/Tests/UnitTests/Caching/PostedTransactionCacheTests.swift new file mode 100644 index 0000000000..43e14c3782 --- /dev/null +++ b/Tests/UnitTests/Caching/PostedTransactionCacheTests.swift @@ -0,0 +1,57 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// PostedTransactionCacheTests.swift +// +// Created by Nacho Soto on 7/28/23. + +import Foundation +import Nimble +import XCTest + +@testable import RevenueCat + +class PostedTransactionCacheTests: TestCase { + + private var userDefaults: UserDefaults! + private var deviceCache: DeviceCache! + private var cache: PostedTransactionCache! + + override func setUpWithError() throws { + try super.setUpWithError() + + self.userDefaults = try XCTUnwrap(.init(suiteName: UUID().uuidString)) + self.deviceCache = .init(sandboxEnvironmentDetector: MockSandboxEnvironmentDetector(isSandbox: true), + userDefaults: self.userDefaults) + self.cache = .init(deviceCache: self.deviceCache) + } + + func testNoPostedTransactions() { + expect(self.cache.hasPostedTransaction(MockStoreTransaction())) == false + } + + func testSavesFirstTransaction() { + let transaction = MockStoreTransaction() + + self.cache.savePostedTransaction(transaction) + expect(self.cache.hasPostedTransaction(transaction)) == true + } + + func testSaveMultipleTransactions() { + let transaction1 = MockStoreTransaction() + let transaction2 = MockStoreTransaction() + + self.cache.savePostedTransaction(transaction1) + self.cache.savePostedTransaction(transaction2) + + expect(self.cache.hasPostedTransaction(transaction1)) == true + expect(self.cache.hasPostedTransaction(transaction2)) == true + } + +} diff --git a/Tests/UnitTests/Mocks/MockDeviceCache.swift b/Tests/UnitTests/Mocks/MockDeviceCache.swift index b8c91bdd8f..7b3e2decb2 100644 --- a/Tests/UnitTests/Mocks/MockDeviceCache.swift +++ b/Tests/UnitTests/Mocks/MockDeviceCache.swift @@ -12,6 +12,37 @@ class MockDeviceCache: DeviceCache { userDefaults: MockUserDefaults()) } + // MARK: - generic methods + + var stubbedUpdateValues: [Any] = [] + var invokedUpdateKey: Bool = false + var invokedUpdateKeyParameters: [(key: String, newValue: Any)] = [] + + override func update( + key: Key, + default defaultValue: Value, + updater: @Sendable (inout Value) -> Void + ) { + // swiftlint:disable:next force_cast + var value = (self.stubbedUpdateValues.popFirst() as! Value?) ?? defaultValue + updater(&value) + + self.invokedUpdateKey = true + self.invokedUpdateKeyParameters.append((key: key.rawValue, newValue: value)) + } + + var stubbedValueForKey: [Any] = [] + var invokedValueForKey: Bool = false + var invokedValueForKeyParameters: [String] = [] + + override func value(for key: Key) -> Value? { + self.invokedValueForKey = true + self.invokedValueForKeyParameters.append(key.rawValue) + + // swiftlint:disable:next force_cast + return self.stubbedValueForKey.popFirst() as! Value? + } + // MARK: appUserID var stubbedAppUserID: String?