Files
Termind/TFMTests/SSHImportTests.swift

582 lines
28 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import CryptoKit
import Foundation
import TermindApplication
import TermindDomain
import TermindInfrastructure
import TermindPersistence
import TermindPresentation
import TermindRemoteInfrastructure
import Testing
import struct TermindDomain.Host
@testable import Termind
@Suite
@MainActor
struct SSHImportTests {
private func root() throws -> URL {
let url = FileManager.default.temporaryDirectory.appendingPathComponent("SSHImport-\(UUID())")
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
return url
}
private func hash(_ address: String) -> String {
let salt = Data(repeating: 7, count: 20)
let digest = HMAC<Insecure.SHA1>.authenticationCode(for: Data(address.utf8), using: SymmetricKey(data: salt))
return "|1|\(salt.base64EncodedString())|\(Data(digest).base64EncodedString())"
}
@Test func overQuotaImportRetainsSelectionAndWritesNothingUntilEntitlementResolves() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
try material.privateKey.write(to: url.appendingPathComponent("id_ed25519"), atomically: true, encoding: .utf8)
try """
Host prod stage
HostName %h.example
User deploy
IdentityFile ~/.ssh/id_ed25519
""".write(to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8)
try "prod.example \(material.publicKey)".write(
to: url.appendingPathComponent("known_hosts"), atomically: true, encoding: .utf8)
let hosts = (0..<3).map { Host(name: "Existing \($0)", host: "existing\($0).invalid") }
let library = HostLibraryModel(hosts: hosts)
library.hostAccess.entitlement = .loading
var upgrades = 0
library.requestHostUpgrade = { upgrades += 1 }
library.resolveInitialHostEntitlement = { library.hostAccess.entitlement = .free }
defer { library.resolveInitialHostEntitlement = nil }
let model = SSHImportModel(library: library, reader: SSHImportReader())
await model.scan(url, knownHostsOnly: false)
let selected = model.selectedItems().map(\.id)
#expect(model.canImport)
await model.commit()
#expect(!model.completed)
#expect(model.error == nil)
#expect(upgrades == 1)
#expect(model.selectedItems().map(\.id) == selected)
#expect(library.hosts == hosts)
#expect(library.identities.isEmpty && library.keys.isEmpty && library.knownHosts.isEmpty)
library.hostAccess.entitlement = .purchased(.purchased)
await model.commit()
#expect(model.completed)
#expect(library.hosts.count == 5)
#expect(library.keys.count == 1 && library.knownHosts.count == 1)
}
@Test
func appDefaultsNewCredentialsAndSSHImportToThisMacWhenICloudSyncIsOff() throws {
let domain = "SSHImportDefaults.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: domain))
defer { defaults.removePersistentDomain(forName: domain) }
defaults.set(false, forKey: SettingsKey.iCloudSyncEnabled)
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")
let decoded = try JSONDecoder().decode(KnownHostEntry.self, from: JSONEncoder().encode(entry))
#expect(decoded.matches(hostname: "2001:db8::1", port: 2222))
#expect(!decoded.matches(hostname: "2001:db8::1", port: 22))
let store = HostLibraryModel(knownHosts: [decoded])
let host = Host(name: "IPv6", host: "2001:db8::1", port: 2222)
#expect(try store.expectedHostKeys(for: host) == [entry.identity])
store.forgetKnownHosts(for: host)
#expect(store.knownHosts.isEmpty)
}
@Test
func configHonorsFirstValuesHostPatternsAndOrderedIncludes() {
let root = URL(fileURLWithPath: "/fixture/.ssh")
var parser = SSHConfigParser(
root: root, home: URL(fileURLWithPath: "/fixture"), username: "local",
files: [
"/fixture/.ssh/config": """
Host prod alias
HostName prod.example
User deploy
IdentityFile "~/.ssh/deploy key"
Include conf/*.conf
Host * !alias
Port 2222
Host *
User fallback
""",
"/fixture/.ssh/conf/01.conf": "Port=2200\nIdentityFile ~/.ssh/second",
"/fixture/.ssh/conf/02.conf": "Port 2300",
])
let entries = parser.parse().compactMap(\.host)
#expect(entries.count == 2)
#expect(entries.allSatisfy { $0.port == 2200 && $0.username == "deploy" })
#expect(entries[0].identityPaths == ["/fixture/.ssh/deploy key", "/fixture/.ssh/second"])
}
@Test
func configReportsCyclesAndDoesNotExecuteMatchOrProxyCommands() {
let root = URL(fileURLWithPath: "/fixture")
var parser = SSHConfigParser(
root: root, home: root, username: "local",
files: [
"/fixture/config":
"Host app\nHostName app.example\nProxyCommand touch /tmp/never\nInclude loop\nMatch exec \"exit 0\"\nUser wrong",
"/fixture/loop": "Include config",
])
let entries = parser.parse()
let host = entries.first { $0.host != nil }
#expect(host?.host?.username == "local")
#expect(host?.warnings.contains(where: { $0.contains("proxycommand") }) == true)
#expect(host?.warnings.contains(where: { $0.contains("Match") }) == true)
#expect(entries.contains { $0.unavailable })
}
@Test
func knownHostsValidatesWireKeysAndPreservesMultipleEndpoints() throws {
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
let content = """
# comment
example,[2001:db8::1]:2222 \(material.publicKey)
\(hash("other.example")) \(material.publicKey)
@revoked revoked.example \(material.publicKey)
*.wild.example \(material.publicKey)
broken ssh-ed25519 AAAA
"""
let rows = SSHImportParser.knownHosts(content, source: "known_hosts")
#expect(rows.filter { !$0.unavailable }.count == 3)
#expect(rows.compactMap(\.knownHost).allSatisfy { $0.fingerprint == material.fingerprint })
#expect(rows.filter(\.unavailable).count == 3)
#expect(rows.compactMap(\.knownHost).contains { $0.matches(hostname: "other.example", port: 22) })
}
@Test
func contentClassifierDoesNotUseFilenames() throws {
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
#expect(SSHImportParser.contentKind(material.privateKey) == .privateKey)
#expect(SSHImportParser.contentKind("example.invalid \(material.publicKey)") == .knownHosts)
#expect(SSHImportParser.contentKind("Host app\n HostName app.invalid") == .configuration)
#expect(SSHImportParser.contentKind("ordinary notes") == .unknown)
}
@Test
func folderRecursivelyFindsRenamedContentAndDoesNotReparseIncludes() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let pieces = url.appendingPathComponent("pieces", isDirectory: true)
let secrets = url.appendingPathComponent("secrets", isDirectory: true)
try FileManager.default.createDirectory(at: pieces, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: secrets, withIntermediateDirectories: true)
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
try "Include pieces/*".write(
to: url.appendingPathComponent("entry.anything"), atomically: true, encoding: .utf8)
try "Host app\n HostName app.invalid\n User deploy\n IdentityFile secrets/credential.data".write(
to: pieces.appendingPathComponent("fragment.notes"), atomically: true, encoding: .utf8)
try material.privateKey.write(
to: secrets.appendingPathComponent("credential.data"), atomically: true, encoding: .utf8)
try "app.invalid \(material.publicKey)".write(
to: pieces.appendingPathComponent("trust.random"), atomically: true, encoding: .utf8)
let rows = try await SSHImportReader().scan(root: url, knownHostsOnly: false)
#expect(rows.compactMap(\.host).map(\.alias) == ["app"])
#expect(rows.compactMap(\.key).count == 1)
#expect(rows.compactMap(\.knownHost).count == 1)
}
@Test
func standaloneKnownHostsRejectsOtherSSHContentButAcceptsComments() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let file = url.appendingPathComponent("arbitrary-name")
let reader = SSHImportReader()
try "Host app\nHostName app.invalid".write(to: file, atomically: true, encoding: .utf8)
await #expect(throws: SSHImportSourceError.unsupportedKnownHostsContent) {
_ = try await reader.scan(root: file, knownHostsOnly: true)
}
try "# intentionally empty\n".write(to: file, atomically: true, encoding: .utf8)
#expect(try await reader.scan(root: file, knownHostsOnly: true).isEmpty)
}
@Test
func privateKeyIsRereadAndDigestCheckedAtCommitBoundary() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let first = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
let second = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
let file = url.appendingPathComponent("credential")
try first.privateKey.write(to: file, atomically: true, encoding: .utf8)
let reader = SSHImportReader()
let rows = try await reader.scan(root: url, knownHostsOnly: false)
let id = try #require(rows.compactMap(\.key).first?.id)
try second.privateKey.write(to: file, atomically: true, encoding: .utf8)
await #expect(throws: SSHImportSourceError.sourceChanged) {
_ = try await reader.credentials(for: [id])
}
}
@Test
func droppedSourcesUseTheSameTypeValidationAsTheFilePicker() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let file = url.appendingPathComponent("known_hosts")
try "# empty".write(to: file, atomically: true, encoding: .utf8)
let reader = SSHImportReader()
await #expect(throws: SSHImportSourceError.expectedFolder) {
_ = try await reader.scan(root: file, knownHostsOnly: false)
}
await #expect(throws: SSHImportSourceError.expectedFile) {
_ = try await reader.scan(root: url, knownHostsOnly: true)
}
#expect(try await reader.scan(root: file, knownHostsOnly: true).isEmpty)
}
@Test
func malformedPrivateKeysAreUnavailableRatherThanAskingForAPassphrase() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
try "-----BEGIN OPENSSH PRIVATE KEY-----\nAAAA\n-----END OPENSSH PRIVATE KEY-----".write(
to: url.appendingPathComponent("broken"), atomically: true, encoding: .utf8)
let reader = SSHImportReader()
let rows = try await reader.scan(root: url, knownHostsOnly: false)
#expect(rows.count == 1)
#expect(rows[0].unavailable && !rows[0].selected && !rows[0].requiresPassphrase)
}
@Test
func selectionStartsCheckedAndCollapseDoesNotChangeIt() {
let model = SSHImportModel(library: HostLibraryModel(), reader: SSHImportReader())
model.items = [
SSHImportItem(category: .hosts, name: "a", source: "config"),
SSHImportItem(category: .keys, name: "b", source: "key"),
SSHImportItem(category: .knownHosts, name: "c", source: "known_hosts"),
SSHImportItem(category: .keys, name: "bad", source: "bad", selected: false, unavailable: true),
]
#expect(model.selectedItems().count == 3)
model.expanded.remove(.keys)
#expect(model.selectedItems().count == 3)
model.setSelected(false, category: .keys)
#expect(model.selectedItems().count == 2)
model.setSelected(false)
#expect(!model.canImport)
model.setSelected(true)
#expect(model.selectedItems().count == 3)
#expect(!model.items[3].selected)
}
@Test
func folderImportMigratesSecretsAndIsIdempotent() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
try material.privateKey.write(to: url.appendingPathComponent("id_ed25519"), atomically: true, encoding: .utf8)
try "Host prod\n HostName prod.example\n User deploy\n IdentityFile ~/.ssh/id_ed25519\n".write(
to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8)
try "prod.example \(material.publicKey)".write(
to: url.appendingPathComponent("known_hosts"), atomically: true, encoding: .utf8)
let group = HostGroup(name: "Imported")
let store = HostLibraryModel(groups: [group])
let model = SSHImportModel(library: store, reader: SSHImportReader())
model.hostGroupID = group.id
model.hostTags = ["production", "ssh"]
model.storage = .thisDevice
model.importColor = .purple
await model.scan(url, knownHostsOnly: false)
#expect(model.selectedItems().count == 4)
#expect(store.hosts.isEmpty && store.keys.isEmpty)
#expect(model.canImport)
await model.commit()
#expect(model.completed)
#expect(store.hosts.count == 1 && store.keys.count == 1 && store.knownHosts.count == 1)
let identity = try #require(store.identities.first)
#expect(identity.username == "deploy" && identity.keyID == store.keys.first?.id)
#expect(store.hosts.first?.groupID == group.id)
#expect(store.hosts.first?.colorTag == .purple)
#expect(store.hosts.first?.tags == ["production", "ssh"])
#expect(store.keys.first?.colorTag == .purple)
#expect(store.keys.first?.storage == .thisDevice)
#expect(identity.colorTag == .purple)
#expect(identity.passwordStorage == .thisDevice)
#expect(store.keyAvailability(for: try #require(store.keys.first)) == .available)
let again = SSHImportModel(library: store, reader: SSHImportReader())
await again.scan(url, knownHostsOnly: false)
#expect(again.selectedItems().isEmpty)
#expect(!again.canImport)
}
@Test
func encryptedKeyCanBeUnlockedAndDeselectionRefreshesHostAssociation() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: "test-password")
try material.privateKey.write(to: url.appendingPathComponent("deploy"), atomically: true, encoding: .utf8)
try "Host prod\nIdentityFile ~/.ssh/deploy".write(
to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8)
let model = SSHImportModel(library: HostLibraryModel(), reader: SSHImportReader())
await model.scan(url, knownHostsOnly: false)
let id = try #require(model.items.first { $0.category == .keys }?.id)
#expect(!model.canImport)
await model.unlock(id, passphrase: "wrong")
#expect(!model.canImport)
await model.unlock(id, passphrase: "test-password")
#expect(model.canImport)
let host = try #require(model.items.first { $0.host != nil })
#expect(model.chosenKey(for: host)?.id == id)
model.setSelected(false, category: .keys)
#expect(model.items.first(where: { $0.id == host.id })?.host?.identityID == nil)
#expect(model.canImport)
}
@Test
func identitySelectionControlsKeysAndHostBindingWithoutChangingHostSelection() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
try material.privateKey.write(
to: url.appendingPathComponent("deploy"), atomically: true, encoding: .utf8)
try "Host app\n User deploy\n IdentityFile ~/.ssh/deploy".write(
to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8)
let model = SSHImportModel(library: HostLibraryModel(), reader: SSHImportReader())
await model.scan(url, knownHostsOnly: false)
let hostID = try #require(model.items.first(where: { $0.host != nil })?.id)
let identityID = try #require(model.items.first(where: { $0.identity != nil })?.id)
let keyID = try #require(model.items.first(where: { $0.key != nil })?.id)
#expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == identityID)
model.setSelected(false, itemID: identityID)
#expect(model.items.first(where: { $0.id == hostID })?.selected == true)
#expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == nil)
#expect(model.items.first(where: { $0.id == keyID })?.selected == true)
model.setSelected(false, itemID: hostID)
model.setSelected(true, itemID: hostID)
#expect(model.items.first(where: { $0.id == identityID })?.selected == false)
#expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == nil)
model.setSelected(true, itemID: identityID)
#expect(model.items.first(where: { $0.id == keyID })?.selected == true)
#expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == identityID)
model.setSelected(false, itemID: keyID)
#expect(model.items.first(where: { $0.id == identityID })?.selected == false)
#expect(model.items.first(where: { $0.id == hostID })?.selected == true)
#expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == nil)
}
@Test
func sharedIdentityIsImportedOnceAndReceivesImportDefaults() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
try material.privateKey.write(
to: url.appendingPathComponent("deploy"), atomically: true, encoding: .utf8)
try """
Host app-one app-two
User deploy
IdentityFile ~/.ssh/deploy
""".write(to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8)
let store = HostLibraryModel()
let model = SSHImportModel(library: store, reader: SSHImportReader())
model.storage = .thisDevice
model.importColor = .orange
await model.scan(url, knownHostsOnly: false)
#expect(model.items.filter { $0.category == .identities }.count == 1)
await model.commit()
let identity = try #require(store.identities.first)
#expect(store.identities.count == 1)
#expect(store.hosts.count == 2)
#expect(store.hosts.allSatisfy { $0.identityID == identity.id })
#expect(identity.passwordStorage == .thisDevice)
#expect(identity.colorTag == .orange)
#expect(!identity.usesPassword)
}
@Test
func exactExistingIdentityIsReusedWhileDifferentKeysGetDistinctNames() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
let first = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
let second = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
try first.privateKey.write(to: url.appendingPathComponent("first"), atomically: true, encoding: .utf8)
try second.privateKey.write(to: url.appendingPathComponent("second"), atomically: true, encoding: .utf8)
try """
Host first
User deploy
IdentityFile ~/.ssh/first
Host second
User deploy
IdentityFile ~/.ssh/second
""".write(to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8)
let existingKey = SSHKeyRef(
label: "Saved", publicKey: first.publicKey, fingerprint: first.fingerprint,
credentialRef: "saved")
let existingIdentity = Identity(name: "Existing Deploy", username: "deploy", keyID: existingKey.id)
let secrets = InMemorySecretStore()
try secrets.setKeyCredential(
.software(SSHPrivateKeySecret(privateKey: first.privateKey)),
for: existingKey.credentialRef, storage: existingKey.storage)
let store = HostLibraryModel(keys: [existingKey], identities: [existingIdentity], secrets: secrets)
let model = SSHImportModel(library: store, reader: SSHImportReader())
await model.scan(url, knownHostsOnly: false)
let identities = model.items.filter { $0.category == .identities }
#expect(identities.count == 2)
#expect(identities.contains { $0.existingID == existingIdentity.id && $0.name == "Existing Deploy" })
#expect(identities.contains { $0.existingID == nil && $0.name.contains("second") })
}
@Test
func jumpDependenciesAndCyclesBlockCommit() {
let model = SSHImportModel(library: HostLibraryModel(), reader: SSHImportReader())
model.items = [
SSHImportItem(
category: .hosts, name: "app", source: "config",
host: SSHImportedHost(alias: "app", hostname: "app", username: "u", jumps: ["jump"])),
SSHImportItem(
category: .hosts, name: "jump", source: "config",
host: SSHImportedHost(alias: "jump", hostname: "jump", username: "u")),
]
#expect(model.canImport)
model.items[1].selected = false
#expect(!model.canImport)
model.items[1].selected = true
model.items[1].host?.jumps = ["app"]
#expect(!model.canImport)
#expect(model.routeProblem != nil)
}
@Test
func importRequiresKnownHostConflictReview() async {
let original = KnownHostEntry(hostname: "host", fingerprint: "SHA256:old")
let store = HostLibraryModel(knownHosts: [original])
let model = SSHImportModel(library: store, reader: SSHImportReader())
model.items = [
SSHImportItem(
category: .knownHosts, name: "host", source: "known_hosts",
knownHost: KnownHostEntry(hostname: "host", fingerprint: "SHA256:new"))
]
#expect(!model.canImport)
model.items[0].acceptedWarnings = true
#expect(model.canImport)
await model.commit()
#expect(model.completed)
#expect(store.knownHosts.first?.fingerprint == original.fingerprint)
}
@Test
func multiHopImportsPreserveStandaloneJumpConfigurations() async throws {
let url = try root()
defer { try? FileManager.default.removeItem(at: url) }
try "Host app\nProxyJump first,second\nHost first\nHost second".write(
to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8)
let store = HostLibraryModel()
let model = SSHImportModel(library: store, reader: SSHImportReader())
await model.scan(url, knownHostsOnly: false)
#expect(model.canImport)
await model.commit()
#expect(model.completed)
let second = try #require(store.hosts.first { $0.name == "second" })
#expect(second.jumpHostID == nil)
let app = try #require(store.hosts.first { $0.name == "app" })
let intermediate = try #require(app.jumpHostID.flatMap(store.host))
#expect(intermediate.host == "second")
#expect(intermediate.jumpHostID.flatMap(store.host)?.name == "first")
let again = SSHImportModel(library: store, reader: SSHImportReader())
await again.scan(url, knownHostsOnly: false)
#expect(again.selectedItems().isEmpty)
}
@Test
func externalKeyIsOnlyReadAfterExplicitAuthorization() async throws {
let url = try root()
let external = try root()
defer {
try? FileManager.default.removeItem(at: url)
try? FileManager.default.removeItem(at: external)
}
let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil)
let keyURL = external.appendingPathComponent("deploy")
try material.privateKey.write(to: keyURL, atomically: true, encoding: .utf8)
try "Host app\nIdentityFile \(keyURL.path)".write(
to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8)
let reader = SSHImportReader()
let before = try await reader.scan(root: url, knownHostsOnly: false)
#expect(before.compactMap(\.key).isEmpty)
#expect(before.contains { $0.needsAuthorization == keyURL.path })
let after = try await reader.scan(root: url, knownHostsOnly: false, grants: [keyURL])
#expect(after.compactMap(\.key).count == 1)
#expect(!after.contains { $0.needsAuthorization != nil })
}
@Test
func keychainPagePruningDoesNotEraseKnownHostSelection() {
let entry = KnownHostEntry(hostname: "host", fingerprint: "SHA256:key")
let store = HostLibraryModel(knownHosts: [entry])
let window = WindowModel()
window.selectVaultItem(.knownHost(entry.id), on: .knownHosts)
window.pruneVaultSelections(in: CredentialLibraryModel(library: store))
#expect(window.vaultSelection(for: .knownHosts) == [.knownHost(entry.id)])
}
@Test
func failedSnapshotRollsBackMetadataAndNewCredentials() throws {
let persistence = FailingImportPersistence()
let secrets = InMemorySecretStore()
let store = HostLibraryModel(persistence: persistence, secrets: secrets)
let key = SSHKeyRef(label: "Imported", credentialRef: "import-test")
let batch = SSHImportBatch(
hosts: [Host(name: "new", host: "new")], keys: [key],
knownHosts: [KnownHostEntry(hostname: "new", fingerprint: "SHA256:key")],
credentials: [key.id: .software(SSHPrivateKeySecret(privateKey: "test-private-material"))])
#expect(throws: (any Error).self) { try store.commitSSHImport(batch) }
#expect(store.hosts.isEmpty && store.keys.isEmpty && store.knownHosts.isEmpty)
#expect(try secrets.keyCredential(for: key.credentialRef, storage: key.storage) == nil)
#expect(!store.saveKnownHost(KnownHostEntry(hostname: "new", fingerprint: "SHA256:key")))
#expect(store.knownHosts.isEmpty)
}
@Test
func vaultKnownHostNavigationUsesFingerprintAndRetainsSelection() {
let entry = KnownHostEntry(hostname: "host", fingerprint: "SHA256:key")
let store = HostLibraryModel(knownHosts: [entry])
let window = WindowModel()
window.selectVaultItem(.knownHost(entry.id), on: .knownHosts)
window.vaultPages.knownHosts.query = "key"
window.openPage(.hosts)
window.openPage(.knownHosts)
window.pruneVaultSelections(in: store)
#expect(window.vaultSelection(for: .knownHosts) == [.knownHost(entry.id)])
#expect(window.vaultPages.knownHosts.query == "key")
#expect(window.findKind == .knownHosts)
#expect(VaultPage.allCases.last == .knownHosts)
}
}
@MainActor
private final class FailingImportPersistence: VaultPersisting {
func load() throws -> VaultSnapshot { VaultSnapshot() }
func save(
_ snapshot: VaultSnapshot, touching keys: Set<VaultRecordKey>
) throws -> VaultMutationSet { throw CocoaError(.fileWriteUnknown) }
}