Files
orbithub/tests/test_rdp_session_backend.cpp
T
ksmithandClaude Sonnet 5 04ce6f7904 Add RdpSessionBackend test coverage (#1)
RdpSessionBackend can't reasonably get the same fixture-driven
state-machine tests SshSessionBackend got: it's driven by FreeRDP's own
event loop and a raw worker thread against a real freerdp_connect(), not
a QProcess we can point at a stand-in binary. What it does have is a
large amount of pure, regression-prone logic -- exactly the kind that
already caused a real historical bug here (the X11-keycode/PC-AT-scancode
mixup fixed in Milestone 7) -- so that's what gets covered instead.

Twelve functions promoted from free functions / private members to
public statics purely so tests can call them without a live connection:
security-mode/performance-profile normalization, the HiDPI scale-value
mapping, desktop-size clamping, both scancode-mapping functions, and the
five FreeRDP error-code interpretation functions. UINT32 is surfaced as
quint32 in the public signatures to keep FreeRDP/WinPR types out of the
header, matching how rdp_freerdp* is already only forward-declared there.

27 test cases, including a couple of direct regression guards: verifying
scancodeFromNativeScanCode() is a faithful passthrough to FreeRDP's X11
table (not a reimplementation), and that it does NOT reproduce the old
"X11 keycode treated as PC/AT scancode" bug for a documented example key.

This closes out #1's originally scoped work (CTest wiring, ProfileRepository,
SshSessionBackend, RdpSessionBackend coverage). Deeper state-machine
coverage for the two session backends remains future work if ever needed,
but isn't blocking here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:32:02 -06:00

282 lines
12 KiB
C++

#include "rdp_session_backend.h"
#include <QTest>
#include <freerdp/error.h>
#include <freerdp/locale/keyboard.h>
#include <freerdp/scancode.h>
class TestRdpSessionBackend : public QObject
{
Q_OBJECT
private slots:
void normalizedRdpSecurityModeRecognizesKnownValues();
void normalizedRdpSecurityModeFallsBackToNegotiate();
void normalizedRdpPerformanceProfileRecognizesKnownValues();
void normalizedRdpPerformanceProfileFallsBackToBalanced();
void nearestFreeRdpScaleValueMapsToLegalValues();
void sanitizeDesktopWidthClampsToLegalRange();
void sanitizeDesktopHeightClampsToLegalRange();
void scancodeFromNativeScanCodeHandlesZero();
#if defined(Q_OS_LINUX)
void scancodeFromNativeScanCodeDelegatesToX11TableOnLinux();
void scancodeFromNativeScanCodeDoesNotTreatX11KeycodeAsPcAtScancode();
#endif
void scancodeForQtKeyMapsDirectKeys();
void scancodeForQtKeyRespectsKeypadModifier();
void scancodeForQtKeyDisambiguatesLeftRightModifiers();
void scancodeForQtKeyReturnsUnknownForUnhandledKey();
void mapRdpErrorRecognizesAuthFailureCodes();
void mapRdpErrorRecognizesAccountStateCodes();
void mapRdpErrorRecognizesNetworkCodes();
void mapRdpErrorFallsBackForUnknownCode();
void isExpectedDisconnectCodeRecognizesBenignCodes();
void isExpectedDisconnectCodeRejectsAuthFailure();
void isExpectedConnectAbortCodeRecognizesCancellation();
void isExpectedConnectAbortCodeRejectsAuthFailure();
void disconnectMessageForCodeRecognizesKnownCodes();
void disconnectMessageForCodeFallsBackForUnknownCode();
void rdpErrorRawIncludesHexCode();
};
void TestRdpSessionBackend::normalizedRdpSecurityModeRecognizesKnownValues()
{
QCOMPARE(RdpSessionBackend::normalizedRdpSecurityMode(QStringLiteral("nla")),
QStringLiteral("NLA"));
QCOMPARE(RdpSessionBackend::normalizedRdpSecurityMode(QStringLiteral(" TLS ")),
QStringLiteral("TLS"));
QCOMPARE(RdpSessionBackend::normalizedRdpSecurityMode(QStringLiteral("Rdp")),
QStringLiteral("RDP"));
}
void TestRdpSessionBackend::normalizedRdpSecurityModeFallsBackToNegotiate()
{
QCOMPARE(RdpSessionBackend::normalizedRdpSecurityMode(QStringLiteral("bogus")),
QStringLiteral("Negotiate"));
QCOMPARE(RdpSessionBackend::normalizedRdpSecurityMode(QString()),
QStringLiteral("Negotiate"));
}
void TestRdpSessionBackend::normalizedRdpPerformanceProfileRecognizesKnownValues()
{
QCOMPARE(RdpSessionBackend::normalizedRdpPerformanceProfile(QStringLiteral("best quality")),
QStringLiteral("Best Quality"));
QCOMPARE(RdpSessionBackend::normalizedRdpPerformanceProfile(QStringLiteral(" Best Performance ")),
QStringLiteral("Best Performance"));
QCOMPARE(RdpSessionBackend::normalizedRdpPerformanceProfile(QStringLiteral("auto detect")),
QStringLiteral("Auto Detect"));
}
void TestRdpSessionBackend::normalizedRdpPerformanceProfileFallsBackToBalanced()
{
QCOMPARE(RdpSessionBackend::normalizedRdpPerformanceProfile(QStringLiteral("bogus")),
QStringLiteral("Balanced"));
}
void TestRdpSessionBackend::nearestFreeRdpScaleValueMapsToLegalValues()
{
// MS-RDPEDISP legally permits only {100, 140, 180} -- anything else is
// silently ignored by the server.
QCOMPARE(RdpSessionBackend::nearestFreeRdpScaleValue(1.0), quint32(100));
QCOMPARE(RdpSessionBackend::nearestFreeRdpScaleValue(1.2), quint32(100));
QCOMPARE(RdpSessionBackend::nearestFreeRdpScaleValue(1.25), quint32(140));
QCOMPARE(RdpSessionBackend::nearestFreeRdpScaleValue(1.6), quint32(140));
QCOMPARE(RdpSessionBackend::nearestFreeRdpScaleValue(2.0), quint32(180));
QCOMPARE(RdpSessionBackend::nearestFreeRdpScaleValue(3.0), quint32(180));
}
void TestRdpSessionBackend::sanitizeDesktopWidthClampsToLegalRange()
{
QCOMPARE(RdpSessionBackend::sanitizeDesktopWidth(0), 1280);
QCOMPARE(RdpSessionBackend::sanitizeDesktopWidth(-100), 1280);
QCOMPARE(RdpSessionBackend::sanitizeDesktopWidth(100), 640);
QCOMPARE(RdpSessionBackend::sanitizeDesktopWidth(1920), 1920);
QCOMPARE(RdpSessionBackend::sanitizeDesktopWidth(99999), 8192);
}
void TestRdpSessionBackend::sanitizeDesktopHeightClampsToLegalRange()
{
QCOMPARE(RdpSessionBackend::sanitizeDesktopHeight(0), 720);
QCOMPARE(RdpSessionBackend::sanitizeDesktopHeight(-100), 720);
QCOMPARE(RdpSessionBackend::sanitizeDesktopHeight(100), 360);
QCOMPARE(RdpSessionBackend::sanitizeDesktopHeight(1080), 1080);
QCOMPARE(RdpSessionBackend::sanitizeDesktopHeight(99999), 4320);
}
void TestRdpSessionBackend::scancodeFromNativeScanCodeHandlesZero()
{
QCOMPARE(RdpSessionBackend::scancodeFromNativeScanCode(0), quint32(RDP_SCANCODE_UNKNOWN));
}
#if defined(Q_OS_LINUX)
void TestRdpSessionBackend::scancodeFromNativeScanCodeDelegatesToX11TableOnLinux()
{
// Our wrapper must be a faithful passthrough to FreeRDP's own
// authoritative X11-keycode table, not a reimplementation of it.
const quint32 apostropheKeycode = 0x30;
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
const quint32 expected =
static_cast<quint32>(freerdp_keyboard_get_rdp_scancode_from_x11_keycode(apostropheKeycode));
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
QCOMPARE(RdpSessionBackend::scancodeFromNativeScanCode(apostropheKeycode), expected);
}
void TestRdpSessionBackend::scancodeFromNativeScanCodeDoesNotTreatX11KeycodeAsPcAtScancode()
{
// Regression guard for the historical bug this table replaced: X11
// keycode 0x30 (apostrophe/quote) must NOT resolve to whatever a naive
// "treat the X11 keycode as a PC/AT set-1 scancode" interpretation
// would give (PC/AT 0x30 is the B key).
const quint32 apostropheKeycode = 0x30;
const quint32 naivePcAtInterpretation = MAKE_RDP_SCANCODE(apostropheKeycode, FALSE);
QVERIFY(RdpSessionBackend::scancodeFromNativeScanCode(apostropheKeycode)
!= naivePcAtInterpretation);
}
#endif
void TestRdpSessionBackend::scancodeForQtKeyMapsDirectKeys()
{
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_Escape, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_ESCAPE));
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_A, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_KEY_A));
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_F1, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_F1));
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_Space, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_SPACE));
}
void TestRdpSessionBackend::scancodeForQtKeyRespectsKeypadModifier()
{
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_Insert, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_INSERT));
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_Insert, Qt::KeypadModifier, 0),
quint32(RDP_SCANCODE_NUMPAD0));
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_Delete, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_DELETE));
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_Delete, Qt::KeypadModifier, 0),
quint32(RDP_SCANCODE_DECIMAL));
}
void TestRdpSessionBackend::scancodeForQtKeyDisambiguatesLeftRightModifiers()
{
// With no reliable native scancode (0 -> RDP_SCANCODE_UNKNOWN, which
// matches neither side), both Shift and Control must default to their
// left variant rather than picking arbitrarily.
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_Shift, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_LSHIFT));
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_Control, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_LCONTROL));
}
void TestRdpSessionBackend::scancodeForQtKeyReturnsUnknownForUnhandledKey()
{
QCOMPARE(RdpSessionBackend::scancodeForQtKey(Qt::Key_MediaPlay, Qt::NoModifier, 0),
quint32(RDP_SCANCODE_UNKNOWN));
}
void TestRdpSessionBackend::mapRdpErrorRecognizesAuthFailureCodes()
{
const QString expected = QStringLiteral("Authentication failed. Check username and password.");
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_CONNECT_LOGON_FAILURE), expected);
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_CONNECT_WRONG_PASSWORD), expected);
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_CONNECT_ACCESS_DENIED), expected);
}
void TestRdpSessionBackend::mapRdpErrorRecognizesAccountStateCodes()
{
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED),
QStringLiteral("Account is disabled."));
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT),
QStringLiteral("Account is locked out."));
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED),
QStringLiteral("Account has expired."));
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED),
QStringLiteral("Password has expired."));
}
void TestRdpSessionBackend::mapRdpErrorRecognizesNetworkCodes()
{
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_DNS_NAME_NOT_FOUND),
QStringLiteral("Host could not be resolved."));
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_CONNECT_TRANSPORT_FAILED),
QStringLiteral("Network transport failed while connecting."));
QCOMPARE(RdpSessionBackend::mapRdpError(FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED),
QStringLiteral("RDP security negotiation failed. Try a different RDP security mode."));
}
void TestRdpSessionBackend::mapRdpErrorFallsBackForUnknownCode()
{
// Not a code mapRdpError special-cases; must still return something
// non-empty rather than crashing or returning an empty string.
const QString result = RdpSessionBackend::mapRdpError(0x7FFFFFFF);
QVERIFY(!result.isEmpty());
}
void TestRdpSessionBackend::isExpectedDisconnectCodeRecognizesBenignCodes()
{
QVERIFY(RdpSessionBackend::isExpectedDisconnectCode(FREERDP_ERROR_SUCCESS));
QVERIFY(RdpSessionBackend::isExpectedDisconnectCode(FREERDP_ERROR_NONE));
QVERIFY(RdpSessionBackend::isExpectedDisconnectCode(FREERDP_ERROR_LOGOFF_BY_USER));
QVERIFY(RdpSessionBackend::isExpectedDisconnectCode(FREERDP_ERROR_IDLE_TIMEOUT));
}
void TestRdpSessionBackend::isExpectedDisconnectCodeRejectsAuthFailure()
{
// An authentication failure must be treated as a real error, never as
// an expected/benign disconnect -- otherwise the user would see no
// error message at all for a failed login.
QVERIFY(!RdpSessionBackend::isExpectedDisconnectCode(FREERDP_ERROR_CONNECT_LOGON_FAILURE));
}
void TestRdpSessionBackend::isExpectedConnectAbortCodeRecognizesCancellation()
{
QVERIFY(RdpSessionBackend::isExpectedConnectAbortCode(FREERDP_ERROR_CONNECT_CANCELLED));
QVERIFY(RdpSessionBackend::isExpectedConnectAbortCode(FREERDP_ERROR_SUCCESS));
}
void TestRdpSessionBackend::isExpectedConnectAbortCodeRejectsAuthFailure()
{
QVERIFY(!RdpSessionBackend::isExpectedConnectAbortCode(FREERDP_ERROR_CONNECT_LOGON_FAILURE));
}
void TestRdpSessionBackend::disconnectMessageForCodeRecognizesKnownCodes()
{
QCOMPARE(RdpSessionBackend::disconnectMessageForCode(FREERDP_ERROR_IDLE_TIMEOUT),
QStringLiteral("RDP session disconnected due to idle timeout."));
QCOMPARE(RdpSessionBackend::disconnectMessageForCode(FREERDP_ERROR_LOGOFF_BY_USER),
QStringLiteral("RDP session signed out."));
QCOMPARE(RdpSessionBackend::disconnectMessageForCode(FREERDP_ERROR_CONNECT_CANCELLED),
QStringLiteral("Connection cancelled."));
}
void TestRdpSessionBackend::disconnectMessageForCodeFallsBackForUnknownCode()
{
QCOMPARE(RdpSessionBackend::disconnectMessageForCode(0x7FFFFFFF),
QStringLiteral("RDP session ended."));
}
void TestRdpSessionBackend::rdpErrorRawIncludesHexCode()
{
const QString result = RdpSessionBackend::rdpErrorRaw(FREERDP_ERROR_SUCCESS);
QVERIFY(result.contains(QStringLiteral("(0x00000000)")));
}
QTEST_GUILESS_MAIN(TestRdpSessionBackend)
#include "test_rdp_session_backend.moc"