import Foundation import Testing @testable import LocalData private func makeLegacyStringKey(name: String, domain: StorageDomain) -> StorageKey { StorageKey( name: name, domain: domain, security: .none, owner: "Legacy", description: "Legacy key" ) } private func makeDestinationStringKey(name: String, domain: StorageDomain) -> StorageKey { StorageKey( name: name, domain: domain, security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none), owner: "Destination", description: "Destination key" ) } private func makeSourceStringKey(name: String, domain: StorageDomain) -> StorageKey { StorageKey( name: name, domain: domain, security: .none, owner: "Source", description: "Source key" ) } private func makeDestinationIntKey(name: String, domain: StorageDomain) -> StorageKey { StorageKey( name: name, domain: domain, security: .none, owner: "Destination", description: "Destination int key" ) } @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 = makeLegacyStringKey(name: "legacy.simple", domain: .userDefaults(suite: nil)) let destinationKey = makeDestinationStringKey( 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 = makeLegacyStringKey(name: "legacy.conditional", domain: .userDefaults(suite: nil)) let destinationKey = makeDestinationStringKey( 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 = makeSourceStringKey(name: "legacy.transform", domain: .userDefaults(suite: nil)) let destinationKey = makeDestinationIntKey(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 = makeSourceStringKey(name: "legacy.aggregate.a", domain: .userDefaults(suite: nil)) let sourceKeyB = makeSourceStringKey(name: "legacy.aggregate.b", domain: .userDefaults(suite: nil)) let destinationKey = makeDestinationStringKey(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 = makeDestinationStringKey(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")) } }