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 key" let availability: PlatformAvailability = .all let syncPolicy: SyncPolicy = .never } private struct DestinationStringKey: StorageKey { typealias Value = String let name: String let domain: StorageDomain let security: SecurityPolicy = .keychain(accessibility: .afterFirstUnlock, accessControl: .none) let serializer: Serializer = .json let owner: String = "Destination" let description: String = "Destination key" let availability: PlatformAvailability = .all 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 MigrationProtocolTests { private let router: StorageRouter init() { let testBaseURL = FileManager.default.temporaryDirectory.appending(path: "MigrationProtocolTests-\(UUID().uuidString)") router = StorageRouter( keychain: MockKeychainHelper(), encryption: EncryptionHelper(keychain: MockKeychainHelper()), file: FileStorageHelper(configuration: FileStorageConfiguration(baseURL: testBaseURL)), defaults: UserDefaultsHelper(defaults: UserDefaults(suiteName: "MigrationProtocolTests.\(UUID().uuidString)")!) ) } @Test func simpleLegacyMigrationTest() async throws { let legacyKey = LegacyStringKey(name: "legacy.simple", domain: .userDefaults(suite: nil)) let destinationKey = DestinationStringKey(name: "modern.simple", domain: .keychain(service: "test.migration")) try await router.set("value", for: legacyKey) let migration = SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: .key(legacyKey)) let context = MigrationContext(appVersion: "1.0") let shouldMigrate = try await migration.shouldMigrate(using: router, context: context) #expect(shouldMigrate == true) let result = try await migration.migrate(using: router, context: context) #expect(result.success == true) let migratedValue = try await router.get(destinationKey) #expect(migratedValue == "value") } @Test func conditionalMigrationTest() async throws { let legacyKey = LegacyStringKey(name: "legacy.conditional", domain: .userDefaults(suite: nil)) let destinationKey = DestinationStringKey(name: "modern.conditional", domain: .keychain(service: "test.migration")) try await router.set("value", for: legacyKey) let fallback = AnyStorageMigration( SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: .key(legacyKey)) ) let migration = AppVersionConditionalMigration( destinationKey: destinationKey, minAppVersion: "2.0", fallbackMigration: fallback ) let context = MigrationContext(appVersion: "1.0") let shouldMigrate = try await migration.shouldMigrate(using: router, context: context) #expect(shouldMigrate == true) let result = try await migration.migrate(using: router, context: context) #expect(result.success == true) } @Test func transformingMigrationTest() async throws { let sourceKey = SourceStringKey(name: "legacy.transform", domain: .userDefaults(suite: nil)) let destinationKey = DestinationIntKey(name: "modern.transform", domain: .userDefaults(suite: nil)) try await router.set("42", for: sourceKey) let migration = DefaultTransformingMigration( destinationKey: destinationKey, sourceKey: sourceKey ) { value in guard let intValue = Int(value) else { throw MigrationError.transformationFailed("Invalid integer") } return intValue } let result = try await migration.migrate(using: router, context: MigrationContext()) #expect(result.success == true) let migratedValue = try await router.get(destinationKey) #expect(migratedValue == 42) } @Test func aggregatingMigrationTest() async throws { let sourceKeyA = SourceStringKey(name: "legacy.aggregate.a", domain: .userDefaults(suite: nil)) let sourceKeyB = SourceStringKey(name: "legacy.aggregate.b", domain: .userDefaults(suite: nil)) let destinationKey = DestinationStringKey(name: "modern.aggregate", domain: .userDefaults(suite: nil)) try await router.set("alpha", for: sourceKeyA) try await router.set("beta", for: sourceKeyB) let migration = DefaultAggregatingMigration( destinationKey: destinationKey, sourceKeys: [.key(sourceKeyA), .key(sourceKeyB)] ) { sources in let values = sources.compactMap { $0.value as? String } return values.joined(separator: ",") } let result = try await migration.migrate(using: router, context: MigrationContext()) #expect(result.success == true) let migratedValue = try await router.get(destinationKey) #expect(migratedValue.contains("alpha")) #expect(migratedValue.contains("beta")) } @Test func migrationErrorHandlingTest() async throws { let destinationKey = DestinationStringKey(name: "modern.error", domain: .userDefaults(suite: nil)) let migration = FailingMigration(destinationKey: destinationKey, error: .transformationFailed("Failed")) let result = try await migration.migrate(using: router, context: MigrationContext()) #expect(result.success == false) #expect(result.errors.first == .transformationFailed("Failed")) } }