141 lines
6.1 KiB
Swift
141 lines
6.1 KiB
Swift
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import Testing
|
|
|
|
import struct TermindDomain.Host
|
|
|
|
@testable import Termind
|
|
|
|
@Suite @MainActor
|
|
struct DesktopAuthenticationSubmissionTests {
|
|
@Test(arguments: [ConnectionHistoryEntry.Kind.ssh, .sftp], [false, true])
|
|
func unsavedInputPreservesSelectionAndPassphraseContinuation(kind: ConnectionHistoryEntry.Kind, selected: Bool)
|
|
throws
|
|
{
|
|
let key = protectedKey()
|
|
let identity = Identity(name: "Deploy", username: " saved-user \n", keyID: key.id)
|
|
let host = Host(name: "Server", host: "server.invalid")
|
|
let store = HostLibraryModel(hosts: [host], keys: [key], identities: [identity])
|
|
let session = DesktopAuthenticationSession(kind: kind, host: host)
|
|
defer { session.disconnect() }
|
|
|
|
session.submit(
|
|
identityID: selected ? identity.id : nil, username: " manual-user \n", password: "one-use",
|
|
keyID: selected ? UUID() : key.id, save: false, store: store)
|
|
|
|
let prompt = try #require(session.prompt)
|
|
#expect(prompt.requiresPassphrase)
|
|
#expect(prompt.username == (selected ? "saved-user" : "manual-user"))
|
|
#expect(prompt.selectedKeyID == key.id)
|
|
#expect(prompt.selectedIdentityID == (selected ? identity.id : nil))
|
|
#expect(store.host(host.id) == host)
|
|
#expect(store.identity(identity.id) == identity)
|
|
|
|
session.submit(
|
|
identityID: nil, username: "ignored", password: "ignored", passphrase: "",
|
|
keyID: UUID(), save: true, store: store)
|
|
#expect(session.prompt?.id == prompt.id)
|
|
#expect(store.host(host.id) == host)
|
|
#expect(store.identities == [identity])
|
|
}
|
|
|
|
@Test(arguments: [ConnectionHistoryEntry.Kind.ssh, .sftp])
|
|
func blankUsernameLeavesTheOriginalPromptAndStoreUntouched(kind: ConnectionHistoryEntry.Kind) throws {
|
|
let host = Host(name: "Server", host: "server.invalid")
|
|
let store = HostLibraryModel(hosts: [host])
|
|
let session = DesktopAuthenticationSession(kind: kind, host: host)
|
|
defer { session.disconnect() }
|
|
let prompt = try #require(session.prompt)
|
|
|
|
session.submit(username: " \n\t ", password: "unused", save: true, store: store)
|
|
|
|
#expect(session.prompt?.id == prompt.id)
|
|
#expect(store.host(host.id) == host)
|
|
#expect(store.identities.isEmpty)
|
|
}
|
|
|
|
@Test(arguments: [ConnectionHistoryEntry.Kind.ssh, .sftp])
|
|
func clearingSavedPasswordDeletesItAndContinuesWithTheKey(kind: ConnectionHistoryEntry.Kind) throws {
|
|
let key = protectedKey()
|
|
let secrets = InMemorySecretStore()
|
|
let identity = Identity(name: "Deploy", username: "deploy", credentialRef: "password", keyID: key.id)
|
|
let host = Host(name: "Server", host: "server.invalid")
|
|
let store = HostLibraryModel(hosts: [host], keys: [key], identities: [identity], secrets: secrets)
|
|
try secrets.setString("old", for: identity.credentialRef, storage: identity.passwordStorage)
|
|
let session = DesktopAuthenticationSession(kind: kind, host: host)
|
|
defer { session.disconnect() }
|
|
|
|
session.submit(identityID: identity.id, username: "ignored", password: "", save: true, store: store)
|
|
|
|
#expect(store.host(host.id)?.identityID == identity.id)
|
|
let updatedIdentity = try #require(store.identity(identity.id))
|
|
#expect(try store.passwordCredential(for: updatedIdentity) == nil)
|
|
#expect(session.prompt?.requiresPassphrase == true)
|
|
#expect(session.prompt?.failureMessage == nil)
|
|
}
|
|
|
|
@Test(arguments: [false, true])
|
|
func temporarySSHHostCanContinueWithOrWithoutSaving(_ save: Bool) throws {
|
|
let key = protectedKey()
|
|
let host = Host(name: "Temporary", host: "temporary.invalid")
|
|
let store = HostLibraryModel(keys: [key])
|
|
let session = DesktopAuthenticationSession(kind: .ssh, host: host)
|
|
defer { session.disconnect() }
|
|
|
|
session.submit(username: " deploy ", password: "one-use", keyID: key.id, save: save, store: store)
|
|
|
|
let prompt = try #require(session.prompt)
|
|
#expect(prompt.host.id == host.id)
|
|
#expect(prompt.username == "deploy")
|
|
#expect(prompt.requiresPassphrase)
|
|
#expect((store.host(host.id) != nil) == save)
|
|
#expect(store.identities.count == (save ? 1 : 0))
|
|
if save {
|
|
let saved = try #require(store.host(host.id))
|
|
#expect(prompt.host.identityID == saved.identityID)
|
|
#expect(store.identity(for: saved)?.username == "deploy")
|
|
#expect(store.identity(for: saved)?.isHostCustom == true)
|
|
#expect(store.reusableIdentities.isEmpty)
|
|
}
|
|
}
|
|
|
|
private func protectedKey() -> SSHKeyRef {
|
|
SSHKeyRef(label: "Protected", publicKey: "ssh-ed25519 fixture", hasPassphrase: true)
|
|
}
|
|
}
|
|
|
|
/// A pending protected-key prompt keeps these entry-point tests entirely local:
|
|
/// continuing without its passphrase cannot open a transport.
|
|
@MainActor
|
|
private final class DesktopAuthenticationSession {
|
|
let ssh: SSHSessionModel?
|
|
let sftp: SFTPChannelModel?
|
|
var prompt: SSHAuthenticationPrompt? { ssh?.pendingAuthentication ?? sftp?.pendingAuthentication }
|
|
|
|
init(kind: ConnectionHistoryEntry.Kind, host: Host) {
|
|
ssh = kind == .ssh ? SSHSessionModel(host: host) : nil
|
|
sftp = kind == .sftp ? SFTPChannelModel(hostID: host.id) : nil
|
|
let prompt = SSHAuthenticationPrompt(host: host, username: "")
|
|
ssh?.pendingAuthentication = prompt
|
|
sftp?.pendingAuthentication = prompt
|
|
}
|
|
|
|
func submit(
|
|
identityID: UUID? = nil, username: String, password: String?, passphrase: String? = nil,
|
|
keyID: UUID? = nil, save: Bool, store: HostLibraryModel
|
|
) {
|
|
ssh?.continueAuthentication(
|
|
identityID: identityID, username: username, password: password, passphrase: passphrase,
|
|
keyID: keyID, save: save, using: store)
|
|
sftp?.continueAuthentication(
|
|
identityID: identityID, username: username, password: password, passphrase: passphrase,
|
|
keyID: keyID, save: save, using: store)
|
|
}
|
|
|
|
func disconnect() {
|
|
ssh?.disconnect()
|
|
sftp?.disconnect()
|
|
}
|
|
}
|