Summary: - Sources: Migrations, Models, Protocols, Services, Utilities - Tests: AnyStorageKeyTests.swift, MigrationAdditionalTests.swift, MigrationIntegrationTests.swift, MigrationProtocolTests.swift, MigrationTests.swift (+1 more) - Docs: Migration, Migration_Refactor_Plan_Clean, Proposal, README - Added symbols: struct MyMigration, typealias DestinationKey, func shouldMigrate, func migrate, extension MyNewKey, protocol StorageMigration (+74 more) - Removed symbols: enum StorageDomain, func migrate, func validatePlatformAvailability, func deserialize, func applySecurity, func retrieve (+1 more) Stats: - 31 files changed, 2820 insertions(+), 80 deletions(-)
177 lines
6.5 KiB
Swift
177 lines
6.5 KiB
Swift
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<String> = .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<String> = .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<String> = .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<String> = .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<Int> = .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)
|
|
}
|
|
}
|