import Foundation import Testing @testable import LocalData private struct LegacyStringKey: StorageKey { typealias Value = String let name: String let domain: StorageDomain let security: SecurityPolicy = .none let serializer: Serializer = .json let owner: String = "Legacy" let description: String = "Legacy string key" let availability: PlatformAvailability = .all let syncPolicy: SyncPolicy = .never } private struct ModernStringKey: StorageKey { typealias Value = String let name: String let domain: StorageDomain let security: SecurityPolicy = .none let serializer: Serializer = .json let owner: String = "Modern" let description: String = "Modern string key" let availability: PlatformAvailability = .all let syncPolicy: SyncPolicy = .never let legacyKey: AnyStorageKey? var migration: AnyStorageMigration? { guard let legacyKey else { return nil } return AnyStorageMigration( SimpleLegacyMigration(destinationKey: self, sourceKey: legacyKey) ) } } private struct PhoneOnlyKey: StorageKey { typealias Value = String let name: String let domain: StorageDomain let security: SecurityPolicy = .none let serializer: Serializer = .json let owner: String = "PhoneOnly" let description: String = "Phone-only key" let availability: PlatformAvailability = .phoneOnly let syncPolicy: SyncPolicy = .never } private struct SourceStringKey: StorageKey { typealias Value = String let name: String let domain: StorageDomain let security: SecurityPolicy = .none let serializer: Serializer = .json let owner: String = "Source" let description: String = "Source key" let availability: PlatformAvailability = .all let syncPolicy: SyncPolicy = .never } private struct DestinationIntKey: StorageKey { typealias Value = Int let name: String let domain: StorageDomain let security: SecurityPolicy = .none let serializer: Serializer = .json let owner: String = "Destination" let description: String = "Destination int key" let availability: PlatformAvailability = .all let syncPolicy: SyncPolicy = .never } @Suite(.serialized) struct MigrationAdditionalTests { private let router: StorageRouter init() { let testBaseURL = FileManager.default.temporaryDirectory.appending(path: "MigrationAdditionalTests-\(UUID().uuidString)") router = StorageRouter( keychain: MockKeychainHelper(), encryption: EncryptionHelper(keychain: MockKeychainHelper()), file: FileStorageHelper(configuration: FileStorageConfiguration(baseURL: testBaseURL)), defaults: UserDefaultsHelper(defaults: UserDefaults(suiteName: "MigrationAdditionalTests.\(UUID().uuidString)")!) ) } @Test func migrationHistoryTrackingTest() async throws { let legacyKey = LegacyStringKey(name: "legacy.history", domain: .userDefaults(suite: nil)) let modernKey = ModernStringKey( name: "modern.history", domain: .userDefaults(suite: nil), legacyKey: .key(legacyKey) ) try await router.set("history", for: legacyKey) _ = try await router.forceMigration(for: modernKey) let history = router.migrationHistory(for: modernKey) #expect(history != nil) } @Test func migrationFailureKeepsSourceTest() async throws { let sourceKey = SourceStringKey(name: "legacy.rollback", domain: .userDefaults(suite: nil)) let destinationKey = DestinationIntKey(name: "modern.rollback", domain: .userDefaults(suite: nil)) try await router.set("not-a-number", for: sourceKey) let migration = DefaultTransformingMigration( destinationKey: destinationKey, sourceKey: sourceKey ) { _ in throw MigrationError.transformationFailed("Invalid integer") } let result = try await migration.migrate(using: router, context: MigrationContext()) #expect(result.success == false) #expect(try await router.exists(sourceKey) == true) #expect(try await router.exists(destinationKey) == false) } @Test func watchAvailabilityBlocksMigrationTest() async throws { let legacyKey = LegacyStringKey(name: "legacy.watch", domain: .userDefaults(suite: nil)) let destinationKey = PhoneOnlyKey(name: "modern.watch", domain: .userDefaults(suite: nil)) let migration = SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: .key(legacyKey)) let deviceInfo = DeviceInfo( platform: .watchOS, systemVersion: "10.0", model: "Watch", isSimulator: true ) let context = MigrationContext(deviceInfo: deviceInfo) let shouldMigrate = try await migration.shouldMigrate(using: router, context: context) #expect(shouldMigrate == false) } @Test func largeDataMigrationTest() async throws { let legacyKey = LegacyStringKey(name: "legacy.large", domain: .userDefaults(suite: nil)) let modernKey = ModernStringKey( name: "modern.large", domain: .userDefaults(suite: nil), legacyKey: .key(legacyKey) ) let largeValue = String(repeating: "a", count: 1_200_000) try await router.set(largeValue, for: legacyKey) let migration = SimpleLegacyMigration(destinationKey: modernKey, sourceKey: .key(legacyKey)) let result = try await migration.migrate(using: router, context: MigrationContext()) #expect(result.success == true) let migratedValue = try await router.get(modernKey) #expect(migratedValue == largeValue) } @Test func typeErasureMigrationTest() async throws { let legacyKey = LegacyStringKey(name: "legacy.erased", domain: .userDefaults(suite: nil)) let modernKey = ModernStringKey( name: "modern.erased", domain: .userDefaults(suite: nil), legacyKey: .key(legacyKey) ) try await router.set("erased", for: legacyKey) let migration = AnyStorageMigration( SimpleLegacyMigration(destinationKey: modernKey, sourceKey: .key(legacyKey)) ) let shouldMigrate = try await migration.shouldMigrate(using: router, context: MigrationContext()) #expect(shouldMigrate == true) let result = try await migration.migrate(using: router, context: MigrationContext()) #expect(result.success == true) } }