Refine store
This commit is contained in:
1 parent
c414693eb9
commit
b40c71eec6
17 files changed
+222
-23
No files matched your search
@@ -110,6 +110,42 @@ struct SettingsPortTests {
|
||||
#expect(defaults.bool(forKey: SettingsKey.settingsSyncEnabled))
|
||||
}
|
||||
|
||||
@Test func newVaultCredentialStorageRequiresBothSwitchesAndProAccess() throws {
|
||||
let domain = "VaultCredentialStorage.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: domain))
|
||||
defer { defaults.removePersistentDomain(forName: domain) }
|
||||
for iCloudEnabled in [false, true] {
|
||||
for vaultEnabled in [false, true] {
|
||||
for canUseVaultSync in [false, true] {
|
||||
defaults.set(iCloudEnabled, forKey: SettingsKey.iCloudSyncEnabled)
|
||||
defaults.set(vaultEnabled, forKey: SettingsKey.vaultSyncEnabled)
|
||||
let expectedKey: KeyStorage =
|
||||
iCloudEnabled && vaultEnabled && canUseVaultSync ? .iCloud : .thisDevice
|
||||
let expectedPassword: PasswordStorage =
|
||||
iCloudEnabled && vaultEnabled && canUseVaultSync ? .iCloud : .thisDevice
|
||||
|
||||
#expect(
|
||||
NewVaultCredentialStorage.keyStorage(
|
||||
defaults: defaults,
|
||||
canUseVaultSync: canUseVaultSync) == expectedKey)
|
||||
#expect(
|
||||
NewVaultCredentialStorage.passwordStorage(
|
||||
defaults: defaults,
|
||||
canUseVaultSync: canUseVaultSync) == expectedPassword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func newVaultCredentialStorageUsesEnabledDefaultsWhenPreferencesAreMissing() throws {
|
||||
let domain = "VaultCredentialStorageDefaults.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: domain))
|
||||
defer { defaults.removePersistentDomain(forName: domain) }
|
||||
|
||||
#expect(NewVaultCredentialStorage.keyStorage(defaults: defaults, canUseVaultSync: true) == .iCloud)
|
||||
#expect(NewVaultCredentialStorage.passwordStorage(defaults: defaults, canUseVaultSync: true) == .iCloud)
|
||||
}
|
||||
|
||||
@Test func rolesTakePrecedenceOverLastSelectedModelAndDeletedModelsFallBack() {
|
||||
let provider = ModelProvider(name: "Test", models: [AIModel(id: "role"), AIModel(id: "last")])
|
||||
let role = AIModelRef(modelProviderID: provider.id, modelID: "role")
|
||||
|
||||
@@ -7,7 +7,7 @@ import struct TermindDomain.Host
|
||||
public final class SSHImportModel {
|
||||
public var items: [SSHImportItem] = []
|
||||
public var expanded = Set(SSHImportCategory.allCases)
|
||||
public var storage: PasswordStorage = .iCloud
|
||||
public var storage: PasswordStorage
|
||||
public var importColor: TagColor = .none
|
||||
public var hostGroupID: HostGroup.ID?
|
||||
public var hostTags: [String] = []
|
||||
@@ -36,10 +36,16 @@ public final class SSHImportModel {
|
||||
var keyID: UUID?
|
||||
}
|
||||
|
||||
public init(library: HostLibraryModel, reader: any SSHImportReading, defaultUsername: String = "") {
|
||||
public init(
|
||||
library: HostLibraryModel,
|
||||
reader: any SSHImportReading,
|
||||
defaultUsername: String = "",
|
||||
storage: PasswordStorage = .iCloud
|
||||
) {
|
||||
self.defaultUsername = defaultUsername
|
||||
self.library = library
|
||||
self.reader = reader
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
public func scan(_ url: URL, knownHostsOnly: Bool) async {
|
||||
|
||||
@@ -68,3 +68,34 @@ public nonisolated enum SettingsKey {
|
||||
if !vault && !ai { defaults.set(true, forKey: vaultSyncEnabled) }
|
||||
}
|
||||
}
|
||||
|
||||
public nonisolated enum NewVaultCredentialStorage {
|
||||
public static func keyStorage(
|
||||
defaults: UserDefaults = .standard,
|
||||
canUseVaultSync: Bool
|
||||
) -> KeyStorage {
|
||||
vaultSyncIsAvailable(defaults: defaults, canUseVaultSync: canUseVaultSync)
|
||||
? .iCloud : .thisDevice
|
||||
}
|
||||
|
||||
public static func passwordStorage(
|
||||
defaults: UserDefaults = .standard,
|
||||
canUseVaultSync: Bool
|
||||
) -> PasswordStorage {
|
||||
vaultSyncIsAvailable(defaults: defaults, canUseVaultSync: canUseVaultSync)
|
||||
? .iCloud : .thisDevice
|
||||
}
|
||||
|
||||
private static func vaultSyncIsAvailable(
|
||||
defaults: UserDefaults,
|
||||
canUseVaultSync: Bool
|
||||
) -> Bool {
|
||||
let iCloudEnabled =
|
||||
defaults.object(forKey: SettingsKey.iCloudSyncEnabled) as? Bool
|
||||
?? SettingsKey.defaultICloudSyncEnabled
|
||||
let vaultEnabled =
|
||||
defaults.object(forKey: SettingsKey.vaultSyncEnabled) as? Bool
|
||||
?? SettingsKey.defaultVaultSyncEnabled
|
||||
return iCloudEnabled && vaultEnabled && canUseVaultSync
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,8 @@ sibling working tree is not the app dependency. Follow `AGENTS.md` for package r
|
||||
Keychain metadata without reading secret data; the legacy software-Key JSON envelope is parsed strictly and fails closed.
|
||||
- Saved iCloud Passwords and software Keys cannot be moved to device-only storage: editors disable that destination and
|
||||
application writes reject it before touching Keychain. New credentials may start device-only, and device-only credentials
|
||||
may move to iCloud.
|
||||
may move to iCloud. New Key and Identity storage pickers default to iCloud only while the master iCloud switch, Vault
|
||||
selection and Pro access are all enabled; otherwise they default to this device. Existing credentials keep their storage.
|
||||
- Editors whose saved Password or software Private Key is unavailable on the current device preserve its reference and
|
||||
disable secret replacement, removal, passphrase and storage changes while still allowing public metadata edits. Key
|
||||
metadata-only saves may update the SSH certificate after validating it against the saved public key, without reading or
|
||||
|
||||
@@ -68,6 +68,7 @@ final class MobileAppModel {
|
||||
let liveActivities: MobileLiveActivityController
|
||||
@ObservationIgnored private var workspaceActivitySessions: [WorkspaceActivityAttributes.Session] = []
|
||||
private let defaults: UserDefaults?
|
||||
private let settingsDefaults: UserDefaults
|
||||
private let snippetTimestampFallback: Date
|
||||
private let background: any MobileBackgroundTaskManaging
|
||||
private let workspaceConnectionServices: MobileWorkspaceConnectionServices
|
||||
@@ -139,7 +140,10 @@ final class MobileAppModel {
|
||||
accessGroup: Bundle.main.object(forInfoDictionaryKey: "TermindKeychainAccessGroup") as? String
|
||||
?? "F6SUNDBBN9.com.akinokaede.termind")
|
||||
let settingsDefaults =
|
||||
memoryOnly ? UserDefaults(suiteName: "termind.tests.\(UUID().uuidString)")! : defaults ?? .standard
|
||||
memoryOnly
|
||||
? defaults ?? UserDefaults(suiteName: "termind.tests.\(UUID().uuidString)")!
|
||||
: defaults ?? .standard
|
||||
self.settingsDefaults = settingsDefaults
|
||||
SettingsKey.migrateSyncPreferences(in: settingsDefaults)
|
||||
let settingsPersistence = SettingsSyncPersistence(context: ModelContext(container), defaults: settingsDefaults)
|
||||
try settingsPersistence.prepare()
|
||||
@@ -264,6 +268,18 @@ final class MobileAppModel {
|
||||
loadRecentWorkspaces()
|
||||
}
|
||||
|
||||
var newVaultKeyStorage: KeyStorage {
|
||||
NewVaultCredentialStorage.keyStorage(
|
||||
defaults: settingsDefaults,
|
||||
canUseVaultSync: pro.canUse(.vaultSync))
|
||||
}
|
||||
|
||||
var newVaultPasswordStorage: PasswordStorage {
|
||||
NewVaultCredentialStorage.passwordStorage(
|
||||
defaults: settingsDefaults,
|
||||
canUseVaultSync: pro.canUse(.vaultSync))
|
||||
}
|
||||
|
||||
func open(
|
||||
hostID: UUID, kind: ConnectionHistoryEntry.Kind,
|
||||
initialPath: String? = nil
|
||||
|
||||
@@ -39,12 +39,17 @@ final class MobileKeyEditorModel: Identifiable {
|
||||
private var originalPrivateKey = ""
|
||||
private var didLoadPrivateKey: Bool
|
||||
|
||||
init(mode: Mode, credentials: CredentialLibraryModel, key: SSHKeyRef? = nil) {
|
||||
init(
|
||||
mode: Mode,
|
||||
credentials: CredentialLibraryModel,
|
||||
key: SSHKeyRef? = nil,
|
||||
defaultStorage: KeyStorage = .iCloud
|
||||
) {
|
||||
self.mode = mode
|
||||
self.credentials = credentials
|
||||
original = key
|
||||
label = key?.label ?? ""
|
||||
storage = key?.storage ?? .iCloud
|
||||
storage = key?.storage ?? defaultStorage
|
||||
certificate = key?.certificate ?? ""
|
||||
colorTag = key?.colorTag ?? .none
|
||||
let availability = key.map { credentials.keyAvailability(for: $0) }
|
||||
|
||||
@@ -128,13 +128,19 @@ struct MobileHostEditor: View {
|
||||
switch route {
|
||||
case .hostIdentity:
|
||||
MobileIdentityEditor(
|
||||
app: app, identity: Identity(name: "", username: "")
|
||||
app: app,
|
||||
identity: Identity(
|
||||
name: "", username: "",
|
||||
passwordStorage: app.newVaultPasswordStorage)
|
||||
) { identity in
|
||||
draft = MobileHostEditorPolicy.selectIdentity(identity, current: draft)
|
||||
}
|
||||
case .proxyIdentity:
|
||||
MobileIdentityEditor(
|
||||
app: app, identity: Identity(name: "", username: ""),
|
||||
app: app,
|
||||
identity: Identity(
|
||||
name: "", username: "",
|
||||
passwordStorage: app.newVaultPasswordStorage),
|
||||
configuration: .proxy
|
||||
) { identity in
|
||||
draft.proxy = MobileHostEditorPolicy.selectProxyIdentity(identity, current: draft.proxy)
|
||||
|
||||
@@ -76,14 +76,20 @@ struct MobileCredentialsView: View {
|
||||
.toolbar {
|
||||
Menu {
|
||||
Button("Generate Key", systemImage: "key") {
|
||||
keyEditor = MobileKeyEditorModel(mode: .generate, credentials: app.credentials)
|
||||
keyEditor = MobileKeyEditorModel(
|
||||
mode: .generate, credentials: app.credentials,
|
||||
defaultStorage: app.newVaultKeyStorage)
|
||||
}
|
||||
Button("Import Key", systemImage: "square.and.arrow.down") {
|
||||
keyEditor = MobileKeyEditorModel(mode: .importKey, credentials: app.credentials)
|
||||
keyEditor = MobileKeyEditorModel(
|
||||
mode: .importKey, credentials: app.credentials,
|
||||
defaultStorage: app.newVaultKeyStorage)
|
||||
}
|
||||
Divider()
|
||||
Button("New Identity", systemImage: "person.crop.circle.badge.plus") {
|
||||
editing = Identity(name: "", username: "")
|
||||
editing = Identity(
|
||||
name: "", username: "",
|
||||
passwordStorage: app.newVaultPasswordStorage)
|
||||
}
|
||||
} label: {
|
||||
Label("Add", systemImage: "plus").labelStyle(.iconOnly)
|
||||
|
||||
@@ -10,6 +10,26 @@ import Testing
|
||||
|
||||
@Suite @MainActor
|
||||
struct MobileVaultFeatureTests {
|
||||
@Test func appDefaultsNewCredentialsToThisDeviceWhenVaultSyncIsOff() throws {
|
||||
let domain = "MobileVaultCredentialDefaults.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: domain))
|
||||
defer { defaults.removePersistentDomain(forName: domain) }
|
||||
defaults.set(false, forKey: SettingsKey.iCloudSyncEnabled)
|
||||
let app = try MobileAppModel(inMemory: true, defaults: defaults)
|
||||
defer { app.outbox.stop() }
|
||||
|
||||
#expect(app.newVaultKeyStorage == .thisDevice)
|
||||
#expect(app.newVaultPasswordStorage == .thisDevice)
|
||||
let generator = MobileKeyEditorModel(
|
||||
mode: .generate,
|
||||
credentials: app.credentials,
|
||||
defaultStorage: app.newVaultKeyStorage)
|
||||
#expect(generator.storage == .thisDevice)
|
||||
|
||||
generator.storage = .iCloud
|
||||
#expect(generator.storage == .iCloud)
|
||||
}
|
||||
|
||||
@Test func hostMenuSeparatesConnectionEditingAndDeletionActions() {
|
||||
#expect(
|
||||
MobileHostMenuPolicy.sections == [
|
||||
|
||||
@@ -20,6 +20,7 @@ struct AppDependencies {
|
||||
let metricsPreferences: (any MetricsConfigurationStoring)?
|
||||
let mcpServerSecrets: any SecretStoring
|
||||
let logging: any ApplicationLogManaging
|
||||
let settingsDefaults: UserDefaults
|
||||
|
||||
init(
|
||||
hostRepository: any HostRepository, outboxRunner: OutboxRunner?,
|
||||
@@ -31,7 +32,8 @@ struct AppDependencies {
|
||||
metricsSync: MetricsSyncCoordinator? = nil,
|
||||
metricsPreferences: (any MetricsConfigurationStoring)? = nil,
|
||||
mcpServerSecrets: any SecretStoring = InMemorySecretStore(),
|
||||
logging: any ApplicationLogManaging = DisabledApplicationLogManager.shared
|
||||
logging: any ApplicationLogManaging = DisabledApplicationLogManager.shared,
|
||||
settingsDefaults: UserDefaults = .standard
|
||||
) {
|
||||
self.hostRepository = hostRepository
|
||||
self.outboxRunner = outboxRunner
|
||||
@@ -44,14 +46,18 @@ struct AppDependencies {
|
||||
self.metricsPreferences = metricsPreferences
|
||||
self.mcpServerSecrets = mcpServerSecrets
|
||||
self.logging = logging
|
||||
self.settingsDefaults = settingsDefaults
|
||||
}
|
||||
|
||||
static func inMemory(hosts: [Host], groups: [HostGroup]) -> Self {
|
||||
Self(
|
||||
let defaults = UserDefaults(suiteName: "termind.tests.\(UUID().uuidString)")!
|
||||
SettingsKey.migrateSyncPreferences(in: defaults)
|
||||
return Self(
|
||||
hostRepository: InMemoryHostRepository(
|
||||
snapshot: HostLibrarySnapshot(hosts: hosts, groups: groups)
|
||||
),
|
||||
outboxRunner: nil
|
||||
outboxRunner: nil,
|
||||
settingsDefaults: defaults
|
||||
)
|
||||
}
|
||||
}
|
||||
+17
-1
@@ -109,13 +109,28 @@ final class AppModel {
|
||||
/// and a run ending is not a reason to stop a deploy.
|
||||
private let agentBackgroundCommands: AgentBackgroundCommands
|
||||
private let outboxRunner: OutboxRunner?
|
||||
private let settingsDefaults: UserDefaults
|
||||
private var windows: [ObjectIdentifier: WeakWindow] = [:]
|
||||
private var deletedAgentConversationIDs = Set<UUID>()
|
||||
/// Conversations already handed to the summarizer. Titling is once per
|
||||
/// conversation, and every run re-arms the hook that would do it.
|
||||
|
||||
func makeSSHImportModel() -> SSHImportModel {
|
||||
SSHImportModel(library: hosts, reader: SSHImportReader())
|
||||
SSHImportModel(
|
||||
library: hosts, reader: SSHImportReader(),
|
||||
storage: newVaultPasswordStorage)
|
||||
}
|
||||
|
||||
var newVaultKeyStorage: KeyStorage {
|
||||
NewVaultCredentialStorage.keyStorage(
|
||||
defaults: settingsDefaults,
|
||||
canUseVaultSync: proAccess.canUse(.vaultSync))
|
||||
}
|
||||
|
||||
var newVaultPasswordStorage: PasswordStorage {
|
||||
NewVaultCredentialStorage.passwordStorage(
|
||||
defaults: settingsDefaults,
|
||||
canUseVaultSync: proAccess.canUse(.vaultSync))
|
||||
}
|
||||
|
||||
init(
|
||||
@@ -139,6 +154,7 @@ final class AppModel {
|
||||
) {
|
||||
self.windowSizeStore = windowSizeStore
|
||||
let dependencies = dependencies ?? .inMemory(hosts: hosts.hosts, groups: hosts.groups)
|
||||
settingsDefaults = dependencies.settingsDefaults
|
||||
logging = dependencies.logging
|
||||
let externalAuthentication = ExternalAuthCoordinator(
|
||||
logger: logging.makeLogger(label: "termind.external-mcp"))
|
||||
|
||||
@@ -124,7 +124,8 @@ struct TermindApp: App {
|
||||
vaultSync: vaultSync, settingsSync: settingsSync,
|
||||
metricsSync: metricsSync, metricsPreferences: metricsPersistence,
|
||||
mcpServerSecrets: secrets,
|
||||
logging: logging
|
||||
logging: logging,
|
||||
settingsDefaults: .standard
|
||||
),
|
||||
proAccess: proAccess,
|
||||
appReview: .live(),
|
||||
|
||||
@@ -233,7 +233,10 @@ struct HostEditorView: View {
|
||||
private func destination(_ route: Route) -> some View {
|
||||
switch route {
|
||||
case .identity:
|
||||
NewIdentitySheet(back: closeNestedEditor) { identity, passwordUpdate in
|
||||
NewIdentitySheet(
|
||||
defaultPasswordStorage: app.newVaultPasswordStorage,
|
||||
back: closeNestedEditor
|
||||
) { identity, passwordUpdate in
|
||||
try app.hostLibrary.upsert(identity, passwordUpdate: passwordUpdate)
|
||||
draft.identityID = identity.id
|
||||
}
|
||||
@@ -243,7 +246,11 @@ struct HostEditorView: View {
|
||||
draft.groupID = group.id
|
||||
}
|
||||
case .proxyIdentity:
|
||||
NewIdentitySheet(configuration: .proxy, back: closeNestedEditor) {
|
||||
NewIdentitySheet(
|
||||
configuration: .proxy,
|
||||
defaultPasswordStorage: app.newVaultPasswordStorage,
|
||||
back: closeNestedEditor
|
||||
) {
|
||||
identity, passwordUpdate in
|
||||
try app.hostLibrary.upsert(identity, passwordUpdate: passwordUpdate)
|
||||
draft.proxy?.identityID = identity.id
|
||||
|
||||
@@ -324,7 +324,9 @@ struct KeychainImportDetail: View {
|
||||
var body: some View {
|
||||
SoftwareKeyEditor(
|
||||
title: "Import Key", symbol: "square.and.arrow.down",
|
||||
close: window.closeVaultWork, save: save
|
||||
close: window.closeVaultWork,
|
||||
defaultStorage: app.newVaultKeyStorage,
|
||||
save: save
|
||||
)
|
||||
}
|
||||
|
||||
@@ -390,6 +392,7 @@ private struct SoftwareKeyEditor: View {
|
||||
init(
|
||||
existing: SSHKeyRef? = nil, title: LocalizedStringKey,
|
||||
symbol: String, close: @escaping () -> Void,
|
||||
defaultStorage: KeyStorage = .iCloud,
|
||||
loadPrivateKey: (() throws -> String)? = nil,
|
||||
saveMetadata: ((_ name: String, _ colorTag: TagColor, _ certificate: String) throws -> SSHKeyRef)? = nil,
|
||||
save:
|
||||
@@ -406,7 +409,7 @@ private struct SoftwareKeyEditor: View {
|
||||
self.save = save
|
||||
_name = State(initialValue: existing?.label ?? "")
|
||||
_colorTag = State(initialValue: existing?.colorTag ?? .none)
|
||||
_storage = State(initialValue: existing?.storage ?? .iCloud)
|
||||
_storage = State(initialValue: existing?.storage ?? defaultStorage)
|
||||
_certificate = State(initialValue: existing?.certificate ?? "")
|
||||
_isLoading = State(initialValue: loadPrivateKey != nil)
|
||||
}
|
||||
@@ -759,7 +762,10 @@ struct KeychainGenerateDetail: View {
|
||||
@Environment(WindowModel.self) private var window
|
||||
|
||||
var body: some View {
|
||||
GenerateKeySheet(generate: generate, close: window.closeVaultWork)
|
||||
GenerateKeySheet(
|
||||
defaultStorage: app.newVaultKeyStorage,
|
||||
generate: generate,
|
||||
close: window.closeVaultWork)
|
||||
}
|
||||
|
||||
private func generate(
|
||||
@@ -828,7 +834,7 @@ private struct GenerateKeySheet: View {
|
||||
|
||||
@State private var label = ""
|
||||
@State private var colorTag = TagColor.none
|
||||
@State private var storage = KeyStorage.iCloud
|
||||
@State private var storage: KeyStorage
|
||||
@State private var kind = Kind.ed25519
|
||||
@State private var curveBits = 256
|
||||
@State private var rsaBits = 3072
|
||||
@@ -846,6 +852,20 @@ private struct GenerateKeySheet: View {
|
||||
.secureEnclave,
|
||||
]
|
||||
|
||||
init(
|
||||
defaultStorage: KeyStorage = .iCloud,
|
||||
generate:
|
||||
@escaping (
|
||||
String, TagColor, SSHKeyMaterial.Generation, String?, SSHKeyMaterial.Cipher, Int,
|
||||
KeyStorage, Bool
|
||||
) async throws -> SSHKeyRef,
|
||||
close: @escaping () -> Void
|
||||
) {
|
||||
self.generate = generate
|
||||
self.close = close
|
||||
_storage = State(initialValue: defaultStorage)
|
||||
}
|
||||
|
||||
private var generation: SSHKeyMaterial.Generation {
|
||||
if storage == .secureEnclave { return .ecdsa(bits: 256) }
|
||||
return switch kind {
|
||||
|
||||
@@ -75,6 +75,7 @@ struct NewIdentitySheet: View {
|
||||
init(
|
||||
identity: Identity? = nil,
|
||||
configuration: IdentityEditorConfiguration = .standard,
|
||||
defaultPasswordStorage: PasswordStorage = .iCloud,
|
||||
back: (() -> Void)? = nil,
|
||||
create: @escaping (Identity, IdentityPasswordUpdate) throws -> Void,
|
||||
close: (() -> Void)? = nil
|
||||
@@ -86,7 +87,7 @@ struct NewIdentitySheet: View {
|
||||
requestedBack = back
|
||||
_name = State(initialValue: identity?.name ?? "")
|
||||
_username = State(initialValue: identity?.username ?? "")
|
||||
_passwordStorage = State(initialValue: identity?.passwordStorage ?? .iCloud)
|
||||
_passwordStorage = State(initialValue: identity?.passwordStorage ?? defaultPasswordStorage)
|
||||
_keyID = State(initialValue: identity?.keyID)
|
||||
_colorTag = State(initialValue: identity?.colorTag ?? .none)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ struct VaultWorkDetailView: View {
|
||||
case .identityEditor(let id):
|
||||
NewIdentitySheet(
|
||||
identity: id.flatMap(app.hostLibrary.identity),
|
||||
defaultPasswordStorage: app.newVaultPasswordStorage,
|
||||
create: { identity, password in
|
||||
try app.hostLibrary.upsert(identity, passwordUpdate: password)
|
||||
window.selectVaultItem(.identity(identity.id), on: .keychain)
|
||||
|
||||
@@ -27,6 +27,26 @@ struct SSHImportTests {
|
||||
return "|1|\(salt.base64EncodedString())|\(Data(digest).base64EncodedString())"
|
||||
}
|
||||
|
||||
@Test
|
||||
func appDefaultsNewCredentialsAndSSHImportToThisMacWhenVaultSyncIsOff() throws {
|
||||
let domain = "SSHImportDefaults.\(UUID())"
|
||||
let defaults = try #require(UserDefaults(suiteName: domain))
|
||||
defer { defaults.removePersistentDomain(forName: domain) }
|
||||
defaults.set(false, forKey: SettingsKey.vaultSyncEnabled)
|
||||
let hosts = HostLibraryModel()
|
||||
let app = AppModel(
|
||||
hosts: hosts,
|
||||
dependencies: AppDependencies(
|
||||
hostRepository: InMemoryHostRepository(),
|
||||
outboxRunner: nil,
|
||||
settingsDefaults: defaults))
|
||||
defer { app.shutdownForTermination() }
|
||||
|
||||
#expect(app.newVaultKeyStorage == .thisDevice)
|
||||
#expect(app.newVaultPasswordStorage == .thisDevice)
|
||||
#expect(app.makeSSHImportModel().storage == .thisDevice)
|
||||
}
|
||||
|
||||
@Test
|
||||
func hashedKeysMatchPortsAndSurvivePersistenceWithoutHosts() throws {
|
||||
let entry = KnownHostEntry(hostname: hash("[2001:db8::1]:2222"), fingerprint: "SHA256:key")
|
||||
|
||||
Reference in new issue
Block a user