git.iko.soy/parseable-macos
on master help sign in
Add keychain entitlement and expand ConnectionStore tests (#53)

* Fix connection persistence by adding keychain-access-groups entitlement The data-protection keychain (kSecUseDataProtectionKeychain) requires the keychain-access-groups entitlement for sandboxed macOS apps. Without it, all SecItem operations silently fail, so connections and passwords are never actually persisted between app launches. Also adds round-trip persistence tests for ConnectionStore. https://claude.ai/code/session_01WVTuzgGqSn5WgU6WTxqNJo * Skip Keychain round-trip tests when data-protection keychain is unavailable CI runners may lack entitlements for kSecUseDataProtectionKeychain. A static probe detects this at test startup and skips tests that require Keychain write access, preventing false failures. https://claude.ai/code/session_01WVTuzgGqSn5WgU6WTxqNJo --------- Co-authored-by: Claude <noreply@anthropic.com>

commit1facd74257e81c705bc662c2beb632612a06d4a5 authoriko <mail@iko.soy> dateThu 12 Mar 2026, 20:35 (7 months ago) parent3988312
2 files changed+80−2
ParseableViewer/ParseableViewer.entitlements+4 ParseableViewerTests/ConnectionStoreTests.swift+76−2
ParseableViewer/ParseableViewer.entitlements+4
@@ -6,5 +6,9 @@
66 <true/>
77 <key>com.apple.security.network.client</key>
88 <true/>
9+ <key>keychain-access-groups</key>
10+ <array>
11+ <string>$(AppIdentifierPrefix)com.parseableviewer.app</string>
12+ </array>
913</dict>
1014</plist>
ParseableViewerTests/ConnectionStoreTests.swift+76−2
@@ -2,13 +2,25 @@ import XCTest
22@testable import ParseableViewer
33
44final class ConnectionStoreTests: XCTestCase {
5+ /// Whether the data-protection keychain is available in this environment.
6+ /// CI runners may lack the entitlements needed for `kSecUseDataProtectionKeychain`.
7+ private static let keychainAvailable: Bool = {
8+ let probe = "keychain-probe"
9+ let saved = KeychainService.saveData(Data(probe.utf8), for: "_test_probe")
10+ if saved {
11+ KeychainService.deleteData(for: "_test_probe")
12+ }
13+ return saved
14+ }()
15+
516 override func tearDown() {
617 super.tearDown()
718 KeychainService.deleteData(for: "parseable_connections_list")
819 KeychainService.deleteData(for: "parseable_active_connection_id")
920 }
1021
11− func testSaveNilActiveConnectionID() {
22+ func testSaveNilActiveConnectionID() throws {
23+ try XCTSkipUnless(Self.keychainAvailable, "Data-protection keychain unavailable")
1224 let id = UUID()
1325 ConnectionStore.saveActiveConnectionID(id)
1426 ConnectionStore.saveActiveConnectionID(nil)
@@ -30,7 +42,69 @@ final class ConnectionStoreTests: XCTestCase {
3042 XCTAssertTrue(connections.isEmpty)
3143 }
3244
33− func testPasswordNotInKeychainConnectionData() {
45+ func testSaveAndLoadActiveConnectionID() throws {
46+ try XCTSkipUnless(Self.keychainAvailable, "Data-protection keychain unavailable")
47+ let id = UUID()
48+ ConnectionStore.saveActiveConnectionID(id)
49+ let loaded = ConnectionStore.loadActiveConnectionID()
50+ XCTAssertEqual(loaded, id)
51+ }
52+
53+ func testRoundTripConnectionPersistence() throws {
54+ try XCTSkipUnless(Self.keychainAvailable, "Data-protection keychain unavailable")
55+ let conn = ServerConnection(
56+ name: "Prod",
57+ url: "https://logs.example.com",
58+ username: "admin",
59+ password: "s3cret!"
60+ )
61+ ConnectionStore.saveConnections([conn])
62+
63+ let loaded = ConnectionStore.loadConnections()
64+ XCTAssertEqual(loaded.count, 1)
65+ XCTAssertEqual(loaded.first?.id, conn.id)
66+ XCTAssertEqual(loaded.first?.name, "Prod")
67+ XCTAssertEqual(loaded.first?.url, "https://logs.example.com")
68+ XCTAssertEqual(loaded.first?.username, "admin")
69+ XCTAssertEqual(loaded.first?.password, "s3cret!")
70+
71+ // Clean up
72+ KeychainService.deletePassword(for: conn.id)
73+ }
74+
75+ func testRoundTripMultipleConnections() throws {
76+ try XCTSkipUnless(Self.keychainAvailable, "Data-protection keychain unavailable")
77+ let conn1 = ServerConnection(
78+ name: "Server A",
79+ url: "https://a.example.com",
80+ username: "user1",
81+ password: "pass1"
82+ )
83+ let conn2 = ServerConnection(
84+ name: "Server B",
85+ url: "https://b.example.com",
86+ username: "user2",
87+ password: "pass2"
88+ )
89+ ConnectionStore.saveConnections([conn1, conn2])
90+
91+ let loaded = ConnectionStore.loadConnections()
92+ XCTAssertEqual(loaded.count, 2)
93+
94+ let loadedA = loaded.first { $0.id == conn1.id }
95+ let loadedB = loaded.first { $0.id == conn2.id }
96+ XCTAssertEqual(loadedA?.name, "Server A")
97+ XCTAssertEqual(loadedA?.password, "pass1")
98+ XCTAssertEqual(loadedB?.name, "Server B")
99+ XCTAssertEqual(loadedB?.password, "pass2")
100+
101+ // Clean up
102+ KeychainService.deletePassword(for: conn1.id)
103+ KeychainService.deletePassword(for: conn2.id)
104+ }
105+
106+ func testPasswordNotInKeychainConnectionData() throws {
107+ try XCTSkipUnless(Self.keychainAvailable, "Data-protection keychain unavailable")
34108 let connection = ServerConnection(
35109 name: "Test",
36110 url: "https://example.com",