buildanything 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +55 -0
  4. package/README.md +71 -61
  5. package/agents/ios-app-review-guardian.md +49 -0
  6. package/agents/ios-foundation-models-specialist.md +46 -0
  7. package/agents/ios-storekit-specialist.md +52 -0
  8. package/agents/ios-swift-architect.md +102 -0
  9. package/agents/ios-swift-search.md +130 -0
  10. package/agents/ios-swift-ui-design.md +104 -0
  11. package/commands/build.md +80 -176
  12. package/commands/fix.md +65 -0
  13. package/commands/setup.md +73 -0
  14. package/commands/ux-review.md +63 -0
  15. package/commands/verify.md +72 -0
  16. package/hooks/session-start +18 -1
  17. package/package.json +5 -2
  18. package/protocols/brainstorm.md +99 -0
  19. package/protocols/build-fix.md +52 -0
  20. package/protocols/cleanup.md +54 -0
  21. package/protocols/design.md +269 -0
  22. package/protocols/eval-harness.md +61 -0
  23. package/protocols/fake-data-detector.md +64 -0
  24. package/protocols/ios-context.md +235 -0
  25. package/protocols/ios-frameworks-map.md +323 -0
  26. package/protocols/ios-phase-branches.md +162 -0
  27. package/protocols/ios-preflight.md +27 -0
  28. package/protocols/metric-loop.md +93 -0
  29. package/protocols/planning.md +87 -0
  30. package/protocols/smoke-test.md +110 -0
  31. package/protocols/verify.md +67 -0
  32. package/protocols/web-phase-branches.md +201 -0
  33. package/skills/ios/_VENDORED.md +60 -0
  34. package/skills/ios/activitykit/LICENSE +131 -0
  35. package/skills/ios/activitykit/SKILL.md +505 -0
  36. package/skills/ios/activitykit/references/activitykit-patterns.md +868 -0
  37. package/skills/ios/app-intents/LICENSE +131 -0
  38. package/skills/ios/app-intents/SKILL.md +494 -0
  39. package/skills/ios/app-intents/references/appintents-advanced.md +1076 -0
  40. package/skills/ios/apple-on-device-ai/LICENSE +131 -0
  41. package/skills/ios/apple-on-device-ai/SKILL.md +505 -0
  42. package/skills/ios/apple-on-device-ai/references/coreml-conversion.md +425 -0
  43. package/skills/ios/apple-on-device-ai/references/coreml-optimization.md +344 -0
  44. package/skills/ios/apple-on-device-ai/references/foundation-models.md +508 -0
  45. package/skills/ios/apple-on-device-ai/references/mlx-swift.md +285 -0
  46. package/skills/ios/ios-26-platform/SKILL.md +53 -0
  47. package/skills/ios/ios-26-platform/references/automatic-adoption.md +161 -0
  48. package/skills/ios/ios-26-platform/references/backward-compat.md +238 -0
  49. package/skills/ios/ios-26-platform/references/liquid-glass.md +255 -0
  50. package/skills/ios/ios-26-platform/references/swiftui-apis.md +277 -0
  51. package/skills/ios/ios-26-platform/references/toolbar-navigation.md +250 -0
  52. package/skills/ios/ios-bootstrap/SKILL.md +98 -0
  53. package/skills/ios/ios-bootstrap/references/apple-docs-mcp-config.md +28 -0
  54. package/skills/ios/ios-bootstrap/references/new-project-dialog.md +41 -0
  55. package/skills/ios/ios-bootstrap/references/xcode-mcp-config.md +29 -0
  56. package/skills/ios/ios-debugger-agent/LICENSE +21 -0
  57. package/skills/ios/ios-debugger-agent/SKILL.md +58 -0
  58. package/skills/ios/ios-debugger-agent/agents/openai.yaml +4 -0
  59. package/skills/ios/ios-entitlements-generator/SKILL.md +47 -0
  60. package/skills/ios/ios-hig/SKILL.md +41 -0
  61. package/skills/ios/ios-hig/references/accessibility.md +81 -0
  62. package/skills/ios/ios-hig/references/content.md +142 -0
  63. package/skills/ios/ios-hig/references/feedback.md +123 -0
  64. package/skills/ios/ios-hig/references/interaction.md +199 -0
  65. package/skills/ios/ios-hig/references/performance-platform.md +129 -0
  66. package/skills/ios/ios-hig/references/privacy-permissions.md +181 -0
  67. package/skills/ios/ios-hig/references/visual-design.md +84 -0
  68. package/skills/ios/ios-info-plist-hardening/SKILL.md +130 -0
  69. package/skills/ios/ios-maestro-flow-author/SKILL.md +68 -0
  70. package/skills/ios/ios-maestro-flow-author/references/input-and-scroll.yaml +17 -0
  71. package/skills/ios/ios-maestro-flow-author/references/modal-and-dismiss.yaml +14 -0
  72. package/skills/ios/ios-maestro-flow-author/references/onboarding-flow.yaml +16 -0
  73. package/skills/ios/ios-maestro-flow-author/references/tab-navigation.yaml +13 -0
  74. package/skills/ios/ios-maestro-flow-author/references/tap-and-assert.yaml +9 -0
  75. package/skills/ios/swift-accessibility/LICENSE +21 -0
  76. package/skills/ios/swift-accessibility/SKILL.md +371 -0
  77. package/skills/ios/swift-accessibility/examples/before-after-appkit.md +446 -0
  78. package/skills/ios/swift-accessibility/examples/before-after-swiftui.md +441 -0
  79. package/skills/ios/swift-accessibility/examples/before-after-uikit.md +464 -0
  80. package/skills/ios/swift-accessibility/references/assistive-access.md +441 -0
  81. package/skills/ios/swift-accessibility/references/display-settings.md +491 -0
  82. package/skills/ios/swift-accessibility/references/dynamic-type.md +420 -0
  83. package/skills/ios/swift-accessibility/references/media-accessibility.md +421 -0
  84. package/skills/ios/swift-accessibility/references/motor-input.md +393 -0
  85. package/skills/ios/swift-accessibility/references/nutrition-labels.md +362 -0
  86. package/skills/ios/swift-accessibility/references/platform-specifics.md +515 -0
  87. package/skills/ios/swift-accessibility/references/semantic-structure.md +585 -0
  88. package/skills/ios/swift-accessibility/references/testing-auditing.md +507 -0
  89. package/skills/ios/swift-accessibility/references/voice-control.md +317 -0
  90. package/skills/ios/swift-accessibility/references/voiceover-swiftui.md +584 -0
  91. package/skills/ios/swift-accessibility/references/voiceover-uikit.md +519 -0
  92. package/skills/ios/swift-accessibility/references/wcag-mapping.md +167 -0
  93. package/skills/ios/swift-accessibility/resources/audit-template.swift +128 -0
  94. package/skills/ios/swift-accessibility/resources/qa-checklist.md +258 -0
  95. package/skills/ios/swift-concurrency/LICENSE +21 -0
  96. package/skills/ios/swift-concurrency/SKILL.md +171 -0
  97. package/skills/ios/swift-concurrency/references/_index.md +50 -0
  98. package/skills/ios/swift-concurrency/references/actors.md +660 -0
  99. package/skills/ios/swift-concurrency/references/async-algorithms.md +847 -0
  100. package/skills/ios/swift-concurrency/references/async-await-basics.md +266 -0
  101. package/skills/ios/swift-concurrency/references/async-sequences.md +710 -0
  102. package/skills/ios/swift-concurrency/references/core-data.md +560 -0
  103. package/skills/ios/swift-concurrency/references/glossary.md +135 -0
  104. package/skills/ios/swift-concurrency/references/linting.md +155 -0
  105. package/skills/ios/swift-concurrency/references/memory-management.md +569 -0
  106. package/skills/ios/swift-concurrency/references/migration.md +1104 -0
  107. package/skills/ios/swift-concurrency/references/performance.md +593 -0
  108. package/skills/ios/swift-concurrency/references/sendable.md +598 -0
  109. package/skills/ios/swift-concurrency/references/tasks.md +636 -0
  110. package/skills/ios/swift-concurrency/references/testing.md +592 -0
  111. package/skills/ios/swift-concurrency/references/threading.md +495 -0
  112. package/skills/ios/swift-security-expert/LICENSE +21 -0
  113. package/skills/ios/swift-security-expert/SKILL.md +470 -0
  114. package/skills/ios/swift-security-expert/references/biometric-authentication.md +565 -0
  115. package/skills/ios/swift-security-expert/references/certificate-trust.md +592 -0
  116. package/skills/ios/swift-security-expert/references/common-anti-patterns.md +690 -0
  117. package/skills/ios/swift-security-expert/references/compliance-owasp-mapping.md +537 -0
  118. package/skills/ios/swift-security-expert/references/credential-storage-patterns.md +721 -0
  119. package/skills/ios/swift-security-expert/references/cryptokit-public-key.md +505 -0
  120. package/skills/ios/swift-security-expert/references/cryptokit-symmetric.md +497 -0
  121. package/skills/ios/swift-security-expert/references/keychain-access-control.md +508 -0
  122. package/skills/ios/swift-security-expert/references/keychain-fundamentals.md +596 -0
  123. package/skills/ios/swift-security-expert/references/keychain-item-classes.md +476 -0
  124. package/skills/ios/swift-security-expert/references/keychain-sharing.md +458 -0
  125. package/skills/ios/swift-security-expert/references/migration-legacy-stores.md +727 -0
  126. package/skills/ios/swift-security-expert/references/secure-enclave.md +539 -0
  127. package/skills/ios/swift-security-expert/references/testing-security-code.md +781 -0
  128. package/skills/ios/swift-testing-expert/LICENSE +21 -0
  129. package/skills/ios/swift-testing-expert/SKILL.md +79 -0
  130. package/skills/ios/swift-testing-expert/references/_index.md +12 -0
  131. package/skills/ios/swift-testing-expert/references/async-testing-and-waiting.md +127 -0
  132. package/skills/ios/swift-testing-expert/references/expectations.md +145 -0
  133. package/skills/ios/swift-testing-expert/references/fundamentals.md +141 -0
  134. package/skills/ios/swift-testing-expert/references/migration-from-xctest.md +127 -0
  135. package/skills/ios/swift-testing-expert/references/parallelization-and-isolation.md +95 -0
  136. package/skills/ios/swift-testing-expert/references/parameterized-testing.md +284 -0
  137. package/skills/ios/swift-testing-expert/references/performance-and-best-practices.md +187 -0
  138. package/skills/ios/swift-testing-expert/references/traits-and-tags.md +114 -0
  139. package/skills/ios/swift-testing-expert/references/xcode-workflows.md +70 -0
  140. package/skills/ios/swiftdata-pro/LICENSE +21 -0
  141. package/skills/ios/swiftdata-pro/SKILL.md +102 -0
  142. package/skills/ios/swiftdata-pro/agents/openai.yaml +10 -0
  143. package/skills/ios/swiftdata-pro/assets/swiftdata-pro-icon.png +0 -0
  144. package/skills/ios/swiftdata-pro/assets/swiftdata-pro-icon.svg +29 -0
  145. package/skills/ios/swiftdata-pro/references/class-inheritance.md +104 -0
  146. package/skills/ios/swiftdata-pro/references/cloudkit.md +10 -0
  147. package/skills/ios/swiftdata-pro/references/core-rules.md +20 -0
  148. package/skills/ios/swiftdata-pro/references/indexing.md +27 -0
  149. package/skills/ios/swiftdata-pro/references/predicates.md +73 -0
  150. package/skills/ios/swiftui-design-principles/AGENTS.md +21 -0
  151. package/skills/ios/swiftui-design-principles/LICENSE +21 -0
  152. package/skills/ios/swiftui-design-principles/README.md +41 -0
  153. package/skills/ios/swiftui-design-principles/SKILL.md +605 -0
  154. package/skills/ios/swiftui-design-principles/metadata.json +10 -0
  155. package/skills/ios/swiftui-liquid-glass/LICENSE +21 -0
  156. package/skills/ios/swiftui-liquid-glass/SKILL.md +95 -0
  157. package/skills/ios/swiftui-liquid-glass/agents/openai.yaml +4 -0
  158. package/skills/ios/swiftui-liquid-glass/references/liquid-glass.md +280 -0
  159. package/skills/ios/swiftui-performance-audit/LICENSE +21 -0
  160. package/skills/ios/swiftui-performance-audit/SKILL.md +111 -0
  161. package/skills/ios/swiftui-performance-audit/agents/openai.yaml +4 -0
  162. package/skills/ios/swiftui-performance-audit/references/code-smells.md +150 -0
  163. package/skills/ios/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
  164. package/skills/ios/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
  165. package/skills/ios/swiftui-performance-audit/references/profiling-intake.md +44 -0
  166. package/skills/ios/swiftui-performance-audit/references/report-template.md +47 -0
  167. package/skills/ios/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
  168. package/skills/ios/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
  169. package/skills/ios/swiftui-pro/LICENSE +21 -0
  170. package/skills/ios/swiftui-pro/SKILL.md +108 -0
  171. package/skills/ios/swiftui-pro/agents/openai.yaml +10 -0
  172. package/skills/ios/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
  173. package/skills/ios/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
  174. package/skills/ios/swiftui-pro/references/accessibility.md +13 -0
  175. package/skills/ios/swiftui-pro/references/api.md +39 -0
  176. package/skills/ios/swiftui-pro/references/data.md +43 -0
  177. package/skills/ios/swiftui-pro/references/design.md +31 -0
  178. package/skills/ios/swiftui-pro/references/hygiene.md +9 -0
  179. package/skills/ios/swiftui-pro/references/navigation.md +14 -0
  180. package/skills/ios/swiftui-pro/references/performance.md +46 -0
  181. package/skills/ios/swiftui-pro/references/swift.md +56 -0
  182. package/skills/ios/swiftui-pro/references/views.md +35 -0
  183. package/skills/ios/swiftui-ui-patterns/LICENSE +21 -0
  184. package/skills/ios/swiftui-ui-patterns/SKILL.md +100 -0
  185. package/skills/ios/swiftui-ui-patterns/agents/openai.yaml +4 -0
  186. package/skills/ios/swiftui-ui-patterns/references/app-wiring.md +201 -0
  187. package/skills/ios/swiftui-ui-patterns/references/async-state.md +96 -0
  188. package/skills/ios/swiftui-ui-patterns/references/components-index.md +50 -0
  189. package/skills/ios/swiftui-ui-patterns/references/controls.md +57 -0
  190. package/skills/ios/swiftui-ui-patterns/references/deeplinks.md +66 -0
  191. package/skills/ios/swiftui-ui-patterns/references/focus.md +90 -0
  192. package/skills/ios/swiftui-ui-patterns/references/form.md +97 -0
  193. package/skills/ios/swiftui-ui-patterns/references/grids.md +71 -0
  194. package/skills/ios/swiftui-ui-patterns/references/haptics.md +71 -0
  195. package/skills/ios/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  196. package/skills/ios/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  197. package/skills/ios/swiftui-ui-patterns/references/list.md +86 -0
  198. package/skills/ios/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  199. package/skills/ios/swiftui-ui-patterns/references/macos-settings.md +71 -0
  200. package/skills/ios/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  201. package/skills/ios/swiftui-ui-patterns/references/media.md +73 -0
  202. package/skills/ios/swiftui-ui-patterns/references/menu-bar.md +101 -0
  203. package/skills/ios/swiftui-ui-patterns/references/navigationstack.md +159 -0
  204. package/skills/ios/swiftui-ui-patterns/references/overlay.md +45 -0
  205. package/skills/ios/swiftui-ui-patterns/references/performance.md +62 -0
  206. package/skills/ios/swiftui-ui-patterns/references/previews.md +48 -0
  207. package/skills/ios/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  208. package/skills/ios/swiftui-ui-patterns/references/scrollview.md +87 -0
  209. package/skills/ios/swiftui-ui-patterns/references/searchable.md +71 -0
  210. package/skills/ios/swiftui-ui-patterns/references/sheets.md +155 -0
  211. package/skills/ios/swiftui-ui-patterns/references/split-views.md +72 -0
  212. package/skills/ios/swiftui-ui-patterns/references/tabview.md +114 -0
  213. package/skills/ios/swiftui-ui-patterns/references/theming.md +71 -0
  214. package/skills/ios/swiftui-ui-patterns/references/title-menus.md +93 -0
  215. package/skills/ios/swiftui-ui-patterns/references/top-bar.md +49 -0
  216. package/skills/ios/swiftui-view-refactor/LICENSE +21 -0
  217. package/skills/ios/swiftui-view-refactor/SKILL.md +207 -0
  218. package/skills/ios/swiftui-view-refactor/agents/openai.yaml +4 -0
  219. package/skills/ios/swiftui-view-refactor/references/mv-patterns.md +161 -0
  220. package/skills/ios/widgetkit/LICENSE +131 -0
  221. package/skills/ios/widgetkit/SKILL.md +502 -0
  222. package/skills/ios/widgetkit/references/widgetkit-advanced.md +871 -0
@@ -0,0 +1,781 @@
1
+ # Testing Keychain, CryptoKit, and Biometric Code
2
+
3
+ > Scope: Unit, integration, and CI patterns for validating keychain, CryptoKit, and biometric security code across simulator, CI runners, and physical devices.
4
+
5
+ **Protocol-based abstraction is the single most important pattern for testable security code.** Wrapping Security framework calls behind a Swift protocol lets you inject an in-memory mock for unit tests while reserving real keychain integration tests for physical devices. The core challenge is that keychain behavior differs dramatically across three environments — Xcode simulator, CI runner, and physical device — and tests that ignore these differences produce flaky failures, crashes, or false confidence.
6
+
7
+ This reference covers mock design, CryptoKit round-trip tests, Secure Enclave guards, biometric mocking, CI/CD keychain creation, simulator limitations, Swift Testing framework patterns, mutation testing, and OWASP MASTG validation. All code targets Swift 5.9+/6.0, iOS 17–18+, with iOS 26 post-quantum notes where applicable.
8
+
9
+ Key sources: Apple TN3137 "On Mac keychain APIs and implementations," WWDC19-413 "Testing in Xcode," WWDC24-10179/10195 "Meet/Go further with Swift Testing," Apple Platform Security Guide, OWASP MASTG.
10
+
11
+ ---
12
+
13
+ ## Protocol-Based Keychain Abstraction
14
+
15
+ The foundation of testable keychain code is a protocol abstracting the four Security framework operations. Every view model, service, or manager that touches the keychain depends on this protocol, never on the Security framework directly.
16
+
17
+ ### KeychainServiceProtocol with Real and Mock Implementations
18
+
19
+ ```swift
20
+ import Foundation
21
+ import Security
22
+
23
+ enum KeychainError: Error, Equatable {
24
+ case duplicateItem
25
+ case itemNotFound
26
+ case authFailed
27
+ case interactionNotAllowed
28
+ case unexpectedData
29
+ case unhandledError(status: OSStatus)
30
+
31
+ init(status: OSStatus) {
32
+ switch status {
33
+ case errSecDuplicateItem: self = .duplicateItem
34
+ case errSecItemNotFound: self = .itemNotFound
35
+ case errSecAuthFailed: self = .authFailed
36
+ case errSecInteractionNotAllowed: self = .interactionNotAllowed
37
+ default: self = .unhandledError(status: status)
38
+ }
39
+ }
40
+ }
41
+
42
+ protocol KeychainServiceProtocol: Sendable {
43
+ func save(_ data: Data, forKey key: String) throws
44
+ func read(forKey key: String) throws -> Data?
45
+ func update(_ data: Data, forKey key: String) throws
46
+ func delete(forKey key: String) throws
47
+ func deleteAll() throws
48
+ }
49
+ ```
50
+
51
+ The real `KeychainService` implementation wraps `SecItem*` calls with the add-or-update pattern and proper `OSStatus` mapping (see `keychain-fundamentals.md` for the full implementation). Key points: `save` attempts update first to avoid `errSecDuplicateItem`; `delete` treats `errSecItemNotFound` as success; the class conforms to `@unchecked Sendable` with immutable stored properties.
52
+
53
+ The mock replaces Security framework with a dictionary. Runs everywhere — simulator, CI, even Linux — with zero entitlement requirements. Supports injectable errors and call counting:
54
+
55
+ ```swift
56
+ final class MockKeychainService: KeychainServiceProtocol, @unchecked Sendable {
57
+ var storage: [String: Data] = [:]
58
+ var saveCallCount = 0
59
+ var readCallCount = 0
60
+ var deleteCallCount = 0
61
+ var errorToThrow: KeychainError?
62
+
63
+ func save(_ data: Data, forKey key: String) throws {
64
+ if let error = errorToThrow { throw error }
65
+ saveCallCount += 1
66
+ storage[key] = data
67
+ }
68
+
69
+ func read(forKey key: String) throws -> Data? {
70
+ if let error = errorToThrow { throw error }
71
+ readCallCount += 1
72
+ return storage[key]
73
+ }
74
+
75
+ func update(_ data: Data, forKey key: String) throws {
76
+ if let error = errorToThrow { throw error }
77
+ guard storage[key] != nil else { throw KeychainError.itemNotFound }
78
+ storage[key] = data
79
+ }
80
+
81
+ func delete(forKey key: String) throws {
82
+ if let error = errorToThrow { throw error }
83
+ storage.removeValue(forKey: key)
84
+ deleteCallCount += 1
85
+ }
86
+
87
+ func deleteAll() throws {
88
+ if let error = errorToThrow { throw error }
89
+ storage.removeAll()
90
+ }
91
+ }
92
+ ```
93
+
94
+ Business logic depends only on the protocol — never on `SecItem*` directly:
95
+
96
+ ```swift
97
+ final class AuthenticationManager {
98
+ private let keychain: KeychainServiceProtocol
99
+
100
+ init(keychain: KeychainServiceProtocol) {
101
+ self.keychain = keychain
102
+ }
103
+
104
+ func storeToken(_ token: String) throws {
105
+ guard let data = token.data(using: .utf8) else {
106
+ throw KeychainError.unexpectedData
107
+ }
108
+ try keychain.save(data, forKey: "auth_token")
109
+ }
110
+
111
+ func retrieveToken() throws -> String? {
112
+ guard let data = try keychain.read(forKey: "auth_token") else { return nil }
113
+ return String(data: data, encoding: .utf8)
114
+ }
115
+ }
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Seven Mistakes AI Generators Make in Keychain Tests
121
+
122
+ Both research providers independently identified overlapping anti-patterns. This merged list covers the full set:
123
+
124
+ **1. Tests that use the real keychain without cleanup.** Tests calling `SecItemAdd` directly leave state across runs. Second run fails with `errSecDuplicateItem` (-25299). AI generators rarely include `setUp`/`tearDown` cleanup.
125
+
126
+ **2. Assuming Secure Enclave exists on simulator.** `SecureEnclave.isAvailable` returns `false` on every simulator. Tests calling `SecureEnclave.P256.Signing.PrivateKey()` directly throw `CryptoKitError` on simulator and crash CI.
127
+
128
+ **3. Not testing error paths.** Real keychain code must handle `errSecDuplicateItem` (-25299), `errSecItemNotFound` (-25300), `errSecAuthFailed` (-25293), and `errSecInteractionNotAllowed` (-25308). AI generators almost never test these failure modes.
129
+
130
+ **4. Assuming biometric hardware.** Tests instantiating a real `LAContext` and asserting `canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics)` returns `true` fail on simulator where no biometric hardware exists.
131
+
132
+ **5. Missing test host app.** Since Xcode 9, test bundles on iOS simulator require a host app to access the keychain. Without one, `SecItemAdd` returns `-25300` or `-34018`. AI generators never mention this requirement.
133
+
134
+ **6. No service/account scoping.** Tests omitting `kSecAttrService` match items from other tests or even other apps. Every keychain operation in tests must use a unique, test-specific service identifier.
135
+
136
+ **7. Confusing data protection keychain with file-based keychain.** Per Apple TN3137, macOS has two keychain implementations. The `security` CLI works with the file-based keychain; iOS apps use the data protection keychain. CI scripts using `security create-keychain` create the wrong type for `SecItemAdd` targets.
137
+
138
+ ---
139
+
140
+ ## Simulator vs. Device Testing Matrix
141
+
142
+ Understanding exactly what works where prevents entire categories of test failures:
143
+
144
+ | Feature | Simulator | Physical Device |
145
+ | ------------------------------------------------------------- | ------------------------------------- | ---------------------------- |
146
+ | Keychain CRUD (`SecItemAdd`, etc.) | ✅ Works | ✅ Works |
147
+ | CryptoKit software crypto (AES-GCM, ChaChaPoly, P256, SHA256) | ✅ Software | ✅ Hardware-accelerated |
148
+ | `kSecAttrAccessible` values | ✅ Accepted but not hardware-enforced | ✅ Hardware-enforced |
149
+ | `SecureEnclave.isAvailable` | Returns **false** | Returns **true** (A7+) |
150
+ | `SecureEnclave.P256.Signing.PrivateKey()` | ❌ Throws | ✅ Works |
151
+ | Biometric prompt on protected items | ❌ Skipped — value returned silently | ✅ Shows prompt |
152
+ | `LAContext.canEvaluatePolicy(.biometrics)` | Returns **false** | Returns **true** if enrolled |
153
+ | Face ID simulation via Xcode menu | ✅ Manual only | N/A (real hardware) |
154
+ | Post-quantum (ML-KEM, ML-DSA) iOS 26+ | ✅ Software (iOS 26 runtime) | ✅ Works |
155
+
156
+ **Critical subtlety:** On simulator, keychain items protected with `kSecAttrAccessControl` and biometric flags return their value without showing a biometric prompt. Simulator tests that store biometric-protected items and read them succeed silently, giving false confidence the biometric gate works.
157
+
158
+ ### Conditional Compilation and Runtime Guards
159
+
160
+ ```swift
161
+ // Compile-time: exclude SE code on simulator
162
+ #if targetEnvironment(simulator)
163
+ let signingKey = SoftwareSigningKey()
164
+ #else
165
+ let signingKey = SecureEnclave.isAvailable
166
+ ? try SecureEnclaveSigningKey()
167
+ : SoftwareSigningKey()
168
+ #endif
169
+
170
+ // Runtime skip in XCTest
171
+ func testDeviceOnlyFeature() throws {
172
+ #if targetEnvironment(simulator)
173
+ throw XCTSkip("Requires physical device")
174
+ #endif
175
+ // Device-only test code here
176
+ }
177
+
178
+ // Runtime detection via ProcessInfo
179
+ struct EnvironmentDetector {
180
+ static var isSimulator: Bool {
181
+ ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] != nil
182
+ }
183
+ static var isRunningTests: Bool {
184
+ ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
185
+ }
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Essential Testing Patterns
192
+
193
+ ### setUp/tearDown Cleanup for Real Keychain Tests
194
+
195
+ ```swift
196
+ final class KeychainIntegrationTests: XCTestCase {
197
+ private let testService = "com.tests.keychain-integration"
198
+ private var keychain: KeychainService!
199
+
200
+ override func setUp() {
201
+ super.setUp()
202
+ keychain = KeychainService(service: testService)
203
+ try? keychain.deleteAll() // Clean slate
204
+ }
205
+
206
+ override func tearDown() {
207
+ try? keychain.deleteAll() // Leave no trace
208
+ super.tearDown()
209
+ }
210
+
211
+ func testSaveAndRetrieveToken() throws {
212
+ let token = "test-jwt-token-12345"
213
+ try keychain.save(token.data(using: .utf8)!, forKey: "access_token")
214
+ let retrieved = try keychain.read(forKey: "access_token")
215
+ XCTAssertEqual(String(data: retrieved!, encoding: .utf8), token)
216
+ }
217
+ }
218
+ ```
219
+
220
+ ### No Cleanup — Flaky Across Runs
221
+
222
+ ```swift
223
+ // ❌ INCORRECT: No cleanup, no isolation
224
+ final class BadKeychainTests: XCTestCase {
225
+ func testSaveToken() {
226
+ let query: [String: Any] = [
227
+ kSecClass as String: kSecClassGenericPassword,
228
+ kSecAttrAccount as String: "token",
229
+ kSecValueData as String: "secret".data(using: .utf8)!
230
+ ]
231
+ let status = SecItemAdd(query as CFDictionary, nil)
232
+ XCTAssertEqual(status, errSecSuccess)
233
+ // First run: passes ✅
234
+ // Second run: FAILS with errSecDuplicateItem (-25299) ❌
235
+ }
236
+ }
237
+ ```
238
+
239
+ ### Testing Error Paths with Injected Failures
240
+
241
+ ```swift
242
+ final class KeychainErrorPathTests: XCTestCase {
243
+ var mockKeychain: MockKeychainService!
244
+ var authManager: AuthenticationManager!
245
+
246
+ override func setUp() {
247
+ mockKeychain = MockKeychainService()
248
+ authManager = AuthenticationManager(keychain: mockKeychain)
249
+ }
250
+
251
+ func testStoreToken_whenDuplicateItem_throwsExpectedError() {
252
+ mockKeychain.errorToThrow = .duplicateItem
253
+ XCTAssertThrowsError(try authManager.storeToken("token")) { error in
254
+ XCTAssertEqual(error as? KeychainError, .duplicateItem)
255
+ }
256
+ }
257
+
258
+ func testRetrieveToken_whenAuthFailed_throwsError() {
259
+ mockKeychain.errorToThrow = .authFailed
260
+ XCTAssertThrowsError(try authManager.retrieveToken()) { error in
261
+ XCTAssertEqual(error as? KeychainError, .authFailed)
262
+ }
263
+ }
264
+
265
+ func testRetrieveToken_whenInteractionNotAllowed_throwsError() {
266
+ // Simulates the most common CI failure scenario
267
+ mockKeychain.errorToThrow = .interactionNotAllowed
268
+ XCTAssertThrowsError(try authManager.retrieveToken()) { error in
269
+ XCTAssertEqual(error as? KeychainError, .interactionNotAllowed)
270
+ }
271
+ }
272
+ }
273
+ ```
274
+
275
+ ### CryptoKit Round-Trip Tests (Simulator-Safe)
276
+
277
+ All CryptoKit software operations work on simulator. These tests run everywhere:
278
+
279
+ ```swift
280
+ import XCTest
281
+ import CryptoKit
282
+
283
+ final class CryptoKitTests: XCTestCase {
284
+
285
+ func testAESGCMRoundTrip() throws {
286
+ let key = SymmetricKey(size: .bits256)
287
+ let plaintext = "Sensitive credentials".data(using: .utf8)!
288
+ let sealedBox = try AES.GCM.seal(plaintext, using: key)
289
+ let ciphertext = sealedBox.combined!
290
+ XCTAssertNotEqual(ciphertext, plaintext)
291
+
292
+ let reopened = try AES.GCM.SealedBox(combined: ciphertext)
293
+ let decrypted = try AES.GCM.open(reopened, using: key)
294
+ XCTAssertEqual(decrypted, plaintext)
295
+ }
296
+
297
+ func testAESGCMWrongKeyFails() throws {
298
+ let correctKey = SymmetricKey(size: .bits256)
299
+ let wrongKey = SymmetricKey(size: .bits256)
300
+ let sealed = try AES.GCM.seal("secret".data(using: .utf8)!, using: correctKey)
301
+ XCTAssertThrowsError(try AES.GCM.open(sealed, using: wrongKey))
302
+ }
303
+
304
+ func testP256SignVerify() throws {
305
+ let privateKey = P256.Signing.PrivateKey()
306
+ let data = "Message to authenticate".data(using: .utf8)!
307
+ let signature = try privateKey.signature(for: data)
308
+ XCTAssertTrue(privateKey.publicKey.isValidSignature(signature, for: data))
309
+
310
+ let tampered = "Tampered message".data(using: .utf8)!
311
+ XCTAssertFalse(privateKey.publicKey.isValidSignature(signature, for: tampered))
312
+ }
313
+
314
+ func testCurve25519KeyAgreement() throws {
315
+ let alice = Curve25519.KeyAgreement.PrivateKey()
316
+ let bob = Curve25519.KeyAgreement.PrivateKey()
317
+ let aliceShared = try alice.sharedSecretFromKeyAgreement(with: bob.publicKey)
318
+ let bobShared = try bob.sharedSecretFromKeyAgreement(with: alice.publicKey)
319
+
320
+ let aliceKey = aliceShared.hkdfDerivedSymmetricKey(
321
+ using: SHA256.self, salt: Data(), sharedInfo: Data(), outputByteCount: 32)
322
+ let bobKey = bobShared.hkdfDerivedSymmetricKey(
323
+ using: SHA256.self, salt: Data(), sharedInfo: Data(), outputByteCount: 32)
324
+
325
+ // Both parties can decrypt each other's messages
326
+ let sealed = try AES.GCM.seal("test".data(using: .utf8)!, using: aliceKey)
327
+ XCTAssertEqual(try AES.GCM.open(sealed, using: bobKey), "test".data(using: .utf8)!)
328
+ }
329
+ }
330
+ ```
331
+
332
+ **iOS 26 note:** Post-quantum cryptography (ML-KEM, ML-DSA) is available via CryptoKit starting iOS 26. Gate these tests with `@available(iOS 26, *)` and use the same round-trip pattern. Software-based PQC works on simulator (see `cryptokit-public-key.md`).
333
+
334
+ ---
335
+
336
+ ## Secure Enclave Test Strategy — Protocol Fallback
337
+
338
+ > **Cross-reference contradiction:** One research source used a function returning `P256.Signing.PrivateKey` for both SE and software paths. This is a type error — `SecureEnclave.P256.Signing.PrivateKey` and `P256.Signing.PrivateKey` are distinct types. The correct approach is a protocol-based abstraction:
339
+
340
+ ### SigningKeyProvider Protocol with SE/Software Implementations
341
+
342
+ ```swift
343
+ import CryptoKit
344
+
345
+ protocol SigningKeyProvider {
346
+ func sign(_ data: Data) throws -> Data
347
+ func publicKeyData() -> Data
348
+ }
349
+
350
+ final class SecureEnclaveSigningKey: SigningKeyProvider {
351
+ private let key: SecureEnclave.P256.Signing.PrivateKey
352
+
353
+ init() throws {
354
+ guard SecureEnclave.isAvailable else {
355
+ throw KeychainError.unhandledError(status: errSecUnimplemented)
356
+ }
357
+ self.key = try SecureEnclave.P256.Signing.PrivateKey()
358
+ }
359
+
360
+ func sign(_ data: Data) throws -> Data {
361
+ try key.signature(for: data).derRepresentation
362
+ }
363
+
364
+ func publicKeyData() -> Data { key.publicKey.derRepresentation }
365
+ }
366
+
367
+ final class SoftwareSigningKey: SigningKeyProvider {
368
+ private let key = P256.Signing.PrivateKey()
369
+
370
+ func sign(_ data: Data) throws -> Data {
371
+ try key.signature(for: data).derRepresentation
372
+ }
373
+
374
+ func publicKeyData() -> Data { key.publicKey.derRepresentation }
375
+ }
376
+
377
+ struct SigningKeyFactory {
378
+ static func make() -> SigningKeyProvider {
379
+ if SecureEnclave.isAvailable,
380
+ let seKey = try? SecureEnclaveSigningKey() {
381
+ return seKey
382
+ }
383
+ return SoftwareSigningKey()
384
+ }
385
+ }
386
+ ```
387
+
388
+ ### Testing Secure Enclave Code
389
+
390
+ ```swift
391
+ // ❌ INCORRECT: Crashes on simulator and CI
392
+ func testSecureEnclaveSigning_BROKEN() throws {
393
+ let key = try SecureEnclave.P256.Signing.PrivateKey() // throws on simulator
394
+ let sig = try key.signature(for: "data".data(using: .utf8)!)
395
+ XCTAssertTrue(key.publicKey.isValidSignature(sig, for: "data".data(using: .utf8)!))
396
+ }
397
+
398
+ // ✅ CORRECT: Skip gracefully when SE unavailable
399
+ func testSecureEnclaveSigning_withGuard() throws {
400
+ try XCTSkipUnless(SecureEnclave.isAvailable,
401
+ "Secure Enclave not available — skipping on simulator")
402
+ let key = try SecureEnclave.P256.Signing.PrivateKey()
403
+ let data = "authenticated payload".data(using: .utf8)!
404
+ let sig = try key.signature(for: data)
405
+ XCTAssertTrue(key.publicKey.isValidSignature(sig, for: data))
406
+ }
407
+
408
+ // ✅ CORRECT: Protocol-based test runs everywhere
409
+ func testSigningWithFallback() throws {
410
+ let signer = SigningKeyFactory.make()
411
+ let data = "payload".data(using: .utf8)!
412
+ let sigBytes = try signer.sign(data)
413
+ XCTAssertFalse(sigBytes.isEmpty)
414
+
415
+ let publicKey = try P256.Signing.PublicKey(derRepresentation: signer.publicKeyData())
416
+ let signature = try P256.Signing.ECDSASignature(derRepresentation: sigBytes)
417
+ XCTAssertTrue(publicKey.isValidSignature(signature, for: data))
418
+ }
419
+ ```
420
+
421
+ ---
422
+
423
+ ## Biometric Flow Testing — LAContext Mocking
424
+
425
+ Wrap `LAContext` behind a protocol for full control over biometric outcomes in tests. Alternatively, subclass `LAContext` directly (simpler but tighter coupling).
426
+
427
+ ### Protocol-Based Approach (Preferred)
428
+
429
+ ```swift
430
+ import LocalAuthentication
431
+
432
+ protocol BiometricAuthContext {
433
+ func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool
434
+ func evaluatePolicy(_ policy: LAPolicy, localizedReason: String,
435
+ reply: @escaping (Bool, Error?) -> Void)
436
+ }
437
+
438
+ extension LAContext: BiometricAuthContext {}
439
+
440
+ final class BiometricAuthManager {
441
+ private let context: BiometricAuthContext
442
+
443
+ init(context: BiometricAuthContext = LAContext()) {
444
+ self.context = context
445
+ }
446
+
447
+ var isBiometricsAvailable: Bool {
448
+ context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
449
+ }
450
+
451
+ func authenticate(reason: String,
452
+ completion: @escaping (Result<Void, Error>) -> Void) {
453
+ guard isBiometricsAvailable else {
454
+ completion(.failure(LAError(.biometryNotAvailable)))
455
+ return
456
+ }
457
+ context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
458
+ localizedReason: reason) { success, error in
459
+ completion(success ? .success(()) : .failure(error ?? LAError(.authenticationFailed)))
460
+ }
461
+ }
462
+ }
463
+
464
+ final class MockBiometricContext: BiometricAuthContext {
465
+ var canEvaluateResult = true
466
+ var evaluateResult = true
467
+ var evaluateError: Error?
468
+ var evaluateCalled = false
469
+
470
+ func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool {
471
+ canEvaluateResult
472
+ }
473
+
474
+ func evaluatePolicy(_ policy: LAPolicy, localizedReason: String,
475
+ reply: @escaping (Bool, Error?) -> Void) {
476
+ evaluateCalled = true
477
+ reply(evaluateResult, evaluateError)
478
+ }
479
+ }
480
+ ```
481
+
482
+ ### Biometric Scenarios to Cover
483
+
484
+ | Scenario | canEvaluate | evaluatePolicy | Error | Expected App Behavior |
485
+ | ------------ | ----------- | -------------- | ---------------------- | ------------------------- |
486
+ | Success | true | true | nil | Proceed |
487
+ | User cancel | true | false | `.userCancel` | Retry or abort gracefully |
488
+ | Lockout | true | false | `.biometryLockout` | Fallback to passcode |
489
+ | Not enrolled | false | n/a | `.biometryNotEnrolled` | Show enrollment guidance |
490
+
491
+ ```swift
492
+ func testBiometricAuthSuccess() {
493
+ let mock = MockBiometricContext()
494
+ mock.canEvaluateResult = true
495
+ mock.evaluateResult = true
496
+ let manager = BiometricAuthManager(context: mock)
497
+
498
+ let exp = expectation(description: "auth")
499
+ manager.authenticate(reason: "Test") { result in
500
+ if case .failure = result { XCTFail("Expected success") }
501
+ exp.fulfill()
502
+ }
503
+ waitForExpectations(timeout: 1)
504
+ XCTAssertTrue(mock.evaluateCalled)
505
+ }
506
+
507
+ func testBiometricAuthUnavailable() {
508
+ let mock = MockBiometricContext()
509
+ mock.canEvaluateResult = false
510
+ let manager = BiometricAuthManager(context: mock)
511
+
512
+ let exp = expectation(description: "unavailable")
513
+ manager.authenticate(reason: "Test") { result in
514
+ if case .success = result { XCTFail("Expected failure") }
515
+ exp.fulfill()
516
+ }
517
+ waitForExpectations(timeout: 1)
518
+ XCTAssertFalse(mock.evaluateCalled) // Should not attempt auth
519
+ }
520
+ ```
521
+
522
+ ---
523
+
524
+ ## CI/CD Pipeline Configuration
525
+
526
+ Running keychain tests in CI is the most error-prone part. The `-25308` (`errSecInteractionNotAllowed`) error is the most common CI failure — keychain locked or requires GUI interaction in a headless environment.
527
+
528
+ ### GitHub Actions
529
+
530
+ ```yaml
531
+ name: iOS CI
532
+ on: [push, pull_request]
533
+ jobs:
534
+ test:
535
+ runs-on: macos-latest
536
+ steps:
537
+ - uses: actions/checkout@v5
538
+ - name: Create temporary keychain
539
+ env:
540
+ KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
541
+ run: |
542
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
543
+ security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
544
+ security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
545
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
546
+ security list-keychain -d user -s $KEYCHAIN_PATH
547
+ # Import cert + CRITICAL partition list step
548
+ echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $RUNNER_TEMP/cert.p12
549
+ security import $RUNNER_TEMP/cert.p12 -P "$P12_PASSWORD" \
550
+ -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
551
+ security set-key-partition-list -S apple-tool:,apple: \
552
+ -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
553
+ - name: Run simulator-safe tests
554
+ run: |
555
+ xcodebuild test -scheme MyApp \
556
+ -destination 'platform=iOS Simulator,name=iPhone 16' \
557
+ -testPlan CITests
558
+ - name: Cleanup
559
+ if: always()
560
+ run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
561
+ ```
562
+
563
+ **`security set-key-partition-list` must be called after importing certificates** — this is the step most people miss. Without it, `codesign` hangs indefinitely waiting for a GUI prompt. The `-A` flag on import grants access to all applications, necessary in CI.
564
+
565
+ **Xcode Cloud:** Uses ephemeral environments — no manual `security create-keychain`. Apple manages signing automatically. Ensure Keychain Sharing capability is enabled. The `-25308` error is common when SPM tries to save credentials.
566
+
567
+ **Fastlane:** `setup_ci` creates a temporary `fastlane_tmp_keychain` and sets it as default. On self-hosted runners, this can interfere with the host machine's keychain.
568
+
569
+ ```ruby
570
+ lane :ci_test do
571
+ setup_ci(timeout: 3600)
572
+ sync_code_signing(type: "development", readonly: is_ci)
573
+ run_tests(scheme: "MyApp", testplan: "CITests", device: "iPhone 16")
574
+ end
575
+ ```
576
+
577
+ ### Common CI Error Reference
578
+
579
+ | Error | OSStatus | Cause | Fix |
580
+ | ----------------------------- | -------- | ------------------------------------- | ----------------------------------------------- |
581
+ | `errSecInteractionNotAllowed` | -25308 | Keychain locked / needs GUI | Unlock keychain + `set-key-partition-list` |
582
+ | `errSecMissingEntitlement` | -34018 | No keychain-access-groups entitlement | Add entitlements to test host app |
583
+ | `errSecItemNotFound` | -25300 | No test host or missing entitlement | Use test host app with keychain capability |
584
+ | `errSecInternalComponent` | -67585 | Partition list not set after import | Call `set-key-partition-list` after cert import |
585
+ | Default keychain not found | -25307 | No default keychain on CI runner | Create and set default keychain |
586
+
587
+ ### Test Host App Requirement
588
+
589
+ Since Xcode 9, test bundles on iOS simulator require a host app to access the keychain. Without one, `SecItemAdd` returns `-25300` or `-34018`. Create a minimal iOS app target, enable the Keychain Sharing capability, and set the test target's **Test Host** and **Bundle Loader** build settings to point at it.
590
+
591
+ ---
592
+
593
+ ## Xcode Test Plans — Separating Simulator from Device
594
+
595
+ Create two test plans for CI/device split:
596
+
597
+ - **CITests.xctestplan**: Only tests using `MockKeychainService` and simulator-safe CryptoKit. Skips integration, biometric, and SE tests.
598
+ - **DeviceTests.xctestplan**: Real keychain integration, Secure Enclave, and biometric hardware tests. Requires physical device.
599
+
600
+ ```bash
601
+ # CI: simulator-safe tests on every push
602
+ xcodebuild test -scheme MyApp \
603
+ -destination 'platform=iOS Simulator,name=iPhone 16' \
604
+ -testPlan CITests
605
+
606
+ # Nightly: device farm runs everything
607
+ xcodebuild test -scheme MyApp \
608
+ -destination 'platform=iOS,id=DEVICE_UDID' \
609
+ -testPlan DeviceTests
610
+ ```
611
+
612
+ ---
613
+
614
+ ## Swift Testing Framework Patterns
615
+
616
+ Swift Testing (WWDC24) introduces tags, traits, and parameterized tests that map well to security test organization:
617
+
618
+ ```swift
619
+ import Testing
620
+ @testable import MyApp
621
+
622
+ extension Tag {
623
+ @Tag static var keychain: Self
624
+ @Tag static var deviceOnly: Self
625
+ @Tag static var ciSafe: Self
626
+ }
627
+
628
+ @Suite(.serialized, .tags(.keychain))
629
+ struct KeychainTests {
630
+
631
+ @Test("Save and retrieve round-trip", .tags(.ciSafe))
632
+ func saveAndRetrieve() throws {
633
+ let mock = MockKeychainService()
634
+ let manager = AuthenticationManager(keychain: mock)
635
+ try manager.storeToken("test-token")
636
+ let result = try #require(try manager.retrieveToken())
637
+ #expect(result == "test-token")
638
+ }
639
+
640
+ @Test("Device-only: real keychain integration",
641
+ .enabled(if: ProcessInfo.processInfo.environment["CI"] == nil),
642
+ .tags(.deviceOnly))
643
+ func realKeychainIntegration() throws {
644
+ let keychain = KeychainService(service: "com.test.swift-testing")
645
+ try keychain.deleteAll()
646
+ defer { try? keychain.deleteAll() }
647
+ try keychain.save("token".data(using: .utf8)!, forKey: "key")
648
+ let data = try #require(try keychain.read(forKey: "key"))
649
+ #expect(String(data: data, encoding: .utf8) == "token")
650
+ }
651
+
652
+ @Test("Parameterized error paths",
653
+ arguments: [
654
+ KeychainError.duplicateItem,
655
+ KeychainError.itemNotFound,
656
+ KeychainError.authFailed,
657
+ KeychainError.interactionNotAllowed
658
+ ])
659
+ func errorPathHandling(expectedError: KeychainError) {
660
+ let mock = MockKeychainService()
661
+ mock.errorToThrow = expectedError
662
+ #expect(throws: KeychainError.self) {
663
+ try mock.read(forKey: "any-key")
664
+ }
665
+ }
666
+ }
667
+ ```
668
+
669
+ The `.serialized` trait ensures keychain tests modifying shared state run sequentially. Tags integrate with test plans for filtering — `.ciSafe` tests run in CI, `.deviceOnly` tests run on device farms.
670
+
671
+ ---
672
+
673
+ ## Advanced Patterns
674
+
675
+ ### Migration Testing: UserDefaults to Keychain
676
+
677
+ Migration code is security-critical — silent failure leaves credentials in UserDefaults (see `migration-legacy-stores.md`). The class under test accepts injected dependencies for both stores:
678
+
679
+ ```swift
680
+ final class StorageMigrationManager {
681
+ private let defaults: UserDefaults
682
+ private let keychain: KeychainServiceProtocol
683
+
684
+ init(defaults: UserDefaults = .standard,
685
+ keychain: KeychainServiceProtocol) {
686
+ self.defaults = defaults
687
+ self.keychain = keychain
688
+ }
689
+
690
+ func migrateIfNeeded() throws {
691
+ let version = defaults.integer(forKey: "migration_version")
692
+ if version < 1 {
693
+ if let token = defaults.string(forKey: "auth_token"),
694
+ let data = token.data(using: .utf8) {
695
+ try keychain.save(data, forKey: "auth_token")
696
+ defaults.removeObject(forKey: "auth_token")
697
+ }
698
+ }
699
+ defaults.set(1, forKey: "migration_version")
700
+ }
701
+ }
702
+ ```
703
+
704
+ Test with isolated `UserDefaults(suiteName:)` and mock keychain:
705
+
706
+ ```swift
707
+ func testMigrationMovesTokenToKeychain() throws {
708
+ let defaults = UserDefaults(suiteName: "migration-test")!
709
+ defaults.removePersistentDomain(forName: "migration-test")
710
+ defaults.set("my-secret", forKey: "auth_token")
711
+ defaults.set(0, forKey: "migration_version")
712
+
713
+ let mock = MockKeychainService()
714
+ let migrator = StorageMigrationManager(defaults: defaults, keychain: mock)
715
+ try migrator.migrateIfNeeded()
716
+
717
+ // Token moved to keychain, removed from UserDefaults
718
+ XCTAssertEqual(String(data: mock.storage["auth_token"]!, encoding: .utf8), "my-secret")
719
+ XCTAssertNil(defaults.string(forKey: "auth_token"))
720
+ }
721
+ ```
722
+
723
+ ### Performance Testing
724
+
725
+ ```swift
726
+ func testKeychainWritePerformance() {
727
+ let keychain = KeychainService(service: "com.test.perf")
728
+ let options = XCTMeasureOptions()
729
+ options.iterationCount = 20
730
+
731
+ measure(metrics: [XCTClockMetric(), XCTCPUMetric()], options: options) {
732
+ let data = UUID().uuidString.data(using: .utf8)!
733
+ try? keychain.save(data, forKey: "perf-key")
734
+ try? keychain.delete(forKey: "perf-key")
735
+ }
736
+ }
737
+ ```
738
+
739
+ ### Mutation Testing
740
+
741
+ Mutation testing introduces deliberate bugs (flipping `==` to `!=`, removing `SecItemDelete` calls, swapping `&&` to `||`) and checks whether your tests catch them. A project can have 81% code coverage but only 16% mutation score — tests execute security code without validating it does the right thing.
742
+
743
+ **Muter** (`brew install muter-mutation-testing/muter/muter`) is the primary Swift mutation testing tool. Its `RelationalOperatorReplacement` operator catches authentication bypasses; `RemoveSideEffects` catches missing `SecItemDelete` calls in logout flows. For security code, target mutation score above **80%**.
744
+
745
+ ### OWASP MASTG Keychain Validation
746
+
747
+ MASTG-TEST-0052 requires that sensitive data use the Keychain, not `NSUserDefaults` or `.plist` files. OWASP also documents that keychain data persists after app uninstallation — the app sandbox is wiped but keychain items remain. Standard mitigation is a fresh-install detector (see `common-anti-patterns.md`):
748
+
749
+ ```swift
750
+ static func handleFreshInstall(keychain: KeychainServiceProtocol) {
751
+ let hasLaunched = UserDefaults.standard.bool(forKey: "has_launched")
752
+ if !hasLaunched {
753
+ try? keychain.deleteAll()
754
+ UserDefaults.standard.set(true, forKey: "has_launched")
755
+ }
756
+ }
757
+ ```
758
+
759
+ ---
760
+
761
+ ## Conclusion
762
+
763
+ Protocol-abstraction is non-negotiable for testable keychain code. Every `SecItem` call should be behind `KeychainServiceProtocol` so that 95%+ of your test suite runs against `MockKeychainService` with zero entitlement requirements and zero CI flakiness. Reserve real-keychain integration tests for a dedicated test plan on physical devices.
764
+
765
+ Three insights most guides miss: (1) the simulator silently returns biometric-protected items without prompting — tests appear to validate biometric gates but test nothing; (2) TN3137's distinction between file-based and data protection keychains means `security create-keychain` in CI creates the wrong keychain type; (3) mutation testing reveals that even high-coverage suites fail to catch inverted conditionals and removed side effects — the exact mutations that create real vulnerabilities.
766
+
767
+ ---
768
+
769
+ ## Summary Checklist
770
+
771
+ 1. **Protocol abstraction** — All keychain access goes through `KeychainServiceProtocol`; no direct `SecItem*` calls in business logic
772
+ 2. **Mock with injectable errors** — `MockKeychainService` supports `errorToThrow` for testing `errSecDuplicateItem`, `errSecAuthFailed`, `errSecInteractionNotAllowed`, and `errSecItemNotFound` paths
773
+ 3. **setUp/tearDown cleanup** — Every integration test using real keychain has both pre-test and post-test cleanup with a test-specific `kSecAttrService`
774
+ 4. **Secure Enclave guard** — All SE tests use `try XCTSkipUnless(SecureEnclave.isAvailable, ...)` or protocol-based fallback; never call `SecureEnclave.P256.*` unconditionally
775
+ 5. **Biometric mock** — `LAContext` wrapped behind protocol or subclass mock; tests cover success, user cancel, lockout, and not-enrolled scenarios
776
+ 6. **Simulator/device split** — Two Xcode test plans: `CITests` (mock-based, simulator-safe) and `DeviceTests` (real keychain, SE, biometrics on physical device)
777
+ 7. **CI keychain setup** — GitHub Actions calls `security set-key-partition-list` after cert import; test target has host app with Keychain Sharing capability enabled
778
+ 8. **CryptoKit round-trips** — Encrypt→decrypt and sign→verify tests for AES-GCM, ChaChaPoly, P256, Curve25519; wrong-key failure tests included
779
+ 9. **Error path coverage** — Every `OSStatus` code the app can encounter has a corresponding test with injected mock failure
780
+ 10. **Migration testing** — UserDefaults→Keychain migration tested with isolated `UserDefaults(suiteName:)` and mock keychain; verifies source cleared after migration
781
+ 11. **Mutation testing baseline** — Muter mutation score ≥80% for security-critical code paths; `RelationalOperatorReplacement` and `RemoveSideEffects` operators enabled