diff --git a/Tests/LocalDataTests/KeychainHelperTests.swift b/Tests/LocalDataTests/KeychainHelperTests.swift new file mode 100644 index 0000000..3ed5148 --- /dev/null +++ b/Tests/LocalDataTests/KeychainHelperTests.swift @@ -0,0 +1,143 @@ +import Foundation +import Testing +@testable import LocalData + +struct KeychainHelperTests { + private let testService = "LocalDataTests.Keychain.\(UUID().uuidString)" + + // MARK: - Basic Round Trip + + @Test func keychainRoundTrip() async throws { + let key = "test.credential" + let data = Data("secret-password".utf8) + + defer { + try? KeychainHelper.shared.delete(service: testService, key: key) + } + + try await KeychainHelper.shared.set( + data, + service: testService, + key: key, + accessibility: .afterFirstUnlock + ) + + let retrieved = try await KeychainHelper.shared.get(service: testService, key: key) + #expect(retrieved == data) + } + + @Test func keychainNotFoundReturnsNil() async throws { + let result = try await KeychainHelper.shared.get( + service: testService, + key: "nonexistent.\(UUID().uuidString)" + ) + #expect(result == nil) + } + + @Test func keychainDeleteNonexistentDoesNotThrow() async throws { + // Should not throw even if item doesn't exist + try await KeychainHelper.shared.delete( + service: testService, + key: "nonexistent.\(UUID().uuidString)" + ) + } + + @Test func keychainExists() async throws { + let key = "test.exists.\(UUID().uuidString)" + let data = Data("test".utf8) + + defer { + try? KeychainHelper.shared.delete(service: testService, key: key) + } + + let beforeExists = try await KeychainHelper.shared.exists(service: testService, key: key) + #expect(beforeExists == false) + + try await KeychainHelper.shared.set( + data, + service: testService, + key: key, + accessibility: .whenUnlocked + ) + + let afterExists = try await KeychainHelper.shared.exists(service: testService, key: key) + #expect(afterExists == true) + } + + @Test func keychainUpdateReplacesData() async throws { + let key = "test.update.\(UUID().uuidString)" + let originalData = Data("original".utf8) + let updatedData = Data("updated".utf8) + + defer { + try? KeychainHelper.shared.delete(service: testService, key: key) + } + + try await KeychainHelper.shared.set( + originalData, + service: testService, + key: key, + accessibility: .afterFirstUnlock + ) + + try await KeychainHelper.shared.set( + updatedData, + service: testService, + key: key, + accessibility: .afterFirstUnlock + ) + + let retrieved = try await KeychainHelper.shared.get(service: testService, key: key) + #expect(retrieved == updatedData) + } + + @Test func keychainDeleteAll() async throws { + let deleteAllService = "LocalDataTests.DeleteAll.\(UUID().uuidString)" + let data = Data("test".utf8) + + // Create multiple items + for i in 0..<3 { + try await KeychainHelper.shared.set( + data, + service: deleteAllService, + key: "key\(i)", + accessibility: .afterFirstUnlock + ) + } + + // Verify they exist + let exists0 = try await KeychainHelper.shared.exists(service: deleteAllService, key: "key0") + #expect(exists0 == true) + + // Delete all + try await KeychainHelper.shared.deleteAll(service: deleteAllService) + + // Verify they're gone + for i in 0..<3 { + let exists = try await KeychainHelper.shared.exists(service: deleteAllService, key: "key\(i)") + #expect(exists == false) + } + } + + // MARK: - Accessibility Options + + @Test(arguments: KeychainAccessibility.allCases) + func keychainAccessibilityOptions(accessibility: KeychainAccessibility) async throws { + let key = "test.accessibility.\(accessibility).\(UUID().uuidString)" + let data = Data("data-for-\(accessibility)".utf8) + + defer { + try? KeychainHelper.shared.delete(service: testService, key: key) + } + + try await KeychainHelper.shared.set( + data, + service: testService, + key: key, + accessibility: accessibility + ) + + let retrieved = try await KeychainHelper.shared.get(service: testService, key: key) + #expect(retrieved == data) + } +} diff --git a/Tests/LocalDataTests/StorageCatalogTests.swift b/Tests/LocalDataTests/StorageCatalogTests.swift new file mode 100644 index 0000000..45f3d34 --- /dev/null +++ b/Tests/LocalDataTests/StorageCatalogTests.swift @@ -0,0 +1,95 @@ +import Foundation +import Testing +@testable import LocalData + +// MARK: - Test Keys + +private struct TestCatalogKey: StorageKey { + typealias Value = String + + let name: String + let domain: StorageDomain = .userDefaults(suite: nil) + let security: SecurityPolicy = .none + let serializer: Serializer = .json + 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 + +private struct ValidCatalog: StorageKeyCatalog { + static var allKeys: [AnyStorageKey] { + [ + .key(TestCatalogKey(name: "valid.key1", description: "First test key")), + .key(TestCatalogKey(name: "valid.key2", description: "Second test key")) + ] + } +} + +private struct DuplicateNameCatalog: StorageKeyCatalog { + static var allKeys: [AnyStorageKey] { + [ + .key(TestCatalogKey(name: "duplicate.name", description: "First instance")), + .key(TestCatalogKey(name: "duplicate.name", description: "Second instance")) + ] + } +} + +private struct EmptyCatalog: StorageKeyCatalog { + static var allKeys: [AnyStorageKey] { [] } +} + +// MARK: - Tests + +struct StorageCatalogTests { + + @Test func auditReportContainsAllKeys() { + let items = StorageAuditReport.items(for: ValidCatalog.self) + + #expect(items.count == 2) + #expect(items[0].name == "valid.key1") + #expect(items[1].name == "valid.key2") + } + + @Test func auditReportRendersText() { + let report = StorageAuditReport.renderText(for: ValidCatalog.self) + + #expect(report.contains("valid.key1")) + #expect(report.contains("valid.key2")) + #expect(report.contains("First test key")) + #expect(report.contains("Second test key")) + } + + @Test func descriptorCapturesKeyMetadata() { + let key = TestCatalogKey(name: "metadata.test", description: "Metadata test key") + let anyKey = AnyStorageKey.key(key) + let descriptor = anyKey.descriptor + + #expect(descriptor.name == "metadata.test") + #expect(descriptor.owner == "CatalogTests") + #expect(descriptor.description == "Metadata test key") + #expect(descriptor.valueType == "String") + } + + @Test func emptyReportForEmptyCatalog() { + let items = StorageAuditReport.items(for: EmptyCatalog.self) + #expect(items.isEmpty) + + let report = StorageAuditReport.renderText(for: EmptyCatalog.self) + #expect(report.isEmpty) + } + + @Test func catalogRegistrationDetectsDuplicates() async { + // Attempting to register a catalog with duplicate key names should throw + await #expect(throws: StorageError.self) { + try await StorageRouter.shared.registerCatalog(DuplicateNameCatalog.self) + } + } +}