143 lines
4.6 KiB
Swift
143 lines
4.6 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
@testable import TermindRemoteInfrastructure
|
|
|
|
struct AuthorizedKeyExportTests {
|
|
@Test(arguments: [
|
|
(
|
|
"ssh-ed25519 AAAAC3Nza test@example.com", "Production",
|
|
"# Production Key by Termind\nssh-ed25519 AAAAC3Nza test@example.com\n"
|
|
),
|
|
(
|
|
"ssh-ed25519 AAAAC3Nza", " Production\n\tServer ",
|
|
"# Production Server Key by Termind\nssh-ed25519 AAAAC3Nza\n"
|
|
),
|
|
])
|
|
func exportPlacesTheKeyNameCommentAboveThePublicKeyOnOneLine(
|
|
publicKey: String, keyName: String, expected: String
|
|
) throws {
|
|
let input = try SSHConnectionOpener.authorizedKeyInstallInput(publicKey: publicKey, keyName: keyName)
|
|
|
|
#expect(String(decoding: input, as: UTF8.self) == expected)
|
|
}
|
|
|
|
@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
|
|
)
|
|
}
|
|
}
|