151 lines
4.8 KiB
Swift
151 lines
4.8 KiB
Swift
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindInfrastructure
|
|
import TermindPersistence
|
|
import TermindPresentation
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
@testable import TermindRemoteInfrastructure
|
|
|
|
struct AuthorizedKeyExportTests {
|
|
@Test
|
|
func exportPlacesTheKeyNameCommentAboveThePublicKey() throws {
|
|
let input = try SSHConnectionOpener.authorizedKeyInstallInput(
|
|
publicKey: "ssh-ed25519 AAAAC3Nza test@example.com",
|
|
keyName: "Production"
|
|
)
|
|
|
|
#expect(
|
|
String(decoding: input, as: UTF8.self)
|
|
== "# Production Key by Termind\nssh-ed25519 AAAAC3Nza test@example.com\n"
|
|
)
|
|
}
|
|
|
|
@Test
|
|
func exportKeepsTheGeneratedCommentOnOneLine() throws {
|
|
let input = try SSHConnectionOpener.authorizedKeyInstallInput(
|
|
publicKey: "ssh-ed25519 AAAAC3Nza",
|
|
keyName: " Production\n\tServer "
|
|
)
|
|
|
|
#expect(
|
|
String(decoding: input, as: UTF8.self)
|
|
== "# Production Server Key by Termind\nssh-ed25519 AAAAC3Nza\n"
|
|
)
|
|
}
|
|
|
|
@Test
|
|
func privateKeyEnvelopeRequiresACompleteJSONObject() throws {
|
|
var encoded = try JSONEncoder().encode(
|
|
SSHPrivateKeySecret(privateKey: "-----BEGIN TEST-----\nsecret\n-----END TEST-----")
|
|
)
|
|
let secret = try KeychainSecretStore.decodePrivateKeySecret(from: &encoded)
|
|
#expect(encoded.isEmpty)
|
|
#expect(secret.matches("-----BEGIN TEST-----\nsecret\n-----END TEST-----"))
|
|
|
|
#expect(throws: KeychainSecretError.self) {
|
|
var data = Data(#"{"privateKey":"secret""#.utf8)
|
|
_ = try KeychainSecretStore.decodePrivateKeySecret(
|
|
from: &data
|
|
)
|
|
}
|
|
#expect(throws: KeychainSecretError.self) {
|
|
var data = Data(#"{"privateKey":"secret"} trailing"#.utf8)
|
|
_ = try KeychainSecretStore.decodePrivateKeySecret(
|
|
from: &data
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func multipleKeysReuseOneAuthenticatedConnection() async throws {
|
|
let password = SSHSecret("one-use")
|
|
let request = SSHConnectionRequest(
|
|
configuration: SSHConnectionConfiguration(
|
|
hostID: UUID(), host: "server.invalid", port: 22,
|
|
username: "deploy", credentials: [.password(password)],
|
|
expectedHostKeys: []
|
|
)
|
|
)
|
|
let connection = AuthorizedKeyConnection()
|
|
|
|
try await SSHPTYChannelWorker.installAuthorizedKeys(
|
|
[
|
|
SSHAuthorizedKeyInstall(publicKey: "ssh-ed25519 first", name: "First"),
|
|
SSHAuthorizedKeyInstall(publicKey: "ssh-ed25519 second", name: "Second"),
|
|
],
|
|
using: request, connection: connection
|
|
)
|
|
|
|
let snapshot = await connection.snapshot()
|
|
#expect(snapshot.connects == 1)
|
|
#expect(snapshot.executions == 2)
|
|
#expect(snapshot.disconnects == 1)
|
|
#expect(
|
|
snapshot.inputs == [
|
|
"# First Key by Termind\nssh-ed25519 first\n",
|
|
"# Second Key by Termind\nssh-ed25519 second\n",
|
|
])
|
|
#expect(password.isConsumed)
|
|
}
|
|
}
|
|
|
|
private actor AuthorizedKeyConnection: SSHConnectionProtocol {
|
|
struct Snapshot: Sendable {
|
|
var connects: Int
|
|
var executions: Int
|
|
var disconnects: Int
|
|
var inputs: [String]
|
|
}
|
|
|
|
private var connects = 0
|
|
private var executions = 0
|
|
private var disconnects = 0
|
|
private var inputs: [String] = []
|
|
|
|
func connect(using configuration: SSHConnectionConfiguration) async throws {
|
|
connects += 1
|
|
}
|
|
|
|
func execute(
|
|
_ command: String, workingDirectory: String?, stdin: SSHCommandInput?,
|
|
timeout: TimeInterval, outputLimit: Int, onOutput: SSHExecOutputHandler?
|
|
) async throws -> SSHExecResult {
|
|
executions += 1
|
|
let input: String
|
|
switch stdin {
|
|
case .plain(let bytes):
|
|
input = String(decoding: bytes, as: UTF8.self)
|
|
case .text(let value):
|
|
input = value
|
|
case .secret(let secret):
|
|
defer { secret.clear() }
|
|
input = try secret.withUnsafeBytes { String(decoding: $0, as: UTF8.self) }
|
|
case nil:
|
|
input = ""
|
|
}
|
|
inputs.append(input)
|
|
return SSHExecResult(
|
|
stdout: "", stderr: "", exitCode: 0,
|
|
stdoutTruncated: false, stderrTruncated: false
|
|
)
|
|
}
|
|
|
|
func openSFTPChannel() async throws -> any SFTPChannelProtocol {
|
|
throw TermindSSHError.disconnected
|
|
}
|
|
|
|
func disconnect() async {
|
|
disconnects += 1
|
|
}
|
|
|
|
func snapshot() -> Snapshot {
|
|
Snapshot(
|
|
connects: connects, executions: executions,
|
|
disconnects: disconnects, inputs: inputs
|
|
)
|
|
}
|
|
}
|