cmux/cmuxTests/SocketControlPasswordStoreTests.swift
Lawrence Chen 163f8572e4
Replace keychain password storage with file-based storage (#576)
Moves socket control password from the macOS login keychain to a
plain file at ~/Library/Application Support/cmux/socket-control-password.
This eliminates the system keychain prompt that interrupts users on
first launch or after keychain changes.

- Directory created with 0700, file written with 0600 permissions
- One-time migration copies existing keychain password to the file,
  deletes the keychain entry, and records a migration version in
  UserDefaults so it runs only once
- CLI SocketPasswordResolver also reads from the file path
- Security framework import is now conditional (#if canImport)
- Adds SocketControlPasswordStoreTests covering round-trip, env
  priority, path resolution, and migration behavior

Fixes https://github.com/manaflow-ai/cmux/issues/541
2026-02-26 14:29:12 -08:00

111 lines
4.9 KiB
Swift

import XCTest
#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif
final class SocketControlPasswordStoreTests: XCTestCase {
func testSaveLoadAndClearRoundTripUsesFileStorage() throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-socket-password-tests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let fileURL = tempDir.appendingPathComponent("socket-password.txt", isDirectory: false)
XCTAssertFalse(SocketControlPasswordStore.hasConfiguredPassword(environment: [:], fileURL: fileURL))
try SocketControlPasswordStore.savePassword("hunter2", fileURL: fileURL)
XCTAssertEqual(try SocketControlPasswordStore.loadPassword(fileURL: fileURL), "hunter2")
XCTAssertTrue(SocketControlPasswordStore.hasConfiguredPassword(environment: [:], fileURL: fileURL))
try SocketControlPasswordStore.clearPassword(fileURL: fileURL)
XCTAssertNil(try SocketControlPasswordStore.loadPassword(fileURL: fileURL))
XCTAssertFalse(SocketControlPasswordStore.hasConfiguredPassword(environment: [:], fileURL: fileURL))
}
func testConfiguredPasswordPrefersEnvironmentOverStoredFile() throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-socket-password-tests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let fileURL = tempDir.appendingPathComponent("socket-password.txt", isDirectory: false)
try SocketControlPasswordStore.savePassword("stored-secret", fileURL: fileURL)
let environment = [SocketControlSettings.socketPasswordEnvKey: "env-secret"]
let configured = SocketControlPasswordStore.configuredPassword(
environment: environment,
fileURL: fileURL
)
XCTAssertEqual(configured, "env-secret")
}
func testDefaultPasswordFileURLUsesCmuxAppSupportPath() throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-socket-password-tests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let resolved = SocketControlPasswordStore.defaultPasswordFileURL(appSupportDirectory: tempDir)
XCTAssertEqual(
resolved?.path,
tempDir.appendingPathComponent("cmux", isDirectory: true)
.appendingPathComponent("socket-control-password", isDirectory: false).path
)
}
func testLegacyKeychainMigrationCopiesPasswordDeletesLegacyAndRunsOnlyOnce() throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-socket-password-tests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let fileURL = tempDir.appendingPathComponent("socket-password.txt", isDirectory: false)
let defaultsSuiteName = "cmux-socket-password-migration-tests-\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: defaultsSuiteName) else {
XCTFail("Expected isolated UserDefaults suite for migration test")
return
}
defer { defaults.removePersistentDomain(forName: defaultsSuiteName) }
var lookupCount = 0
var deleteCount = 0
SocketControlPasswordStore.migrateLegacyKeychainPasswordIfNeeded(
defaults: defaults,
fileURL: fileURL,
loadLegacyPassword: {
lookupCount += 1
return "legacy-secret"
},
deleteLegacyPassword: {
deleteCount += 1
return true
}
)
XCTAssertEqual(try SocketControlPasswordStore.loadPassword(fileURL: fileURL), "legacy-secret")
XCTAssertEqual(lookupCount, 1)
XCTAssertEqual(deleteCount, 1)
SocketControlPasswordStore.migrateLegacyKeychainPasswordIfNeeded(
defaults: defaults,
fileURL: fileURL,
loadLegacyPassword: {
lookupCount += 1
return "new-value"
},
deleteLegacyPassword: {
deleteCount += 1
return true
}
)
XCTAssertEqual(lookupCount, 1)
XCTAssertEqual(deleteCount, 1)
XCTAssertEqual(try SocketControlPasswordStore.loadPassword(fileURL: fileURL), "legacy-secret")
}
}