Tests AnyStorageKeyTests.swift, AuditTests.swift, LocalDataTests.swift (+13 more)

Summary:
- Tests: AnyStorageKeyTests.swift, AuditTests.swift, LocalDataTests.swift, MigrationAdditionalTests.swift, MigrationIntegrationTests.swift (+11 more)
- Added symbols: func makeStringKey, func makeUserDefaultsKey, func makeFileKey, func makeLegacyStringKey, func makeModernStringKey, func makePhoneOnlyKey (+14 more)
- Removed symbols: struct StringKey, typealias Value, struct TestKey, struct TestUserDefaultsKey, struct TestFileKey, struct LegacyStringKey (+19 more)

Stats:
- 16 files changed, 329 insertions(+), 386 deletions(-)
This commit is contained in:
Matt Bruce 2026-01-17 09:18:04 -06:00
parent 7b4a0b46f8
commit 0afaf34c78
16 changed files with 329 additions and 386 deletions

View File

@ -4,20 +4,18 @@ import Testing
@Suite struct AnyStorageKeyTests { @Suite struct AnyStorageKeyTests {
private struct StringKey: StorageKey { private func makeStringKey(name: String) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain = .userDefaults(suite: nil) domain: .userDefaults(suite: nil),
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "Test",
let owner: String = "Test" description: "Test"
let description: String = "Test" )
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
} }
@Test func anyStorageKeyCapturesDescriptor() { @Test func anyStorageKeyCapturesDescriptor() {
let key = StringKey(name: "test.key") let key = makeStringKey(name: "test.key")
let anyKey = AnyStorageKey.key(key) let anyKey = AnyStorageKey.key(key)
#expect(anyKey.descriptor.name == "test.key") #expect(anyKey.descriptor.name == "test.key")
@ -27,7 +25,7 @@ import Testing
@Test func anyStorageKeyTriggersMigration() async throws { @Test func anyStorageKeyTriggersMigration() async throws {
let router = StorageRouter(keychain: MockKeychainHelper()) let router = StorageRouter(keychain: MockKeychainHelper())
let key = StringKey(name: "test.key") let key = makeStringKey(name: "test.key")
let anyKey = AnyStorageKey.key(key) let anyKey = AnyStorageKey.key(key)
// This will call router.forceMigration(for: key) // This will call router.forceMigration(for: key)

View File

@ -7,31 +7,35 @@ import Testing
private struct AuditCatalog: StorageKeyCatalog { private struct AuditCatalog: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[ [
.key(TestKey(name: "k1", domain: .userDefaults(suite: nil))), .key(AuditTests.makeTestKey(name: "k1", domain: .userDefaults(suite: nil))),
.key(TestKey(name: "k2", domain: .keychain(service: "s"), security: .keychain(accessibility: .afterFirstUnlock, accessControl: .userPresence))), .key(AuditTests.makeTestKey(
.key(TestKey(name: "k3", domain: .fileSystem(directory: .documents))), name: "k2",
.key(TestKey(name: "k4", domain: .encryptedFileSystem(directory: .caches))), domain: .keychain(service: "s"),
.key(TestKey(name: "k5", domain: .appGroupUserDefaults(identifier: "ig"), security: .encrypted(.recommended))) security: .keychain(accessibility: .afterFirstUnlock, accessControl: .userPresence)
)),
.key(AuditTests.makeTestKey(name: "k3", domain: .fileSystem(directory: .documents))),
.key(AuditTests.makeTestKey(name: "k4", domain: .encryptedFileSystem(directory: .caches))),
.key(AuditTests.makeTestKey(
name: "k5",
domain: .appGroupUserDefaults(identifier: "ig"),
security: .encrypted(.recommended)
))
] ]
} }
} }
private struct TestKey: StorageKey { private static func makeTestKey(
typealias Value = String name: String,
let name: String domain: StorageDomain,
let domain: StorageDomain security: SecurityPolicy = .none
let security: SecurityPolicy ) -> StorageKey<String> {
let serializer: Serializer<String> = .json StorageKey(
let owner: String = "Audit" name: name,
let description: String = "Desc" domain: domain,
let availability: PlatformAvailability = .all security: security,
let syncPolicy: SyncPolicy = .never owner: "Audit",
description: "Desc"
init(name: String, domain: StorageDomain, security: SecurityPolicy = .none) { )
self.name = name
self.domain = domain
self.security = security
}
} }
@Test func renderCatalogText() { @Test func renderCatalogText() {

View File

@ -2,40 +2,24 @@ import Foundation
import Testing import Testing
@testable import LocalData @testable import LocalData
private struct TestUserDefaultsKey: StorageKey { private func makeUserDefaultsKey(name: String, suiteName: String) -> StorageKey<String> {
typealias Value = String StorageKey(
name: name,
let name: String domain: .userDefaults(suite: suiteName),
let domain: StorageDomain security: .none,
let security: SecurityPolicy = .none owner: "LocalDataTests",
let serializer: Serializer<String> = .json description: "Test-only key for user defaults round-trip."
let owner: String = "LocalDataTests" )
let description: String = "Test-only key for user defaults round-trip."
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
init(name: String, suiteName: String) {
self.name = name
self.domain = .userDefaults(suite: suiteName)
}
} }
private struct TestFileKey: StorageKey { private func makeFileKey(name: String, directory: URL) -> StorageKey<String> {
typealias Value = String StorageKey(
name: name,
let name: String domain: .fileSystem(directory: .custom(directory)),
let domain: StorageDomain security: .none,
let security: SecurityPolicy = .none owner: "LocalDataTests",
let serializer: Serializer<String> = .json description: "Test-only key for file system round-trip."
let owner: String = "LocalDataTests" )
let description: String = "Test-only key for file system round-trip."
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
init(name: String, directory: URL) {
self.name = name
self.domain = .fileSystem(directory: .custom(directory))
}
} }
@Suite(.serialized) @Suite(.serialized)
@ -50,7 +34,7 @@ struct LocalDataTests {
} }
} }
let key = TestUserDefaultsKey(name: "test.string", suiteName: suiteName) let key = makeUserDefaultsKey(name: "test.string", suiteName: suiteName)
let storedValue = "1.0.0" let storedValue = "1.0.0"
try await router.set(storedValue, for: key) try await router.set(storedValue, for: key)
@ -72,7 +56,7 @@ struct LocalDataTests {
try? FileManager.default.removeItem(at: tempDirectory) try? FileManager.default.removeItem(at: tempDirectory)
} }
let key = TestFileKey(name: "test.json", directory: tempDirectory) let key = makeFileKey(name: "test.json", directory: tempDirectory)
let storedValue = "payload" let storedValue = "payload"
try await router.set(storedValue, for: key) try await router.set(storedValue, for: key)

View File

@ -2,72 +2,65 @@ import Foundation
import Testing import Testing
@testable import LocalData @testable import LocalData
private struct LegacyStringKey: StorageKey { private func makeLegacyStringKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "Legacy",
let owner: String = "Legacy" description: "Legacy string key"
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 func makeModernStringKey(
name: String,
domain: StorageDomain,
legacyKey: AnyStorageKey?
) -> StorageKey<String> {
StorageKey(
name: name,
domain: domain,
security: .none,
owner: "Modern",
description: "Modern string key",
migration: { destinationKey in
guard let legacyKey else { return nil }
return AnyStorageMigration(
SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: legacyKey)
)
}
)
} }
private struct PhoneOnlyKey: StorageKey { private func makePhoneOnlyKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "PhoneOnly",
let owner: String = "PhoneOnly" description: "Phone-only key",
let description: String = "Phone-only key" availability: .phoneOnly
let availability: PlatformAvailability = .phoneOnly )
let syncPolicy: SyncPolicy = .never
} }
private struct SourceStringKey: StorageKey { private func makeSourceStringKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "Source",
let owner: String = "Source" description: "Source key"
let description: String = "Source key" )
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
} }
private struct DestinationIntKey: StorageKey { private func makeDestinationIntKey(name: String, domain: StorageDomain) -> StorageKey<Int> {
typealias Value = Int StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<Int> = .json owner: "Destination",
let owner: String = "Destination" description: "Destination int key"
let description: String = "Destination int key" )
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
} }
@Suite(.serialized) @Suite(.serialized)
@ -85,8 +78,8 @@ struct MigrationAdditionalTests {
} }
@Test func migrationHistoryTrackingTest() async throws { @Test func migrationHistoryTrackingTest() async throws {
let legacyKey = LegacyStringKey(name: "legacy.history", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyStringKey(name: "legacy.history", domain: .userDefaults(suite: nil))
let modernKey = ModernStringKey( let modernKey = makeModernStringKey(
name: "modern.history", name: "modern.history",
domain: .userDefaults(suite: nil), domain: .userDefaults(suite: nil),
legacyKey: .key(legacyKey) legacyKey: .key(legacyKey)
@ -95,13 +88,13 @@ struct MigrationAdditionalTests {
try await router.set("history", for: legacyKey) try await router.set("history", for: legacyKey)
_ = try await router.forceMigration(for: modernKey) _ = try await router.forceMigration(for: modernKey)
let history = router.migrationHistory(for: modernKey) let history = await router.migrationHistory(for: modernKey)
#expect(history != nil) #expect(history != nil)
} }
@Test func migrationFailureKeepsSourceTest() async throws { @Test func migrationFailureKeepsSourceTest() async throws {
let sourceKey = SourceStringKey(name: "legacy.rollback", domain: .userDefaults(suite: nil)) let sourceKey = makeSourceStringKey(name: "legacy.rollback", domain: .userDefaults(suite: nil))
let destinationKey = DestinationIntKey(name: "modern.rollback", domain: .userDefaults(suite: nil)) let destinationKey = makeDestinationIntKey(name: "modern.rollback", domain: .userDefaults(suite: nil))
try await router.set("not-a-number", for: sourceKey) try await router.set("not-a-number", for: sourceKey)
@ -119,8 +112,8 @@ struct MigrationAdditionalTests {
} }
@Test func watchAvailabilityBlocksMigrationTest() async throws { @Test func watchAvailabilityBlocksMigrationTest() async throws {
let legacyKey = LegacyStringKey(name: "legacy.watch", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyStringKey(name: "legacy.watch", domain: .userDefaults(suite: nil))
let destinationKey = PhoneOnlyKey(name: "modern.watch", domain: .userDefaults(suite: nil)) let destinationKey = makePhoneOnlyKey(name: "modern.watch", domain: .userDefaults(suite: nil))
let migration = SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: .key(legacyKey)) let migration = SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: .key(legacyKey))
let deviceInfo = DeviceInfo( let deviceInfo = DeviceInfo(
@ -136,8 +129,8 @@ struct MigrationAdditionalTests {
} }
@Test func largeDataMigrationTest() async throws { @Test func largeDataMigrationTest() async throws {
let legacyKey = LegacyStringKey(name: "legacy.large", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyStringKey(name: "legacy.large", domain: .userDefaults(suite: nil))
let modernKey = ModernStringKey( let modernKey = makeModernStringKey(
name: "modern.large", name: "modern.large",
domain: .userDefaults(suite: nil), domain: .userDefaults(suite: nil),
legacyKey: .key(legacyKey) legacyKey: .key(legacyKey)
@ -155,8 +148,8 @@ struct MigrationAdditionalTests {
} }
@Test func typeErasureMigrationTest() async throws { @Test func typeErasureMigrationTest() async throws {
let legacyKey = LegacyStringKey(name: "legacy.erased", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyStringKey(name: "legacy.erased", domain: .userDefaults(suite: nil))
let modernKey = ModernStringKey( let modernKey = makeModernStringKey(
name: "modern.erased", name: "modern.erased",
domain: .userDefaults(suite: nil), domain: .userDefaults(suite: nil),
legacyKey: .key(legacyKey) legacyKey: .key(legacyKey)

View File

@ -2,36 +2,34 @@ import Foundation
import Testing import Testing
@testable import LocalData @testable import LocalData
private struct LegacyIntegrationKey: StorageKey { private func makeLegacyIntegrationKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "Legacy",
let owner: String = "Legacy" description: "Legacy integration key"
let description: String = "Legacy integration key"
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
private struct ModernIntegrationKey: StorageKey {
typealias Value = String
let name: String
let domain: StorageDomain
let security: SecurityPolicy = .keychain(accessibility: .afterFirstUnlock, accessControl: .none)
let serializer: Serializer<String> = .json
let owner: String = "Modern"
let description: String = "Modern integration 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 func makeModernIntegrationKey(
name: String,
domain: StorageDomain,
legacyKey: AnyStorageKey?
) -> StorageKey<String> {
StorageKey(
name: name,
domain: domain,
security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none),
owner: "Modern",
description: "Modern integration key",
migration: { destinationKey in
guard let legacyKey else { return nil }
return AnyStorageMigration(
SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: legacyKey)
)
}
)
} }
private struct IntegrationCatalog: StorageKeyCatalog { private struct IntegrationCatalog: StorageKeyCatalog {
@ -53,8 +51,8 @@ struct MigrationIntegrationTests {
} }
@Test func endToEndMigrationTest() async throws { @Test func endToEndMigrationTest() async throws {
let legacyKey = LegacyIntegrationKey(name: "legacy.integration", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyIntegrationKey(name: "legacy.integration", domain: .userDefaults(suite: nil))
let modernKey = ModernIntegrationKey( let modernKey = makeModernIntegrationKey(
name: "modern.integration", name: "modern.integration",
domain: .keychain(service: "test.migration"), domain: .keychain(service: "test.migration"),
legacyKey: .key(legacyKey) legacyKey: .key(legacyKey)
@ -69,8 +67,8 @@ struct MigrationIntegrationTests {
} }
@Test func migrationRegistrationTest() async throws { @Test func migrationRegistrationTest() async throws {
let legacyKey = LegacyIntegrationKey(name: "legacy.catalog", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyIntegrationKey(name: "legacy.catalog", domain: .userDefaults(suite: nil))
let modernKey = ModernIntegrationKey( let modernKey = makeModernIntegrationKey(
name: "modern.catalog", name: "modern.catalog",
domain: .keychain(service: "test.migration"), domain: .keychain(service: "test.migration"),
legacyKey: .key(legacyKey) legacyKey: .key(legacyKey)
@ -86,8 +84,8 @@ struct MigrationIntegrationTests {
} }
@Test func concurrentMigrationTest() async throws { @Test func concurrentMigrationTest() async throws {
let legacyKey = LegacyIntegrationKey(name: "legacy.concurrent", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyIntegrationKey(name: "legacy.concurrent", domain: .userDefaults(suite: nil))
let modernKey = ModernIntegrationKey( let modernKey = makeModernIntegrationKey(
name: "modern.concurrent", name: "modern.concurrent",
domain: .keychain(service: "test.migration"), domain: .keychain(service: "test.migration"),
legacyKey: .key(legacyKey) legacyKey: .key(legacyKey)
@ -105,7 +103,7 @@ struct MigrationIntegrationTests {
} }
@Test func migrationFailureResultTest() async throws { @Test func migrationFailureResultTest() async throws {
let destinationKey = ModernIntegrationKey( let destinationKey = makeModernIntegrationKey(
name: "modern.failure", name: "modern.failure",
domain: .userDefaults(suite: nil), domain: .userDefaults(suite: nil),
legacyKey: nil legacyKey: nil

View File

@ -2,52 +2,44 @@ import Foundation
import Testing import Testing
@testable import LocalData @testable import LocalData
private struct LegacyStringKey: StorageKey { private func makeLegacyStringKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "Legacy",
let owner: String = "Legacy" description: "Legacy key"
let description: String = "Legacy key" )
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
} }
private struct DestinationStringKey: StorageKey { private func makeDestinationStringKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .keychain(accessibility: .afterFirstUnlock, accessControl: .none) security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none),
let serializer: Serializer<String> = .json owner: "Destination",
let owner: String = "Destination" description: "Destination key"
let description: String = "Destination key" )
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
} }
private struct SourceStringKey: StorageKey { private func makeSourceStringKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "Source",
let owner: String = "Source" description: "Source key"
let description: String = "Source key" )
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
} }
private struct DestinationIntKey: StorageKey { private func makeDestinationIntKey(name: String, domain: StorageDomain) -> StorageKey<Int> {
typealias Value = Int StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<Int> = .json owner: "Destination",
let owner: String = "Destination" description: "Destination int key"
let description: String = "Destination int key" )
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
} }
@Suite(.serialized) @Suite(.serialized)
@ -65,8 +57,11 @@ struct MigrationProtocolTests {
} }
@Test func simpleLegacyMigrationTest() async throws { @Test func simpleLegacyMigrationTest() async throws {
let legacyKey = LegacyStringKey(name: "legacy.simple", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyStringKey(name: "legacy.simple", domain: .userDefaults(suite: nil))
let destinationKey = DestinationStringKey(name: "modern.simple", domain: .keychain(service: "test.migration")) let destinationKey = makeDestinationStringKey(
name: "modern.simple",
domain: .keychain(service: "test.migration")
)
try await router.set("value", for: legacyKey) try await router.set("value", for: legacyKey)
let migration = SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: .key(legacyKey)) let migration = SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: .key(legacyKey))
@ -83,8 +78,11 @@ struct MigrationProtocolTests {
} }
@Test func conditionalMigrationTest() async throws { @Test func conditionalMigrationTest() async throws {
let legacyKey = LegacyStringKey(name: "legacy.conditional", domain: .userDefaults(suite: nil)) let legacyKey = makeLegacyStringKey(name: "legacy.conditional", domain: .userDefaults(suite: nil))
let destinationKey = DestinationStringKey(name: "modern.conditional", domain: .keychain(service: "test.migration")) let destinationKey = makeDestinationStringKey(
name: "modern.conditional",
domain: .keychain(service: "test.migration")
)
try await router.set("value", for: legacyKey) try await router.set("value", for: legacyKey)
let fallback = AnyStorageMigration( let fallback = AnyStorageMigration(
@ -105,8 +103,8 @@ struct MigrationProtocolTests {
} }
@Test func transformingMigrationTest() async throws { @Test func transformingMigrationTest() async throws {
let sourceKey = SourceStringKey(name: "legacy.transform", domain: .userDefaults(suite: nil)) let sourceKey = makeSourceStringKey(name: "legacy.transform", domain: .userDefaults(suite: nil))
let destinationKey = DestinationIntKey(name: "modern.transform", domain: .userDefaults(suite: nil)) let destinationKey = makeDestinationIntKey(name: "modern.transform", domain: .userDefaults(suite: nil))
try await router.set("42", for: sourceKey) try await router.set("42", for: sourceKey)
let migration = DefaultTransformingMigration( let migration = DefaultTransformingMigration(
@ -127,9 +125,9 @@ struct MigrationProtocolTests {
} }
@Test func aggregatingMigrationTest() async throws { @Test func aggregatingMigrationTest() async throws {
let sourceKeyA = SourceStringKey(name: "legacy.aggregate.a", domain: .userDefaults(suite: nil)) let sourceKeyA = makeSourceStringKey(name: "legacy.aggregate.a", domain: .userDefaults(suite: nil))
let sourceKeyB = SourceStringKey(name: "legacy.aggregate.b", domain: .userDefaults(suite: nil)) let sourceKeyB = makeSourceStringKey(name: "legacy.aggregate.b", domain: .userDefaults(suite: nil))
let destinationKey = DestinationStringKey(name: "modern.aggregate", 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("alpha", for: sourceKeyA)
try await router.set("beta", for: sourceKeyB) try await router.set("beta", for: sourceKeyB)
@ -151,7 +149,7 @@ struct MigrationProtocolTests {
} }
@Test func migrationErrorHandlingTest() async throws { @Test func migrationErrorHandlingTest() async throws {
let destinationKey = DestinationStringKey(name: "modern.error", domain: .userDefaults(suite: nil)) let destinationKey = makeDestinationStringKey(name: "modern.error", domain: .userDefaults(suite: nil))
let migration = FailingMigration(destinationKey: destinationKey, error: .transformationFailed("Failed")) let migration = FailingMigration(destinationKey: destinationKey, error: .transformationFailed("Failed"))
let result = try await migration.migrate(using: router, context: MigrationContext()) let result = try await migration.migrate(using: router, context: MigrationContext())

View File

@ -2,42 +2,34 @@ import Foundation
import Testing import Testing
@testable import LocalData @testable import LocalData
private struct LegacyKey: StorageKey { private func makeLegacyKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "Legacy",
let owner: String = "Legacy" description: "Legacy key"
let description: String = "Legacy key"
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
private struct ModernKey: StorageKey {
typealias Value = String
let name: String
let domain: StorageDomain
let security: SecurityPolicy = .keychain(accessibility: .afterFirstUnlock, accessControl: .none)
let serializer: Serializer<String> = .json
let owner: String = "Modern"
let description: String = "Modern key"
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
let legacyKey: AnyStorageKey?
init(name: String, domain: StorageDomain, legacyKey: AnyStorageKey?) {
self.name = name
self.domain = domain
self.legacyKey = legacyKey
}
var migration: AnyStorageMigration? {
guard let legacyKey else { return nil }
return AnyStorageMigration(
SimpleLegacyMigration(destinationKey: self, sourceKey: legacyKey)
) )
} }
private func makeModernKey(
name: String,
domain: StorageDomain,
legacyKey: AnyStorageKey?
) -> StorageKey<String> {
StorageKey(
name: name,
domain: domain,
security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none),
owner: "Modern",
description: "Modern key",
migration: { destinationKey in
guard let legacyKey else { return nil }
return AnyStorageMigration(
SimpleLegacyMigration(destinationKey: destinationKey, sourceKey: legacyKey)
)
}
)
} }
@Suite(.serialized) @Suite(.serialized)
@ -65,7 +57,7 @@ struct MigrationTests {
} }
// 1. Setup legacy data manually in UserDefaults // 1. Setup legacy data manually in UserDefaults
let legacyKey = LegacyKey(name: legacyName, domain: .userDefaults(suite: suiteName)) let legacyKey = makeLegacyKey(name: legacyName, domain: .userDefaults(suite: suiteName))
try await router.set(secretValue, for: legacyKey) try await router.set(secretValue, for: legacyKey)
// Verify it exists in legacy location // Verify it exists in legacy location
@ -73,7 +65,7 @@ struct MigrationTests {
#expect(existsInLegacy == true) #expect(existsInLegacy == true)
// 2. Setup modern key with legacy source // 2. Setup modern key with legacy source
let modernKey = ModernKey( let modernKey = makeModernKey(
name: modernName, name: modernName,
domain: .keychain(service: "test.migration"), domain: .keychain(service: "test.migration"),
legacyKey: .key(legacyKey) legacyKey: .key(legacyKey)
@ -104,11 +96,11 @@ struct MigrationTests {
} }
// 1. Setup legacy data // 1. Setup legacy data
let legacyKey = LegacyKey(name: legacyName, domain: .userDefaults(suite: suiteName)) let legacyKey = makeLegacyKey(name: legacyName, domain: .userDefaults(suite: suiteName))
try await router.set(value, for: legacyKey) try await router.set(value, for: legacyKey)
// 2. Setup modern key // 2. Setup modern key
let modernKey = ModernKey( let modernKey = makeModernKey(
name: modernName, name: modernName,
domain: .userDefaults(suite: suiteName), domain: .userDefaults(suite: suiteName),
legacyKey: .key(legacyKey) legacyKey: .key(legacyKey)

View File

@ -1,10 +1,8 @@
import Foundation import Foundation
@testable import LocalData @testable import LocalData
struct MockMigration<Destination: StorageKey>: StorageMigration { struct MockMigration<Value: Codable & Sendable>: StorageMigration {
typealias DestinationKey = Destination let destinationKey: StorageKey<Value>
let destinationKey: Destination
let shouldSucceed: Bool let shouldSucceed: Bool
let shouldMigrateResult: Bool let shouldMigrateResult: Bool
let migrationDelay: TimeInterval let migrationDelay: TimeInterval
@ -28,10 +26,8 @@ struct MockMigration<Destination: StorageKey>: StorageMigration {
} }
} }
struct FailingMigration<Destination: StorageKey>: StorageMigration { struct FailingMigration<Value: Codable & Sendable>: StorageMigration {
typealias DestinationKey = Destination let destinationKey: StorageKey<Value>
let destinationKey: Destination
let error: MigrationError let error: MigrationError
func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool { true } func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool { true }

View File

@ -2,39 +2,35 @@ import Foundation
import Testing import Testing
@testable import LocalData @testable import LocalData
private struct TestRegistryKey: StorageKey { private func makeRegistryKey(
typealias Value = String name: String,
let name: String owner: String = "Test",
let domain: StorageDomain = .userDefaults(suite: nil) description: String = "Test"
let security: SecurityPolicy = .none ) -> StorageKey<String> {
let serializer: Serializer<String> = .json StorageKey(
let owner: String name: name,
let description: String domain: .userDefaults(suite: nil),
let availability: PlatformAvailability = .all security: .none,
let syncPolicy: SyncPolicy = .never owner: owner,
description: description
init(name: String, owner: String = "Test", description: String = "Test") { )
self.name = name
self.owner = owner
self.description = description
}
} }
private struct CatalogA: StorageKeyCatalog { private struct CatalogA: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[.key(TestRegistryKey(name: "key.a", owner: "ModuleA"))] [.key(makeRegistryKey(name: "key.a", owner: "ModuleA"))]
} }
} }
private struct CatalogB: StorageKeyCatalog { private struct CatalogB: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[.key(TestRegistryKey(name: "key.b", owner: "ModuleB"))] [.key(makeRegistryKey(name: "key.b", owner: "ModuleB"))]
} }
} }
private struct CatalogCollision: StorageKeyCatalog { private struct CatalogCollision: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[.key(TestRegistryKey(name: "key.a", owner: "ModuleCollision"))] [.key(makeRegistryKey(name: "key.a", owner: "ModuleCollision"))]
} }
} }

View File

@ -16,26 +16,22 @@ import Testing
) )
} }
private struct DomainKey: StorageKey { private func makeDomainKey(
typealias Value = String name: String,
let name: String domain: StorageDomain,
let domain: StorageDomain security: SecurityPolicy = .none
let security: SecurityPolicy ) -> StorageKey<String> {
let serializer: Serializer<String> = .json StorageKey(
let owner: String = "DomainTests" name: name,
let description: String = "Domain test key" domain: domain,
let availability: PlatformAvailability = .all security: security,
let syncPolicy: SyncPolicy = .never owner: "DomainTests",
description: "Domain test key"
init(name: String, domain: StorageDomain, security: SecurityPolicy = .none) { )
self.name = name
self.domain = domain
self.security = security
}
} }
@Test func domainUserDefaults() async throws { @Test func domainUserDefaults() async throws {
let key = DomainKey(name: "defaults.key", domain: .userDefaults(suite: nil)) let key = makeDomainKey(name: "defaults.key", domain: .userDefaults(suite: nil))
try await router.set("value", for: key) try await router.set("value", for: key)
#expect(try await router.get(key) == "value") #expect(try await router.get(key) == "value")
try await router.remove(key) try await router.remove(key)
@ -46,14 +42,14 @@ import Testing
// We use a mock configuration to avoid requiring a real app group // We use a mock configuration to avoid requiring a real app group
await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: "group.test")) await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: "group.test"))
let key = DomainKey(name: "appgroup.defaults.key", domain: .appGroupUserDefaults(identifier: "group.test")) let key = makeDomainKey(name: "appgroup.defaults.key", domain: .appGroupUserDefaults(identifier: "group.test"))
try await router.set("value", for: key) try await router.set("value", for: key)
#expect(try await router.get(key) == "value") #expect(try await router.get(key) == "value")
try await router.remove(key) try await router.remove(key)
} }
@Test func domainKeychain() async throws { @Test func domainKeychain() async throws {
let key = DomainKey( let key = makeDomainKey(
name: "keychain.key", name: "keychain.key",
domain: .keychain(service: "test"), domain: .keychain(service: "test"),
security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none) security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none)
@ -64,14 +60,14 @@ import Testing
} }
@Test func domainFileSystem() async throws { @Test func domainFileSystem() async throws {
let key = DomainKey(name: "file.key", domain: .fileSystem(directory: .documents)) let key = makeDomainKey(name: "file.key", domain: .fileSystem(directory: .documents))
try await router.set("value", for: key) try await router.set("value", for: key)
#expect(try await router.get(key) == "value") #expect(try await router.get(key) == "value")
try await router.remove(key) try await router.remove(key)
} }
@Test func domainEncryptedFileSystem() async throws { @Test func domainEncryptedFileSystem() async throws {
let key = DomainKey(name: "encfile.key", domain: .encryptedFileSystem(directory: .documents)) let key = makeDomainKey(name: "encfile.key", domain: .encryptedFileSystem(directory: .documents))
try await router.set("value", for: key) try await router.set("value", for: key)
#expect(try await router.get(key) == "value") #expect(try await router.get(key) == "value")
try await router.remove(key) try await router.remove(key)
@ -80,7 +76,10 @@ import Testing
@Test func domainAppGroupFileSystem() async throws { @Test func domainAppGroupFileSystem() async throws {
// App blocks usually fail or return nil in tests, but we exercise the path // App blocks usually fail or return nil in tests, but we exercise the path
await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: "group.test")) await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: "group.test"))
let key = DomainKey(name: "appgroup.file.key", domain: .appGroupFileSystem(identifier: "group.test", directory: .documents)) let key = makeDomainKey(
name: "appgroup.file.key",
domain: .appGroupFileSystem(identifier: "group.test", directory: .documents)
)
do { do {
try await router.set("value", for: key) try await router.set("value", for: key)
@ -94,7 +93,7 @@ import Testing
@Test func resolutionFailureService() async throws { @Test func resolutionFailureService() async throws {
// Clear default service // Clear default service
await router.updateStorageConfiguration(StorageConfiguration(defaultKeychainService: nil)) await router.updateStorageConfiguration(StorageConfiguration(defaultKeychainService: nil))
let key = DomainKey( let key = makeDomainKey(
name: "bad.service.key", name: "bad.service.key",
domain: .keychain(service: nil), domain: .keychain(service: nil),
security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none) security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none)
@ -108,7 +107,7 @@ import Testing
@Test func resolutionFailureIdentifier() async throws { @Test func resolutionFailureIdentifier() async throws {
// Clear default identifier // Clear default identifier
await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: nil)) await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: nil))
let key = DomainKey(name: "bad.id.key", domain: .appGroupUserDefaults(identifier: nil)) let key = makeDomainKey(name: "bad.id.key", domain: .appGroupUserDefaults(identifier: nil))
await #expect(throws: StorageError.invalidAppGroupIdentifier("none")) { await #expect(throws: StorageError.invalidAppGroupIdentifier("none")) {
try await router.set("value", for: key) try await router.set("value", for: key)

View File

@ -2,21 +2,19 @@ import Foundation
import Testing import Testing
@testable import LocalData @testable import LocalData
private struct MockKey: StorageKey { private func makeMockKey(name: String, domain: StorageDomain) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain domain: domain,
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "ErrorTests",
let owner: String = "ErrorTests" description: "Test key"
let description: String = "Test key" )
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
} }
private struct PartialCatalog: StorageKeyCatalog { private struct PartialCatalog: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[.key(MockKey(name: "registered.key", domain: .userDefaults(suite: nil)))] [.key(makeMockKey(name: "registered.key", domain: .userDefaults(suite: nil)))]
} }
} }
@ -37,7 +35,7 @@ struct RouterErrorTests {
@Test func unregisteredKeyThrows() async throws { @Test func unregisteredKeyThrows() async throws {
try await router.registerCatalog(PartialCatalog()) try await router.registerCatalog(PartialCatalog())
let badKey = MockKey(name: "unregistered.key", domain: .userDefaults(suite: nil)) let badKey = makeMockKey(name: "unregistered.key", domain: .userDefaults(suite: nil))
await #expect(throws: StorageError.unregisteredKey("unregistered.key")) { await #expect(throws: StorageError.unregisteredKey("unregistered.key")) {
try await router.set("value", for: badKey) try await router.set("value", for: badKey)
@ -51,7 +49,7 @@ struct RouterErrorTests {
defaultAppGroupIdentifier: nil defaultAppGroupIdentifier: nil
)) ))
let appGroupKey = MockKey(name: "appgroup.key", domain: .appGroupUserDefaults(identifier: nil)) let appGroupKey = makeMockKey(name: "appgroup.key", domain: .appGroupUserDefaults(identifier: nil))
await #expect(throws: StorageError.invalidAppGroupIdentifier("none")) { await #expect(throws: StorageError.invalidAppGroupIdentifier("none")) {
try await router.set("value", for: appGroupKey) try await router.set("value", for: appGroupKey)
@ -65,7 +63,7 @@ struct RouterErrorTests {
defaultAppGroupIdentifier: "test" defaultAppGroupIdentifier: "test"
)) ))
let _ = MockKey(name: "keychain.key", domain: .keychain(service: nil)) let _ = makeMockKey(name: "keychain.key", domain: .keychain(service: nil))
// Note: Keychain security policy must match keychain domain in descriptor // Note: Keychain security policy must match keychain domain in descriptor
// but descriptor is usually created from key. // but descriptor is usually created from key.

View File

@ -17,20 +17,22 @@ import Security
) )
} }
private struct SecurityKey: StorageKey { private func makeSecurityKey(
typealias Value = String name: String,
let name: String domain: StorageDomain,
let domain: StorageDomain security: SecurityPolicy
let security: SecurityPolicy ) -> StorageKey<String> {
let serializer: Serializer<String> = .json StorageKey(
let owner: String = "SecurityTests" name: name,
let description: String = "Security test key" domain: domain,
let availability: PlatformAvailability = .all security: security,
let syncPolicy: SyncPolicy = .never owner: "SecurityTests",
description: "Security test key"
)
} }
@Test func applySecurityNone() async throws { @Test func applySecurityNone() async throws {
let key = SecurityKey(name: "none.key", domain: .userDefaults(suite: nil), security: .none) let key = makeSecurityKey(name: "none.key", domain: .userDefaults(suite: nil), security: .none)
let value = "test-value" let value = "test-value"
try await router.set(value, for: key) try await router.set(value, for: key)
@ -39,7 +41,7 @@ import Security
} }
@Test func applySecurityEncryptedAES() async throws { @Test func applySecurityEncryptedAES() async throws {
let key = SecurityKey( let key = makeSecurityKey(
name: "aes.key", name: "aes.key",
domain: .userDefaults(suite: nil), domain: .userDefaults(suite: nil),
security: .encrypted(.aes256(keyDerivation: .hkdf())) security: .encrypted(.aes256(keyDerivation: .hkdf()))
@ -52,7 +54,7 @@ import Security
} }
@Test func applySecurityEncryptedChaCha() async throws { @Test func applySecurityEncryptedChaCha() async throws {
let key = SecurityKey( let key = makeSecurityKey(
name: "chacha.key", name: "chacha.key",
domain: .userDefaults(suite: nil), domain: .userDefaults(suite: nil),
security: .encrypted(.chacha20Poly1305(keyDerivation: .hkdf())) security: .encrypted(.chacha20Poly1305(keyDerivation: .hkdf()))
@ -65,7 +67,7 @@ import Security
} }
@Test func applySecurityKeychain() async throws { @Test func applySecurityKeychain() async throws {
let key = SecurityKey( let key = makeSecurityKey(
name: "keychain.key", name: "keychain.key",
domain: .keychain(service: "test-service"), domain: .keychain(service: "test-service"),
security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none) security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none)
@ -78,7 +80,7 @@ import Security
} }
@Test func applySecurityPBKDF2() async throws { @Test func applySecurityPBKDF2() async throws {
let key = SecurityKey( let key = makeSecurityKey(
name: "pbkdf2.key", name: "pbkdf2.key",
domain: .userDefaults(suite: nil), domain: .userDefaults(suite: nil),
security: .encrypted(.aes256(keyDerivation: .pbkdf2())) security: .encrypted(.aes256(keyDerivation: .pbkdf2()))

View File

@ -4,22 +4,14 @@ import Testing
// MARK: - Test Keys // MARK: - Test Keys
private struct TestCatalogKey: StorageKey { private func makeCatalogKey(name: String, description: String = "Test key") -> StorageKey<String> {
typealias Value = String StorageKey(
name: name,
let name: String domain: .userDefaults(suite: nil),
let domain: StorageDomain = .userDefaults(suite: nil) security: .none,
let security: SecurityPolicy = .none owner: "CatalogTests",
let serializer: Serializer<String> = .json description: description
let owner: String = "CatalogTests" )
let description: String
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
init(name: String, description: String = "Test key") {
self.name = name
self.description = description
}
} }
// MARK: - Test Catalogs // MARK: - Test Catalogs
@ -27,8 +19,8 @@ private struct TestCatalogKey: StorageKey {
private struct ValidCatalog: StorageKeyCatalog { private struct ValidCatalog: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[ [
.key(TestCatalogKey(name: "valid.key1", description: "First test key")), .key(makeCatalogKey(name: "valid.key1", description: "First test key")),
.key(TestCatalogKey(name: "valid.key2", description: "Second test key")) .key(makeCatalogKey(name: "valid.key2", description: "Second test key"))
] ]
} }
} }
@ -36,8 +28,8 @@ private struct ValidCatalog: StorageKeyCatalog {
private struct DuplicateNameCatalog: StorageKeyCatalog { private struct DuplicateNameCatalog: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[ [
.key(TestCatalogKey(name: "duplicate.name", description: "First instance")), .key(makeCatalogKey(name: "duplicate.name", description: "First instance")),
.key(TestCatalogKey(name: "duplicate.name", description: "Second instance")) .key(makeCatalogKey(name: "duplicate.name", description: "Second instance"))
] ]
} }
} }
@ -49,7 +41,7 @@ private struct EmptyCatalog: StorageKeyCatalog {
private struct MissingDescriptionCatalog: StorageKeyCatalog { private struct MissingDescriptionCatalog: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[ [
.key(TestCatalogKey(name: "missing.desc", description: " ")) .key(makeCatalogKey(name: "missing.desc", description: " "))
] ]
} }
} }
@ -78,7 +70,7 @@ struct StorageCatalogTests {
} }
@Test func descriptorCapturesKeyMetadata() { @Test func descriptorCapturesKeyMetadata() {
let key = TestCatalogKey(name: "metadata.test", description: "Metadata test key") let key = makeCatalogKey(name: "metadata.test", description: "Metadata test key")
let anyKey = AnyStorageKey.key(key) let anyKey = AnyStorageKey.key(key)
let descriptor = anyKey.descriptor let descriptor = anyKey.descriptor

View File

@ -4,19 +4,13 @@ import Testing
@Suite struct StorageKeyDefaultsTests { @Suite struct StorageKeyDefaultsTests {
private struct MinimalKey: StorageKey {
typealias Value = Int
let name: String = "minimal.key"
let domain: StorageDomain = .userDefaults(suite: nil)
let serializer: Serializer<Int> = .json
let owner: String = "Test"
let description: String = "Test"
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
@Test func defaultSecurityPolicyIsRecommended() { @Test func defaultSecurityPolicyIsRecommended() {
let key = MinimalKey() let key = StorageKey<Int>(
name: "minimal.key",
domain: .userDefaults(suite: nil),
owner: "Test",
description: "Test"
)
// This exercises the default implementation in StorageKey+Defaults.swift // This exercises the default implementation in StorageKey+Defaults.swift
#expect(key.security == .recommended) #expect(key.security == .recommended)
} }

View File

@ -16,21 +16,20 @@ struct SyncIntegrationTests {
) )
} }
private struct SyncKey: StorageKey { private static func makeSyncKey(name: String) -> StorageKey<String> {
typealias Value = String StorageKey(
let name: String name: name,
let domain: StorageDomain = .userDefaults(suite: nil) domain: .userDefaults(suite: nil),
let security: SecurityPolicy = .none security: .none,
let serializer: Serializer<String> = .json owner: "SyncTests",
let owner: String = "SyncTests" description: "Sync key",
let description: String = "Sync key" syncPolicy: .automaticSmall
let availability: PlatformAvailability = .all )
let syncPolicy: SyncPolicy = .automaticSmall
} }
private struct SyncCatalog: StorageKeyCatalog { private struct SyncCatalog: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[.key(SyncKey(name: "sync.test.key"))] [.key(SyncIntegrationTests.makeSyncKey(name: "sync.test.key"))]
} }
} }
@ -46,7 +45,7 @@ struct SyncIntegrationTests {
try await router.updateFromSync(keyName: keyName, data: data) try await router.updateFromSync(keyName: keyName, data: data)
// 3. Verify it was stored in the local domain // 3. Verify it was stored in the local domain
let retrieved: String? = try await router.get(SyncKey(name: keyName)) let retrieved: String? = try await router.get(Self.makeSyncKey(name: keyName))
#expect(retrieved == expectedValue) #expect(retrieved == expectedValue)
} }

View File

@ -1,7 +1,7 @@
{ {
"configurations" : [ "configurations" : [
{ {
"id" : "D2951487-F388-4A07-A1E8-3A4B179619B9", "id" : "CB38B4BA-86AE-457E-B74C-31A492DEB330",
"name" : "Configuration 1", "name" : "Configuration 1",
"options" : { "options" : {