diff --git a/DevCycle/Models/Cache.swift b/DevCycle/Models/Cache.swift index f840020..9fb35a8 100644 --- a/DevCycle/Models/Cache.swift +++ b/DevCycle/Models/Cache.swift @@ -18,12 +18,18 @@ protocol CacheServiceProtocol { class CacheService: CacheServiceProtocol { struct CacheKeys { + static let versionPrefix = "VERSION_\(PlatformDetails().sdkVersion)." + static let anonUserId = "ANONYMOUS_USER_ID" - static let identifiedConfigKey = "IDENTIFIED_CONFIG" - static let anonymousConfigKey = "ANONYMOUS_CONFIG" + static let identifiedConfig = "IDENTIFIED_CONFIG" + static let anonymousConfig = "ANONYMOUS_CONFIG" + static let userIdSuffix = ".USER_ID" static let expiryDateSuffix = ".EXPIRY_DATE" + static let identifiedConfigKey = "\(versionPrefix)\(identifiedConfig)" + static let anonymousConfigKey = "\(versionPrefix)\(anonymousConfig)" + // Legacy keys for cleanup static let legacyUser = "user" static let legacyConfig = "config" @@ -126,6 +132,24 @@ class CacheService: CacheServiceProtocol { return baseKey } + private func cleanupDeprecatedCachedConfigs() { + let deprecatedKeys: [String] = defaults.dictionaryRepresentation().keys.compactMap { key in + // Only include keys that contain one of these patterns + guard key.contains(CacheKeys.identifiedConfig) || key.contains(CacheKeys.anonymousConfig) else { + return nil + } + + return key.starts(with: CacheKeys.versionPrefix) ? nil : key + } + + for key in deprecatedKeys { + if defaults.object(forKey: key) != nil { + defaults.removeObject(forKey: key) + Log.debug("Cleaned up cached config: \(key)") + } + } + } + // MARK: - Legacy Cache Migration func migrateLegacyCache() { @@ -138,6 +162,9 @@ class CacheService: CacheServiceProtocol { // Clean up legacy config cache cleanupLegacyConfigCache() + + // Clean up config cache from other SDK versions + cleanupDeprecatedCachedConfigs() } private func cleanupLegacyUserCache() { diff --git a/DevCycleTests/Models/DevCycleUserTest.swift b/DevCycleTests/Models/DevCycleUserTest.swift index 468e854..229afe2 100644 --- a/DevCycleTests/Models/DevCycleUserTest.swift +++ b/DevCycleTests/Models/DevCycleUserTest.swift @@ -240,16 +240,17 @@ class DevCycleUserTest: XCTestCase { let identifiedUserId = "identified_user_123" let anonymousUserId = "anon_user_456" + let versionPrefix = "VERSION_\(PlatformDetails().sdkVersion)" let configData = "{\"variables\": {\"test\": \"value\"}}".data(using: .utf8) let fetchDate = Int(Date().timeIntervalSince1970) - defaults.set(configData, forKey: "IDENTIFIED_CONFIG") - defaults.set(identifiedUserId, forKey: "IDENTIFIED_CONFIG.USER_ID") - defaults.set(fetchDate, forKey: "IDENTIFIED_CONFIG.FETCH_DATE") + defaults.set(configData, forKey: "\(versionPrefix).IDENTIFIED_CONFIG") + defaults.set(identifiedUserId, forKey: "\(versionPrefix).IDENTIFIED_CONFIG.USER_ID") + defaults.set(fetchDate, forKey: "\(versionPrefix).IDENTIFIED_CONFIG.FETCH_DATE") - defaults.set(configData, forKey: "ANONYMOUS_CONFIG") - defaults.set(anonymousUserId, forKey: "ANONYMOUS_CONFIG.USER_ID") - defaults.set(fetchDate, forKey: "ANONYMOUS_CONFIG.FETCH_DATE") + defaults.set(configData, forKey: "\(versionPrefix).ANONYMOUS_CONFIG") + defaults.set(anonymousUserId, forKey: "\(versionPrefix).ANONYMOUS_CONFIG.USER_ID") + defaults.set(fetchDate, forKey: "\(versionPrefix).ANONYMOUS_CONFIG.FETCH_DATE") cacheService.migrateLegacyCache() @@ -274,25 +275,25 @@ class DevCycleUserTest: XCTestCase { "Legacy anonymous fetch date should be removed") XCTAssertEqual( - defaults.object(forKey: "IDENTIFIED_CONFIG_\(identifiedUserId)") as? Data, + defaults.object(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(identifiedUserId)") as? Data, configData, "New identified config data should match original") XCTAssertNotNil( - defaults.object(forKey: "IDENTIFIED_CONFIG_\(identifiedUserId).EXPIRY_DATE"), + defaults.object(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(identifiedUserId).EXPIRY_DATE"), "New identified expiry date should be set") XCTAssertEqual( - defaults.object(forKey: "ANONYMOUS_CONFIG_\(anonymousUserId)") as? Data, + defaults.object(forKey: "\(versionPrefix).ANONYMOUS_CONFIG_\(anonymousUserId)") as? Data, configData, "New anonymous config data should match original") XCTAssertNotNil( - defaults.object(forKey: "ANONYMOUS_CONFIG_\(anonymousUserId).EXPIRY_DATE"), + defaults.object(forKey: "\(versionPrefix).ANONYMOUS_CONFIG_\(anonymousUserId).EXPIRY_DATE"), "New anonymous expiry date should be set") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(identifiedUserId)") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(identifiedUserId).EXPIRY_DATE") - defaults.removeObject(forKey: "ANONYMOUS_CONFIG_\(anonymousUserId)") - defaults.removeObject(forKey: "ANONYMOUS_CONFIG_\(anonymousUserId).EXPIRY_DATE") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(identifiedUserId)") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(identifiedUserId).EXPIRY_DATE") + defaults.removeObject(forKey: "\(versionPrefix).ANONYMOUS_CONFIG_\(anonymousUserId)") + defaults.removeObject(forKey: "\(versionPrefix).ANONYMOUS_CONFIG_\(anonymousUserId).EXPIRY_DATE") } func testLegacyCacheMigrationSkipsWhenNoData() { @@ -310,6 +311,7 @@ class DevCycleUserTest: XCTestCase { let defaults = UserDefaults.standard let userId = "test_user_123" + let versionPrefix = "VERSION_\(PlatformDetails().sdkVersion)" let legacyConfigData = "{\"variables\": {\"legacy\": \"oldValue\"}}".data(using: .utf8) let newConfigData = "{\"variables\": {\"new\": \"newValue\"}}".data(using: .utf8) let legacyFetchDate = Int(Date().timeIntervalSince1970) - 3600 // 1 hour ago @@ -319,9 +321,9 @@ class DevCycleUserTest: XCTestCase { defaults.set(userId, forKey: "IDENTIFIED_CONFIG.USER_ID") defaults.set(legacyFetchDate, forKey: "IDENTIFIED_CONFIG.FETCH_DATE") - defaults.set(newConfigData, forKey: "IDENTIFIED_CONFIG_\(userId)") - defaults.set(userId, forKey: "IDENTIFIED_CONFIG_\(userId).USER_ID") - defaults.set(newExpiryDate, forKey: "IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE") + defaults.set(newConfigData, forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId)") + defaults.set(userId, forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).USER_ID") + defaults.set(newExpiryDate, forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE") cacheService.migrateLegacyCache() @@ -336,17 +338,17 @@ class DevCycleUserTest: XCTestCase { "Legacy fetch date should be removed when new cache exists") XCTAssertEqual( - defaults.object(forKey: "IDENTIFIED_CONFIG_\(userId)") as? Data, + defaults.object(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId)") as? Data, newConfigData, "New config data should remain unchanged") XCTAssertEqual( - defaults.integer(forKey: "IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE"), + defaults.integer(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE"), newExpiryDate, "New expiry date should remain unchanged") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(userId)") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(userId).USER_ID") - defaults.removeObject(forKey: "IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId)") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).USER_ID") + defaults.removeObject(forKey: "\(versionPrefix).IDENTIFIED_CONFIG_\(userId).EXPIRY_DATE") } func testLegacyUserCacheCleanup() { @@ -390,6 +392,40 @@ class DevCycleUserTest: XCTestCase { defaults.object(forKey: "config"), "Legacy config cache should be removed after migration") } + + func testOtherSDKVersionConfigCacheCleanup() { + let cacheService = CacheService() + let defaults = UserDefaults.standard + + let userId = "test_user_123" + let versionPrefix = "VERSION_\(PlatformDetails().sdkVersion)" + let oldSdkCacheKey = "VERSION_1.23.0.IDENTIFIED_CONFIG_\(userId)" + let currentSdkCacheKey = "\(versionPrefix).IDENTIFIED_CONFIG_\(userId)" + + // Set up legacy config cache data + let legacyConfigData = "{\"variables\": {\"legacy\": \"oldValue\"}}".data(using: .utf8) + let newConfigData = "{\"variables\": {\"new\": \"newValue\"}}".data(using: .utf8) + + defaults.set(legacyConfigData, forKey: oldSdkCacheKey) + defaults.set(newConfigData, forKey: currentSdkCacheKey) + + // Verify legacy config cache exists + XCTAssertNotNil( + defaults.object(forKey: oldSdkCacheKey), "Legacy config cache should exist before migration") + + // Run migration + cacheService.migrateLegacyCache() + + // Verify legacy config cache is cleaned up + XCTAssertNil( + defaults.object(forKey: oldSdkCacheKey), + "Legacy config cache should be removed after migration") + + XCTAssertEqual( + defaults.object(forKey: currentSdkCacheKey) as? Data, + newConfigData, + "New config data should remain unchanged") + } } extension DevCycleUserTest {