Files
orbithub/src/ssh_session_backend.h
T
ksmithandClaude Sonnet 5 9842a44de0 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>
2026-09-15 15:16:32 -06:00

70 lines
2.4 KiB
C++

#ifndef ORBITHUB_SSH_SESSION_BACKEND_H
#define ORBITHUB_SSH_SESSION_BACKEND_H
#include "session_backend.h"
#include <QProcess>
#include <QString>
#include <QTimer>
class SshSessionBackend : public SessionBackend
{
Q_OBJECT
public:
explicit SshSessionBackend(const Profile& profile, QObject* parent = nullptr);
// Test-only: overrides the executable launched instead of "ssh", so
// tests can point it at a controllable fixture script.
SshSessionBackend(const Profile& profile, const QString& sshProgramOverride, QObject* parent);
~SshSessionBackend() override;
// Pure, state-free helpers exposed as public statics purely so tests
// can exercise them directly without spinning up a real ssh process.
static QString mapSshError(const QString& rawError);
static QString escapeForShellSingleQuotes(const QString& value);
public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
void reconnectSession(const SessionConnectOptions& options) override;
void sendInput(const QString& input) override;
void confirmHostKey(bool trustHost) override;
void updateTerminalSize(int columns, int rows) override;
private slots:
void onProcessStarted();
void onProcessErrorOccurred(QProcess::ProcessError error);
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
void onReadyReadStandardOutput();
void onReadyReadStandardError();
void onConnectedProbeTimeout();
private:
QProcess* m_process;
QTimer* m_connectedProbeTimer;
SessionState m_state;
bool m_userInitiatedDisconnect;
bool m_reconnectPending;
SessionConnectOptions m_reconnectOptions;
SessionConnectOptions m_activeOptions;
QString m_lastRawError;
QString m_askPassScriptPath;
bool m_waitingForPasswordPrompt;
bool m_waitingForHostKeyConfirmation;
bool m_passwordSubmitted;
int m_terminalColumns;
int m_terminalRows;
QString m_sshProgram;
void setState(SessionState state, const QString& message);
bool startSshProcess(const SessionConnectOptions& options);
bool configureAskPass(const SessionConnectOptions& options,
QProcessEnvironment& environment,
QString& error);
void cleanupAskPassScript();
QString knownHostsFileForNullDevice() const;
void applyTerminalSizeIfAvailable();
};
#endif