Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
86c3012bf7
commit
ef882bb6b2
@ -128,8 +128,8 @@ actor SyncHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A private proxy class to handle WCSessionDelegate callbacks and route them to the SyncHelper actor.
|
/// An internal proxy class to handle WCSessionDelegate callbacks and route them to the SyncHelper actor.
|
||||||
private final class SessionDelegateProxy: NSObject, WCSessionDelegate {
|
internal final class SessionDelegateProxy: NSObject, WCSessionDelegate {
|
||||||
static let shared = SessionDelegateProxy()
|
static let shared = SessionDelegateProxy()
|
||||||
|
|
||||||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||||
|
|||||||
@ -53,4 +53,33 @@ import Testing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func fileStorageAppGroupRoundTrip() async throws {
|
||||||
|
// In simulator, this usually returns a placeholder path
|
||||||
|
let identifier = "group.com.test.localdata"
|
||||||
|
let fileName = "appgroup_file.txt"
|
||||||
|
let data = Data("appgroup-file-content".utf8)
|
||||||
|
|
||||||
|
// Only run if the simulator/environment gives us a container
|
||||||
|
if let _ = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier) {
|
||||||
|
try await fileStorageHelper.write(data, to: .documents, fileName: fileName, appGroupIdentifier: identifier)
|
||||||
|
|
||||||
|
let exists = await fileStorageHelper.exists(in: .documents, fileName: fileName, appGroupIdentifier: identifier)
|
||||||
|
#expect(exists == true)
|
||||||
|
|
||||||
|
let retrieved = try await fileStorageHelper.read(from: .documents, fileName: fileName, appGroupIdentifier: identifier)
|
||||||
|
#expect(retrieved == data)
|
||||||
|
|
||||||
|
let list = try await fileStorageHelper.list(in: .documents, appGroupIdentifier: identifier)
|
||||||
|
#expect(list.contains(fileName))
|
||||||
|
|
||||||
|
let size = try await fileStorageHelper.size(of: .documents, fileName: fileName, appGroupIdentifier: identifier)
|
||||||
|
#expect(size == Int64(data.count))
|
||||||
|
|
||||||
|
try await fileStorageHelper.delete(from: .documents, fileName: fileName, appGroupIdentifier: identifier)
|
||||||
|
|
||||||
|
let afterDelete = await fileStorageHelper.exists(in: .documents, fileName: fileName, appGroupIdentifier: identifier)
|
||||||
|
#expect(afterDelete == false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
Tests/LocalDataTests/RouterDomainTests.swift
Normal file
117
Tests/LocalDataTests/RouterDomainTests.swift
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
@testable import LocalData
|
||||||
|
|
||||||
|
@Suite struct RouterDomainTests {
|
||||||
|
private let router: StorageRouter
|
||||||
|
private let mockKeychain = MockKeychainHelper()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let testBaseURL = FileManager.default.temporaryDirectory.appending(path: "RouterDomainTests-\(UUID().uuidString)")
|
||||||
|
router = StorageRouter(
|
||||||
|
keychain: mockKeychain,
|
||||||
|
encryption: EncryptionHelper(keychain: mockKeychain),
|
||||||
|
file: FileStorageHelper(configuration: FileStorageConfiguration(baseURL: testBaseURL)),
|
||||||
|
defaults: UserDefaultsHelper(defaults: UserDefaults(suiteName: "RouterDomainTests-\(UUID().uuidString)")!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DomainKey: StorageKey {
|
||||||
|
typealias Value = String
|
||||||
|
let name: String
|
||||||
|
let domain: StorageDomain
|
||||||
|
let security: SecurityPolicy
|
||||||
|
let serializer: Serializer<String> = .json
|
||||||
|
let owner: String = "DomainTests"
|
||||||
|
let description: String = "Domain test key"
|
||||||
|
let availability: PlatformAvailability = .all
|
||||||
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
|
||||||
|
init(name: String, domain: StorageDomain, security: SecurityPolicy = .none) {
|
||||||
|
self.name = name
|
||||||
|
self.domain = domain
|
||||||
|
self.security = security
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func domainUserDefaults() async throws {
|
||||||
|
let key = DomainKey(name: "defaults.key", domain: .userDefaults(suite: nil))
|
||||||
|
try await router.set("value", for: key)
|
||||||
|
#expect(try await router.get(key) == "value")
|
||||||
|
try await router.remove(key)
|
||||||
|
#expect(await (try? router.exists(key)) == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func domainAppGroupUserDefaults() async throws {
|
||||||
|
// We use a mock configuration to avoid requiring a real app group
|
||||||
|
await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: "group.test"))
|
||||||
|
|
||||||
|
let key = DomainKey(name: "appgroup.defaults.key", domain: .appGroupUserDefaults(identifier: "group.test"))
|
||||||
|
try await router.set("value", for: key)
|
||||||
|
#expect(try await router.get(key) == "value")
|
||||||
|
try await router.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func domainKeychain() async throws {
|
||||||
|
let key = DomainKey(
|
||||||
|
name: "keychain.key",
|
||||||
|
domain: .keychain(service: "test"),
|
||||||
|
security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none)
|
||||||
|
)
|
||||||
|
try await router.set("value", for: key)
|
||||||
|
#expect(try await router.get(key) == "value")
|
||||||
|
try await router.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func domainFileSystem() async throws {
|
||||||
|
let key = DomainKey(name: "file.key", domain: .fileSystem(directory: .documents))
|
||||||
|
try await router.set("value", for: key)
|
||||||
|
#expect(try await router.get(key) == "value")
|
||||||
|
try await router.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func domainEncryptedFileSystem() async throws {
|
||||||
|
let key = DomainKey(name: "encfile.key", domain: .encryptedFileSystem(directory: .documents))
|
||||||
|
try await router.set("value", for: key)
|
||||||
|
#expect(try await router.get(key) == "value")
|
||||||
|
try await router.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func domainAppGroupFileSystem() async throws {
|
||||||
|
// App blocks usually fail or return nil in tests, but we exercise the path
|
||||||
|
await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: "group.test"))
|
||||||
|
let key = DomainKey(name: "appgroup.file.key", domain: .appGroupFileSystem(identifier: "group.test", directory: .documents))
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await router.set("value", for: key)
|
||||||
|
#expect(try await router.get(key) == "value")
|
||||||
|
try await router.remove(key)
|
||||||
|
} catch StorageError.invalidAppGroupIdentifier {
|
||||||
|
// Path covered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func resolutionFailureService() async throws {
|
||||||
|
// Clear default service
|
||||||
|
await router.updateStorageConfiguration(StorageConfiguration(defaultKeychainService: nil))
|
||||||
|
let key = DomainKey(
|
||||||
|
name: "bad.service.key",
|
||||||
|
domain: .keychain(service: nil),
|
||||||
|
security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none)
|
||||||
|
)
|
||||||
|
|
||||||
|
await #expect(throws: StorageError.keychainError(errSecBadReq)) {
|
||||||
|
try await router.set("value", for: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func resolutionFailureIdentifier() async throws {
|
||||||
|
// Clear default identifier
|
||||||
|
await router.updateStorageConfiguration(StorageConfiguration(defaultAppGroupIdentifier: nil))
|
||||||
|
let key = DomainKey(name: "bad.id.key", domain: .appGroupUserDefaults(identifier: nil))
|
||||||
|
|
||||||
|
await #expect(throws: StorageError.invalidAppGroupIdentifier("none")) {
|
||||||
|
try await router.set("value", for: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
Tests/LocalDataTests/RouterSecurityTests.swift
Normal file
92
Tests/LocalDataTests/RouterSecurityTests.swift
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
import Security
|
||||||
|
@testable import LocalData
|
||||||
|
|
||||||
|
@Suite struct RouterSecurityTests {
|
||||||
|
private let router: StorageRouter
|
||||||
|
private let mockKeychain = MockKeychainHelper()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let testBaseURL = FileManager.default.temporaryDirectory.appending(path: "RouterSecurityTests-\(UUID().uuidString)")
|
||||||
|
router = StorageRouter(
|
||||||
|
keychain: mockKeychain,
|
||||||
|
encryption: EncryptionHelper(keychain: mockKeychain),
|
||||||
|
file: FileStorageHelper(configuration: FileStorageConfiguration(baseURL: testBaseURL)),
|
||||||
|
defaults: UserDefaultsHelper(defaults: UserDefaults(suiteName: "RouterSecurityTests-\(UUID().uuidString)")!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SecurityKey: StorageKey {
|
||||||
|
typealias Value = String
|
||||||
|
let name: String
|
||||||
|
let domain: StorageDomain
|
||||||
|
let security: SecurityPolicy
|
||||||
|
let serializer: Serializer<String> = .json
|
||||||
|
let owner: String = "SecurityTests"
|
||||||
|
let description: String = "Security test key"
|
||||||
|
let availability: PlatformAvailability = .all
|
||||||
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func applySecurityNone() async throws {
|
||||||
|
let key = SecurityKey(name: "none.key", domain: .userDefaults(suite: nil), security: .none)
|
||||||
|
let value = "test-value"
|
||||||
|
|
||||||
|
try await router.set(value, for: key)
|
||||||
|
let retrieved: String = try await router.get(key)
|
||||||
|
#expect(retrieved == value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func applySecurityEncryptedAES() async throws {
|
||||||
|
let key = SecurityKey(
|
||||||
|
name: "aes.key",
|
||||||
|
domain: .userDefaults(suite: nil),
|
||||||
|
security: .encrypted(.aes256(keyDerivation: .hkdf()))
|
||||||
|
)
|
||||||
|
let value = "aes-secret"
|
||||||
|
|
||||||
|
try await router.set(value, for: key)
|
||||||
|
let retrieved: String = try await router.get(key)
|
||||||
|
#expect(retrieved == value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func applySecurityEncryptedChaCha() async throws {
|
||||||
|
let key = SecurityKey(
|
||||||
|
name: "chacha.key",
|
||||||
|
domain: .userDefaults(suite: nil),
|
||||||
|
security: .encrypted(.chacha20Poly1305(keyDerivation: .hkdf()))
|
||||||
|
)
|
||||||
|
let value = "chacha-secret"
|
||||||
|
|
||||||
|
try await router.set(value, for: key)
|
||||||
|
let retrieved: String = try await router.get(key)
|
||||||
|
#expect(retrieved == value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func applySecurityKeychain() async throws {
|
||||||
|
let key = SecurityKey(
|
||||||
|
name: "keychain.key",
|
||||||
|
domain: .keychain(service: "test-service"),
|
||||||
|
security: .keychain(accessibility: .afterFirstUnlock, accessControl: .none)
|
||||||
|
)
|
||||||
|
let value = "keychain-secret"
|
||||||
|
|
||||||
|
try await router.set(value, for: key)
|
||||||
|
let retrieved: String = try await router.get(key)
|
||||||
|
#expect(retrieved == value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func applySecurityPBKDF2() async throws {
|
||||||
|
let key = SecurityKey(
|
||||||
|
name: "pbkdf2.key",
|
||||||
|
domain: .userDefaults(suite: nil),
|
||||||
|
security: .encrypted(.aes256(keyDerivation: .pbkdf2()))
|
||||||
|
)
|
||||||
|
let value = "pbkdf2-secret"
|
||||||
|
|
||||||
|
try await router.set(value, for: key)
|
||||||
|
let retrieved: String = try await router.get(key)
|
||||||
|
#expect(retrieved == value)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Tests/LocalDataTests/SyncDelegateTests.swift
Normal file
43
Tests/LocalDataTests/SyncDelegateTests.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
import WatchConnectivity
|
||||||
|
@testable import LocalData
|
||||||
|
|
||||||
|
@Suite struct SyncDelegateTests {
|
||||||
|
|
||||||
|
@Test func delegateProxyActivationCallbacks() {
|
||||||
|
let proxy = SessionDelegateProxy.shared
|
||||||
|
let session = WCSession.default
|
||||||
|
|
||||||
|
// Exercise activation completion (success)
|
||||||
|
proxy.session(session, activationDidCompleteWith: .activated, error: nil)
|
||||||
|
|
||||||
|
// Exercise activation completion (error)
|
||||||
|
let error = NSError(domain: "test", code: 1, userInfo: nil)
|
||||||
|
proxy.session(session, activationDidCompleteWith: .notActivated, error: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func delegateProxyContextReceived() async throws {
|
||||||
|
let proxy = SessionDelegateProxy.shared
|
||||||
|
let session = WCSession.default
|
||||||
|
let context: [String: Any] = [
|
||||||
|
"test.sync.key": Data("sync-data".utf8)
|
||||||
|
]
|
||||||
|
|
||||||
|
// This triggers SyncHelper.handleReceivedContext
|
||||||
|
proxy.session(session, didReceiveApplicationContext: context)
|
||||||
|
|
||||||
|
// Wait a bit for the Task to start/finish
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@Test func delegateProxyiOSCallbacks() {
|
||||||
|
let proxy = SessionDelegateProxy.shared
|
||||||
|
let session = WCSession.default
|
||||||
|
|
||||||
|
proxy.sessionDidBecomeInactive(session)
|
||||||
|
proxy.sessionDidDeactivate(session)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user