Add SshSessionBackend test coverage (#1)

Adds two kinds of coverage, continuing #1's remaining scope:

1. Pure-function tests for mapSshError() and escapeForShellSingleQuotes(),
   promoted from private members to public statics purely so tests can
   call them without spinning up a process. escapeForShellSingleQuotes()
   is the actual security boundary for password auth (it's what stops a
   password containing a single quote from breaking out of the askpass
   script's quoting), so it gets a real adversarial test, not just a
   happy-path one.

2. State-machine tests (connect -> Connected, auth failure -> Failed with
   the right mapped message, connection refused -> Failed, input
   round-tripping, reconnect) driven against tests/fixtures/fake_ssh.sh,
   a small controllable stand-in for the real ssh binary, instead of a
   real network/SSH server. This needed one small testability seam: a new
   constructor overload that overrides the launched program ("ssh" in
   production, the fixture script in tests).

POSIX-only for now: the fixture is a shell script, so the state-machine
tests QSKIP on Windows until an equivalent fixture exists there; the
pure-function tests run everywhere.

RdpSessionBackend coverage is still open -- it's a bigger lift again
(FreeRDP's own event loop, not just a QProcess), left for a follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 15:16:32 -06:00
co-authored by Claude Sonnet 5
parent eb63bde870
commit 9842a44de0
5 changed files with 302 additions and 11 deletions
+16 -10
View File
@@ -7,16 +7,14 @@
#include <QTextStream>
#include <QUuid>
namespace {
QString escapeForShellSingleQuotes(const QString& value)
SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
: SshSessionBackend(profile, QStringLiteral("ssh"), parent)
{
QString escaped = value;
escaped.replace(QStringLiteral("'"), QStringLiteral("'\"'\"'"));
return escaped;
}
}
SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
SshSessionBackend::SshSessionBackend(const Profile& profile,
const QString& sshProgramOverride,
QObject* parent)
: SessionBackend(profile, parent),
m_process(new QProcess(this)),
m_connectedProbeTimer(new QTimer(this)),
@@ -27,7 +25,8 @@ SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
m_waitingForHostKeyConfirmation(false),
m_passwordSubmitted(false),
m_terminalColumns(0),
m_terminalRows(0)
m_terminalRows(0),
m_sshProgram(sshProgramOverride)
{
m_connectedProbeTimer->setSingleShot(true);
@@ -395,7 +394,7 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
args << target;
m_process->setProcessEnvironment(environment);
m_process->setProgram(QStringLiteral("ssh"));
m_process->setProgram(m_sshProgram);
m_process->setArguments(args);
m_process->setProcessChannelMode(QProcess::SeparateChannels);
@@ -471,7 +470,7 @@ void SshSessionBackend::cleanupAskPassScript()
}
}
QString SshSessionBackend::mapSshError(const QString& rawError) const
QString SshSessionBackend::mapSshError(const QString& rawError)
{
const QString raw = rawError.trimmed();
if (raw.contains(QStringLiteral("Permission denied"), Qt::CaseInsensitive)) {
@@ -510,6 +509,13 @@ QString SshSessionBackend::mapSshError(const QString& rawError) const
return QStringLiteral("SSH connection failed.");
}
QString SshSessionBackend::escapeForShellSingleQuotes(const QString& value)
{
QString escaped = value;
escaped.replace(QStringLiteral("'"), QStringLiteral("'\"'\"'"));
return escaped;
}
QString SshSessionBackend::knownHostsFileForNullDevice() const
{
#ifdef Q_OS_WIN