Compare commits

...
Author SHA1 Message Date
ksmithandClaude Sonnet 5 1506d87719 Make username/password connect prompts hard to miss (issue #22)
The inline prompt bar previously had no explicit styling and rendered
in the same color as the rest of the tab; a tab showing the prompt in
the background had no indication anything needed attention. The bar
now uses a solid QPalette::Highlight fill with HighlightedText for the
label and a hand-drawn contrasting badge, and a background tab gets a
"(Needs input)" title suffix plus a distinct tab-bar color.

A first pass at the tab color (#6a1b9a) was reported unreadable in
dark mode; replaced with #ab47bc, tuned to match the visibility of the
existing connection-state colors.

Bump version to v2026.9.16.6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 09:18:43 -06:00
ksmithandClaude Sonnet 5 b3cedcfa48 Fix connect-time username prompt never reaching SSH/RDP authentication
The username entered at the connect-time prompt (added for issue #21)
was updating SessionTab's own in-memory Profile copy, but
SshSessionBackend/RdpSessionBackend are constructed with -- and only
ever read from -- their own separate Profile copy on a worker thread,
which never saw that edit. Authentication was still built from the
original (blank) username regardless of what was typed into the
prompt.

SessionConnectOptions gains a username field, populated by SessionTab
on every connect attempt and threaded through the same way password
already is; both backends now prefer options.username over
profile().username. Covered by a new SSH regression test using an
exact-match fixture host that only succeeds for a specific
user@host target.

Bump version to v2026.9.16.5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:40:34 -06:00
ksmithandClaude Sonnet 5 0cdf930303 Fix connect-time username prompt being unreachable for SSH/RDP
validateProfileForConnect() still hard-failed with a blocking
QMessageBox for a blank SSH/RDP username, running before
requestConnectOptions() ever got a chance to prompt for it inline --
so the connect-time username prompt added for issue #21 was dead code
in practice; users just got told to go edit the profile instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:28:41 -06:00
11 changed files with 249 additions and 14 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 3.21)
project(OrbitHub VERSION 2026.9.16.3 LANGUAGES CXX) project(OrbitHub VERSION 2026.9.16.6 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
+42
View File
@@ -184,6 +184,48 @@ Delivered:
through the dialog), so importing any SSH/RDP entry without a through the dialog), so importing any SSH/RDP entry without a
username still failed outright until this second check was found and username still failed outright until this second check was found and
removed too removed too
- A third, independent username check was still live even after the two
above were removed: `SessionTab::validateProfileForConnect()` (run at
the very top of `connectSession()`/`reconnectSession()`, before
`requestConnectOptions()` ever gets a chance to run its async prompt)
had its own hard-fail "SSH/RDP username is required" `QMessageBox`,
so a blank-username profile still couldn't connect at all -- it just
told the user to go edit the profile instead of ever prompting inline.
Removed; connect-time prompting is now the only username gate for
SSH/RDP
- Even with the three checks above gone, a username entered at the
connect-time prompt still never actually reached SSH or RDP
authentication: `SshSessionBackend`/`RdpSessionBackend` are constructed
with their own `Profile` copy up front (moved to a worker thread) and
read `profile().username` directly, which never sees `SessionTab`'s
later edit to its own in-memory profile once the user answers the
prompt. `SessionConnectOptions` (which already carries `password` the
same way) gained a `username` field, populated by `SessionTab` from
its profile copy on every connect attempt; both backends now prefer
`options.username` over `profile().username` when building the actual
connect target/auth call. Covered by a new SSH regression test
(`tests/fixtures/fake_ssh.sh`'s `requireuser` host only accepts an
exact `prompted-user@requireuser` target, so the test fails unless the
option, not the stale profile copy, is actually used) -- RDP has no
equivalent fake-server test harness, so that side relies on mirroring
the already-tested `m_activeOptions.password` pattern exactly
- Issue #22: the inline username/password prompt bar used to just be a
plain `QWidget` with `setAutoFillBackground(true)` and no explicit
color, which meant it rendered in the same color as everything else
around it and was easy to miss -- especially on a tab that wasn't the
active one, where there was previously no indication anything needed
attention at all. Now uses a solid `QPalette::Highlight` fill with
`QPalette::HighlightedText` for the label (the OS theme's own
guaranteed-contrasting pair, so it stays correct under both light and
dark themes without a hardcoded color) plus a hand-drawn "?" badge
(not a themed `QStyle` icon, whose own colors are outside our control
and could land close in hue to the bar's background); a background
tab showing the prompt gets its title suffixed "(Needs input)" and its
tab-bar text colored distinctly from the four connection-state colors.
A first pass at the tab color (`#6a1b9a`) was reported unreadable in
dark mode -- its perceived luminance was well below the four existing
state colors -- and was replaced with `#ab47bc`, tuned to roughly
match their visibility
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding - Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
used to abort the connection generically; `kAnnouncedEncodings` is now used to abort the connection generically; `kAnnouncedEncodings` is now
the single source of truth for what `SetEncodings` announces and what the single source of truth for what `SetEncodings` announces and what
+7 -1
View File
@@ -1640,7 +1640,13 @@ void RdpSessionBackend::workerMain()
const Profile& p = profile(); const Profile& p = profile();
const QString host = p.host.trimmed(); const QString host = p.host.trimmed();
QString username = p.username.trimmed(); // m_activeOptions.username carries a value prompted for at connect time
// (see SessionTab::requestConnectOptions()) when the saved profile's
// own username was blank; profile().username never sees that edit
// since this backend's Profile copy was captured at construction time.
QString username = m_activeOptions.username.trimmed().isEmpty()
? p.username.trimmed()
: m_activeOptions.username.trimmed();
QString domain = p.domain.trimmed(); QString domain = p.domain.trimmed();
if (domain.isEmpty()) { if (domain.isEmpty()) {
const int domainSeparator = username.indexOf(QLatin1Char('\\')); const int domainSeparator = username.indexOf(QLatin1Char('\\'));
+6
View File
@@ -12,6 +12,12 @@
class SessionConnectOptions class SessionConnectOptions
{ {
public: public:
// Only set when the profile's own username was blank and SessionTab
// prompted for one inline at connect time (see issue #21); empty means
// "use the backend's own profile().username" as before. SSH/RDP need
// this up front, unlike VNC's Apple auth which discovers the need for
// one mid-connection via usernameRequested()/provideUsername() instead.
QString username;
QString password; QString password;
QString privateKeyPath; QString privateKeyPath;
QString knownHostsPolicy; QString knownHostsPolicy;
+83 -8
View File
@@ -17,10 +17,14 @@
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMessageBox> #include <QMessageBox>
#include <QPainter>
#include <QPalette>
#include <QPixmap>
#include <QPlainTextEdit> #include <QPlainTextEdit>
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QMimeData> #include <QMimeData>
#include <QColor>
#include <QComboBox> #include <QComboBox>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QPushButton> #include <QPushButton>
@@ -59,6 +63,34 @@ TerminalTheme themeForName(const QString& themeName)
return TerminalTheme::loadKonsoleTheme( return TerminalTheme::loadKonsoleTheme(
QStringLiteral(":/KodoTermThemes/konsole/Breeze.colorscheme")); QStringLiteral(":/KodoTermThemes/konsole/Breeze.colorscheme"));
} }
// A filled circle with a bold "?", used on the username/password prompt
// bar (issue #22). Drawn by hand rather than pulled from a QStyle standard
// icon because a themed icon's own internal colors are outside our
// control and could end up close in hue to the bar's own background,
// undermining the contrast the bar is trying to achieve; painting it
// ourselves guarantees fillColor/textColor are exactly the same
// guaranteed-contrasting pair used for the rest of the bar.
QPixmap questionMarkBadgePixmap(const QColor& fillColor, const QColor& textColor, int diameter)
{
QPixmap pixmap(diameter, diameter);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(Qt::NoPen);
painter.setBrush(fillColor);
painter.drawEllipse(0, 0, diameter, diameter);
QFont font = painter.font();
font.setBold(true);
font.setPixelSize(static_cast<int>(diameter * 0.65));
painter.setFont(font);
painter.setPen(textColor);
painter.drawText(QRect(0, 0, diameter, diameter), Qt::AlignCenter, QStringLiteral("?"));
return pixmap;
}
} }
SessionTab::SessionTab(const Profile& profile, SessionTab::SessionTab(const Profile& profile,
@@ -93,10 +125,12 @@ SessionTab::SessionTab(const Profile& profile,
m_exportEventsButton(nullptr), m_exportEventsButton(nullptr),
m_eventsPanel(nullptr), m_eventsPanel(nullptr),
m_passwordPromptBar(nullptr), m_passwordPromptBar(nullptr),
m_passwordPromptIcon(nullptr),
m_passwordPromptLabel(nullptr), m_passwordPromptLabel(nullptr),
m_passwordPromptInput(nullptr), m_passwordPromptInput(nullptr),
m_passwordPromptConnectButton(nullptr), m_passwordPromptConnectButton(nullptr),
m_passwordPromptCancelButton(nullptr), m_passwordPromptCancelButton(nullptr),
m_awaitingUserInput(false),
m_eventSeverityFilter(EventSeverity::Info), m_eventSeverityFilter(EventSeverity::Info),
m_eventsPanelExpanded(preferences.eventsPanelExpanded) m_eventsPanelExpanded(preferences.eventsPanelExpanded)
{ {
@@ -341,9 +375,17 @@ SessionTab::~SessionTab()
QString SessionTab::tabTitle() const QString SessionTab::tabTitle() const
{ {
if (m_awaitingUserInput) {
return QStringLiteral("%1 (Needs input)").arg(m_profile.name);
}
return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix()); return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix());
} }
bool SessionTab::awaitingUserInput() const
{
return m_awaitingUserInput;
}
void SessionTab::connectSession() void SessionTab::connectSession()
{ {
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) { if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
@@ -806,21 +848,42 @@ void SessionTab::setupUi()
applyTerminalTheme(m_terminalThemeName); applyTerminalTheme(m_terminalThemeName);
// Styled distinctly (issue #22: this bar used to blend straight into
// the plain window background and was easy to miss, especially on a
// tab you weren't actively looking at). A solid QPalette::Highlight
// fill with QPalette::HighlightedText for the label is used rather
// than a subtle tint -- a faint tint proved to still be easy to miss,
// and Highlight/HighlightedText are the OS theme's own guaranteed-
// contrasting pair, so this stays readable under both light and dark
// themes without hardcoding a color.
auto* passwordPromptLayout = new QHBoxLayout(); auto* passwordPromptLayout = new QHBoxLayout();
m_passwordPromptIcon = new QLabel(this);
const QColor highlight = palette().color(QPalette::Highlight);
const QColor highlightedText = palette().color(QPalette::HighlightedText);
m_passwordPromptIcon->setPixmap(questionMarkBadgePixmap(highlightedText, highlight, 22));
m_passwordPromptLabel = new QLabel(this); m_passwordPromptLabel = new QLabel(this);
m_passwordPromptLabel->setObjectName(QStringLiteral("passwordPromptLabel"));
m_passwordPromptInput = new QLineEdit(this); m_passwordPromptInput = new QLineEdit(this);
m_passwordPromptInput->setEchoMode(QLineEdit::Password); m_passwordPromptInput->setEchoMode(QLineEdit::Password);
m_passwordPromptConnectButton = new QPushButton(QStringLiteral("Connect"), this); m_passwordPromptConnectButton = new QPushButton(QStringLiteral("Connect"), this);
m_passwordPromptCancelButton = new QPushButton(QStringLiteral("Cancel"), this); m_passwordPromptCancelButton = new QPushButton(QStringLiteral("Cancel"), this);
passwordPromptLayout->addWidget(m_passwordPromptIcon);
passwordPromptLayout->addWidget(m_passwordPromptLabel); passwordPromptLayout->addWidget(m_passwordPromptLabel);
passwordPromptLayout->addWidget(m_passwordPromptInput, 1); passwordPromptLayout->addWidget(m_passwordPromptInput, 1);
passwordPromptLayout->addWidget(m_passwordPromptConnectButton); passwordPromptLayout->addWidget(m_passwordPromptConnectButton);
passwordPromptLayout->addWidget(m_passwordPromptCancelButton); passwordPromptLayout->addWidget(m_passwordPromptCancelButton);
passwordPromptLayout->setContentsMargins(10, 8, 10, 8);
m_passwordPromptBar = new QWidget(this); m_passwordPromptBar = new QWidget(this);
m_passwordPromptBar->setObjectName(QStringLiteral("passwordPromptBar"));
m_passwordPromptBar->setLayout(passwordPromptLayout); m_passwordPromptBar->setLayout(passwordPromptLayout);
m_passwordPromptBar->setAutoFillBackground(true); m_passwordPromptBar->setAutoFillBackground(true);
m_passwordPromptBar->setVisible(false); m_passwordPromptBar->setVisible(false);
m_passwordPromptBar->setStyleSheet(
QStringLiteral("QWidget#passwordPromptBar { background-color: %1; }"
"QWidget#passwordPromptBar QLabel#passwordPromptLabel "
"{ color: %2; font-weight: bold; font-size: 11pt; }")
.arg(highlight.name(), highlightedText.name()));
rootLayout->addWidget(m_passwordPromptBar); rootLayout->addWidget(m_passwordPromptBar);
connect(m_passwordPromptConnectButton, &QPushButton::clicked, this, [this]() { connect(m_passwordPromptConnectButton, &QPushButton::clicked, this, [this]() {
@@ -998,6 +1061,10 @@ void SessionTab::requestConnectOptions(
{ {
SessionConnectOptions baseOptions; SessionConnectOptions baseOptions;
baseOptions.knownHostsPolicy = m_profile.knownHostsPolicy; baseOptions.knownHostsPolicy = m_profile.knownHostsPolicy;
// The backend's own Profile copy was captured when it was constructed
// and never sees later edits to m_profile (e.g. the username prompt
// below) -- it has to travel through here instead.
baseOptions.username = m_profile.username.trimmed();
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0; const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0; const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
@@ -1157,12 +1224,24 @@ void SessionTab::showPasswordPrompt(const QString& labelText,
m_passwordPromptInput->setEchoMode(maskInput ? QLineEdit::Password : QLineEdit::Normal); m_passwordPromptInput->setEchoMode(maskInput ? QLineEdit::Password : QLineEdit::Normal);
m_passwordPromptBar->setVisible(true); m_passwordPromptBar->setVisible(true);
m_passwordPromptInput->setFocus(); m_passwordPromptInput->setFocus();
if (!m_awaitingUserInput) {
m_awaitingUserInput = true;
emit awaitingUserInputChanged(true);
emit tabTitleChanged(tabTitle());
}
} }
void SessionTab::hidePasswordPrompt() void SessionTab::hidePasswordPrompt()
{ {
m_passwordPromptBar->setVisible(false); m_passwordPromptBar->setVisible(false);
m_passwordPromptCallback = nullptr; m_passwordPromptCallback = nullptr;
if (m_awaitingUserInput) {
m_awaitingUserInput = false;
emit awaitingUserInputChanged(false);
emit tabTitleChanged(tabTitle());
}
} }
bool SessionTab::validateProfileForConnect() bool SessionTab::validateProfileForConnect()
@@ -1181,14 +1260,10 @@ bool SessionTab::validateProfileForConnect()
return false; return false;
} }
if ((m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0 // SSH/RDP no longer hard-require a username here -- a blank one is
|| m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) // handled by requestConnectOptions() prompting for it inline at connect
&& m_profile.username.trimmed().isEmpty()) { // time (see issue #21). Do not re-add a check here without also
QMessageBox::warning(this, // updating that flow.
QStringLiteral("Connect"),
QStringLiteral("%1 username is required.").arg(m_profile.protocol));
return false;
}
return true; return true;
} }
+8
View File
@@ -67,10 +67,16 @@ public:
bool supportsVncScaleToggle() const; bool supportsVncScaleToggle() const;
void setVncScaleToFit(bool scaleToFit); void setVncScaleToFit(bool scaleToFit);
bool vncScaleToFit() const; bool vncScaleToFit() const;
bool awaitingUserInput() const;
signals: signals:
void tabTitleChanged(const QString& title); void tabTitleChanged(const QString& title);
void tabStateChanged(SessionState state); void tabStateChanged(SessionState state);
// Fires whenever the inline username/password prompt bar is shown or
// hidden -- independent of tabStateChanged(), since the backend is
// still just "Connecting" while it's up. Lets SessionWindow mark a
// background tab that needs the user's attention (issue #22).
void awaitingUserInputChanged(bool waiting);
void terminalThemeChanged(const QString& themeName); void terminalThemeChanged(const QString& themeName);
void terminalFontSizeChanged(int pointSize); void terminalFontSizeChanged(int pointSize);
void eventsPanelVisibilityChanged(bool expanded); void eventsPanelVisibilityChanged(bool expanded);
@@ -129,11 +135,13 @@ private:
QToolButton* m_exportEventsButton; QToolButton* m_exportEventsButton;
QWidget* m_eventsPanel; QWidget* m_eventsPanel;
QWidget* m_passwordPromptBar; QWidget* m_passwordPromptBar;
QLabel* m_passwordPromptIcon;
QLabel* m_passwordPromptLabel; QLabel* m_passwordPromptLabel;
QLineEdit* m_passwordPromptInput; QLineEdit* m_passwordPromptInput;
QPushButton* m_passwordPromptConnectButton; QPushButton* m_passwordPromptConnectButton;
QPushButton* m_passwordPromptCancelButton; QPushButton* m_passwordPromptCancelButton;
std::function<void(std::optional<QString>)> m_passwordPromptCallback; std::function<void(std::optional<QString>)> m_passwordPromptCallback;
bool m_awaitingUserInput;
enum class EventSeverity { enum class EventSeverity {
Info, Info,
Warning, Warning,
+38
View File
@@ -34,6 +34,19 @@ QColor tabColorForState(SessionState state, const QPalette& palette)
return palette.color(QPalette::WindowText); return palette.color(QPalette::WindowText);
} }
// Distinct from all four tabColorForState() colors -- a tab awaiting a
// username/password prompt response needs to stand out even from a
// tab that's merely "Connecting" (issue #22), including when it isn't
// the one currently in view. #6a1b9a (a much darker violet) was tried
// first and reported unreadable against a dark-theme tab bar -- its
// perceived luminance is well below the other three colors above, which
// this one is tuned to roughly match so it reads about as well as they
// do in both light and dark themes.
QColor awaitingInputTabColor()
{
return QColor(QStringLiteral("#ab47bc"));
}
QStringList terminalThemeNames() QStringList terminalThemeNames()
{ {
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")}; return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
@@ -57,6 +70,7 @@ SessionWindow::SessionWindow(QWidget* parent)
QWidget* tab = m_tabs->widget(index); QWidget* tab = m_tabs->widget(index);
if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) { if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) {
sessionTab->disconnectSession(); sessionTab->disconnectSession();
m_tabStates.remove(sessionTab);
} }
m_tabs->removeTab(index); m_tabs->removeTab(index);
delete tab; delete tab;
@@ -261,6 +275,7 @@ void SessionWindow::addSessionTab(const Profile& profile)
} else { } else {
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name)); setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
} }
m_tabStates.insert(tab, SessionState::Disconnected);
m_tabs->tabBar()->setTabTextColor( m_tabs->tabBar()->setTabTextColor(
index, tabColorForState(SessionState::Disconnected, m_tabs->palette())); index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
@@ -272,6 +287,13 @@ void SessionWindow::addSessionTab(const Profile& profile)
&SessionTab::tabStateChanged, &SessionTab::tabStateChanged,
this, this,
[this, tab](SessionState state) { [this, tab](SessionState state) {
m_tabStates.insert(tab, state);
if (tab->awaitingUserInput()) {
// Keep the "needs input" color on top -- it'll be
// restored to reflect this state once the prompt
// resolves (see awaitingUserInputChanged below).
return;
}
for (int i = 0; i < m_tabs->count(); ++i) { for (int i = 0; i < m_tabs->count(); ++i) {
if (m_tabs->widget(i) == tab) { if (m_tabs->widget(i) == tab) {
m_tabs->tabBar()->setTabTextColor( m_tabs->tabBar()->setTabTextColor(
@@ -280,6 +302,22 @@ void SessionWindow::addSessionTab(const Profile& profile)
} }
} }
}); });
connect(tab,
&SessionTab::awaitingUserInputChanged,
this,
[this, tab](bool waiting) {
for (int i = 0; i < m_tabs->count(); ++i) {
if (m_tabs->widget(i) != tab) {
continue;
}
const QColor color = waiting
? awaitingInputTabColor()
: tabColorForState(m_tabStates.value(tab, SessionState::Disconnected),
m_tabs->palette());
m_tabs->tabBar()->setTabTextColor(i, color);
return;
}
});
connect(tab, connect(tab,
&SessionTab::terminalThemeChanged, &SessionTab::terminalThemeChanged,
this, this,
+5
View File
@@ -4,6 +4,7 @@
#include "profile_repository.h" #include "profile_repository.h"
#include "session_tab.h" #include "session_tab.h"
#include <QHash>
#include <QMainWindow> #include <QMainWindow>
class QTabWidget; class QTabWidget;
@@ -21,6 +22,10 @@ private:
QTabWidget* m_tabs; QTabWidget* m_tabs;
ProfilesWindow* m_profilesWidget; ProfilesWindow* m_profilesWidget;
SessionUiPreferences m_preferences; SessionUiPreferences m_preferences;
// Last known connection state per tab, so the tab color can be
// restored correctly once an awaitingUserInputChanged(false) fires
// (that signal is orthogonal to SessionState -- see session_tab.h).
QHash<SessionTab*, SessionState> m_tabStates;
void addSessionTab(const Profile& profile); void addSessionTab(const Profile& profile);
void updateTabTitle(SessionTab* tab, const QString& title); void updateTabTitle(SessionTab* tab, const QString& title);
+9 -2
View File
@@ -388,9 +388,16 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
<< QStringLiteral("PasswordAuthentication=no"); << QStringLiteral("PasswordAuthentication=no");
} }
const QString target = p.username.trimmed().isEmpty() // options.username carries a value prompted for at connect time (see
// SessionTab::requestConnectOptions()) when the saved profile's own
// username was blank; profile().username never sees that edit since
// the backend's Profile copy was captured at construction time.
const QString username = options.username.trimmed().isEmpty()
? p.username.trimmed()
: options.username.trimmed();
const QString target = username.isEmpty()
? p.host.trimmed() ? p.host.trimmed()
: QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed()); : QStringLiteral("%1@%2").arg(username, p.host.trimmed());
args << target; args << target;
m_process->setProcessEnvironment(environment); m_process->setProcessEnvironment(environment);
+16
View File
@@ -6,6 +6,22 @@
# host, optionally as user@host, as the final argument). # host, optionally as user@host, as the final argument).
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
prompted-user@requireuser)
# Only the exact user@host below is accepted -- used to prove a
# username supplied via SessionConnectOptions (prompted for at
# connect time because the saved profile's own username was
# blank) actually reaches the ssh command line, not just that
# *some* connection to this host succeeds.
echo "Welcome to the fake host."
while IFS= read -r line; do
echo "$line"
done
exit 0
;;
*@requireuser|requireuser)
echo "Permission denied (publickey,password)." >&2
exit 255
;;
*@succeed|succeed) *@succeed|succeed)
echo "Welcome to the fake host." echo "Welcome to the fake host."
# Stay alive echoing stdin back (simulates an interactive # Stay alive echoing stdin back (simulates an interactive
+34 -2
View File
@@ -21,6 +21,13 @@ Profile makeProfile(const QString& fixtureHost)
return profile; return profile;
} }
Profile makeBlankUsernameProfile(const QString& fixtureHost)
{
Profile profile = makeProfile(fixtureHost);
profile.username.clear();
return profile;
}
SessionConnectOptions makeOptions() SessionConnectOptions makeOptions()
{ {
SessionConnectOptions options; SessionConnectOptions options;
@@ -50,10 +57,12 @@ private slots:
void connectionRefusedReachesFailedState(); void connectionRefusedReachesFailedState();
void sendInputEchoesThroughOutputReceived(); void sendInputEchoesThroughOutputReceived();
void reconnectRestartsAndReachesConnectedAgain(); void reconnectRestartsAndReachesConnectedAgain();
void connectOptionsUsernameReachesProcessWhenProfileUsernameIsBlank();
private: private:
QString fixturePath() const; QString fixturePath() const;
void createBackend(const QString& fixtureHost); void createBackend(const QString& fixtureHost);
void createBackend(const Profile& profile);
std::unique_ptr<SshSessionBackend> m_backend; std::unique_ptr<SshSessionBackend> m_backend;
SessionState m_lastState = SessionState::Disconnected; SessionState m_lastState = SessionState::Disconnected;
@@ -69,8 +78,12 @@ QString TestSshSessionBackend::fixturePath() const
void TestSshSessionBackend::createBackend(const QString& fixtureHost) void TestSshSessionBackend::createBackend(const QString& fixtureHost)
{ {
m_backend = createBackend(makeProfile(fixtureHost));
std::make_unique<SshSessionBackend>(makeProfile(fixtureHost), fixturePath(), nullptr); }
void TestSshSessionBackend::createBackend(const Profile& profile)
{
m_backend = std::make_unique<SshSessionBackend>(profile, fixturePath(), nullptr);
connect(m_backend.get(), connect(m_backend.get(),
&SessionBackend::stateChanged, &SessionBackend::stateChanged,
this, this,
@@ -232,5 +245,24 @@ void TestSshSessionBackend::reconnectRestartsAndReachesConnectedAgain()
QTRY_COMPARE(m_lastState, SessionState::Connected); QTRY_COMPARE(m_lastState, SessionState::Connected);
} }
void TestSshSessionBackend::connectOptionsUsernameReachesProcessWhenProfileUsernameIsBlank()
{
// Regression test for a bug where a username entered at the
// connect-time prompt (SessionTab::requestConnectOptions(), for a
// profile with no saved username -- issue #21) never actually reached
// the ssh process: SshSessionBackend built its target purely from
// profile().username, which is a separate copy captured when the
// backend was constructed and never sees SessionTab's later edit.
// fixtures/fake_ssh.sh's "requireuser" host only accepts the exact
// target "prompted-user@requireuser", so this fails unless
// SessionConnectOptions::username is actually used.
createBackend(makeBlankUsernameProfile(QStringLiteral("requireuser")));
SessionConnectOptions options = makeOptions();
options.username = QStringLiteral("prompted-user");
m_backend->connectSession(options);
QTRY_COMPARE(m_lastState, SessionState::Connected);
}
QTEST_GUILESS_MAIN(TestSshSessionBackend) QTEST_GUILESS_MAIN(TestSshSessionBackend)
#include "test_ssh_session_backend.moc" #include "test_ssh_session_backend.moc"