From 04ce6f7904f97ca199a11e729134c310ea031839 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Tue, 15 Sep 2026 15:32:02 -0600 Subject: [PATCH] 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 --- src/rdp_session_backend.cpp | 120 ++++++------ src/rdp_session_backend.h | 20 +- tests/CMakeLists.txt | 23 +++ tests/test_rdp_session_backend.cpp | 281 +++++++++++++++++++++++++++++ 4 files changed, 383 insertions(+), 61 deletions(-) create mode 100644 tests/test_rdp_session_backend.cpp diff --git a/src/rdp_session_backend.cpp b/src/rdp_session_backend.cpp index 4784259..9da7980 100644 --- a/src/rdp_session_backend.cpp +++ b/src/rdp_session_backend.cpp @@ -89,59 +89,13 @@ constexpr double kDefaultDpi = 96.0; constexpr double kMillimetersPerInch = 25.4; #ifdef ORBITHUB_HAS_FREERDP -QString normalizedRdpSecurityMode(const QString& value) -{ - const QString mode = value.trimmed(); - if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) { - return QStringLiteral("NLA"); - } - if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) { - return QStringLiteral("TLS"); - } - if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) { - return QStringLiteral("RDP"); - } - return QStringLiteral("Negotiate"); -} - -// MS-RDPEDISP restricts DesktopScaleFactor/DeviceScaleFactor to exactly -// these three values; FreeRDP's own reference client enforces the same -// set (client/common/cmdline.c, parse_scale_options). Anything else is -// silently ignored by the server, so map the real, continuous -// devicePixelRatio down to the nearest one. -UINT32 nearestFreeRdpScaleValue(qreal ratio) -{ - if (ratio <= 1.2) { - return 100; - } - if (ratio <= 1.6) { - return 140; - } - return 180; -} - -QString normalizedRdpPerformanceProfile(const QString& value) -{ - const QString profile = value.trimmed(); - if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) { - return QStringLiteral("Best Quality"); - } - if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) { - return QStringLiteral("Best Performance"); - } - if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) { - return QStringLiteral("Auto Detect"); - } - return QStringLiteral("Balanced"); -} - bool applyRdpSecurityMode(rdpSettings* settings, const QString& mode) { if (settings == nullptr) { return false; } - const QString normalized = normalizedRdpSecurityMode(mode); + const QString normalized = RdpSessionBackend::normalizedRdpSecurityMode(mode); BOOL rdp = FALSE; BOOL tls = FALSE; @@ -175,7 +129,7 @@ bool applyRdpPerformanceProfile(rdpSettings* settings, const QString& profile) return false; } - const QString normalized = normalizedRdpPerformanceProfile(profile); + const QString normalized = RdpSessionBackend::normalizedRdpPerformanceProfile(profile); UINT32 connectionType = CONNECTION_TYPE_BROADBAND_HIGH; BOOL networkAutoDetect = FALSE; if (normalized == QStringLiteral("Best Quality")) { @@ -875,8 +829,55 @@ BOOL orbitAuthenticateEx(freerdp* instance, return TRUE; } +} -UINT32 scancodeFromNativeScanCode(quint32 nativeScanCode) +QString RdpSessionBackend::normalizedRdpSecurityMode(const QString& value) +{ + const QString mode = value.trimmed(); + if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) { + return QStringLiteral("NLA"); + } + if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) { + return QStringLiteral("TLS"); + } + if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) { + return QStringLiteral("RDP"); + } + return QStringLiteral("Negotiate"); +} + +// MS-RDPEDISP restricts DesktopScaleFactor/DeviceScaleFactor to exactly +// these three values; FreeRDP's own reference client enforces the same +// set (client/common/cmdline.c, parse_scale_options). Anything else is +// silently ignored by the server, so map the real, continuous +// devicePixelRatio down to the nearest one. +quint32 RdpSessionBackend::nearestFreeRdpScaleValue(qreal ratio) +{ + if (ratio <= 1.2) { + return 100; + } + if (ratio <= 1.6) { + return 140; + } + return 180; +} + +QString RdpSessionBackend::normalizedRdpPerformanceProfile(const QString& value) +{ + const QString profile = value.trimmed(); + if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) { + return QStringLiteral("Best Quality"); + } + if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) { + return QStringLiteral("Best Performance"); + } + if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) { + return QStringLiteral("Auto Detect"); + } + return QStringLiteral("Balanced"); +} + +quint32 RdpSessionBackend::scancodeFromNativeScanCode(quint32 nativeScanCode) { if (nativeScanCode == 0) { return RDP_SCANCODE_UNKNOWN; @@ -935,10 +936,12 @@ UINT32 scancodeFromNativeScanCode(quint32 nativeScanCode) #endif } -UINT32 scancodeForQtKey(int key, Qt::KeyboardModifiers modifiers, quint32 nativeScanCode) +quint32 RdpSessionBackend::scancodeForQtKey(int key, + Qt::KeyboardModifiers modifiers, + quint32 nativeScanCode) { const bool keypad = modifiers.testFlag(Qt::KeypadModifier); - const UINT32 nativeScancode = scancodeFromNativeScanCode(nativeScanCode); + const quint32 nativeScancode = scancodeFromNativeScanCode(nativeScanCode); switch (key) { case Qt::Key_Escape: @@ -1163,7 +1166,7 @@ UINT32 scancodeForQtKey(int key, Qt::KeyboardModifiers modifiers, quint32 native } } -QString mapRdpError(UINT32 code) +QString RdpSessionBackend::mapRdpError(quint32 code) { switch (code) { case FREERDP_ERROR_CONNECT_LOGON_FAILURE: @@ -1226,7 +1229,7 @@ QString mapRdpError(UINT32 code) return QStringLiteral("RDP connection failed (0x%1).").arg(code, 8, 16, QChar('0')); } -bool isExpectedDisconnectCode(UINT32 code) +bool RdpSessionBackend::isExpectedDisconnectCode(quint32 code) { switch (code) { case FREERDP_ERROR_SUCCESS: @@ -1256,7 +1259,7 @@ bool isExpectedDisconnectCode(UINT32 code) } } -bool isExpectedConnectAbortCode(UINT32 code) +bool RdpSessionBackend::isExpectedConnectAbortCode(quint32 code) { switch (code) { case FREERDP_ERROR_SUCCESS: @@ -1268,7 +1271,7 @@ bool isExpectedConnectAbortCode(UINT32 code) } } -QString disconnectMessageForCode(UINT32 code) +QString RdpSessionBackend::disconnectMessageForCode(quint32 code) { switch (code) { case FREERDP_ERROR_IDLE_TIMEOUT: @@ -1304,7 +1307,7 @@ QString disconnectMessageForCode(UINT32 code) } } -QString rdpErrorRaw(UINT32 code) +QString RdpSessionBackend::rdpErrorRaw(quint32 code) { const char* name = freerdp_get_last_error_name(code); const QString text = (name != nullptr && name[0] != '\0') ? QString::fromUtf8(name) @@ -1312,7 +1315,6 @@ QString rdpErrorRaw(UINT32 code) return QStringLiteral("%1 (0x%2)").arg(text).arg(code, 8, 16, QChar('0')); } #endif -} RdpSessionBackend::RdpSessionBackend(const Profile& profile, QObject* parent) : SessionBackend(profile, parent), @@ -2434,7 +2436,7 @@ void RdpSessionBackend::emitConnectionFailureAsync(const QString& displayMessage Qt::QueuedConnection); } -int RdpSessionBackend::sanitizeDesktopWidth(int width) const +int RdpSessionBackend::sanitizeDesktopWidth(int width) { if (width <= 0) { return kDefaultDesktopWidth; @@ -2442,7 +2444,7 @@ int RdpSessionBackend::sanitizeDesktopWidth(int width) const return qBound(kMinDesktopWidth, width, kMaxDesktopWidth); } -int RdpSessionBackend::sanitizeDesktopHeight(int height) const +int RdpSessionBackend::sanitizeDesktopHeight(int height) { if (height <= 0) { return kDefaultDesktopHeight; diff --git a/src/rdp_session_backend.h b/src/rdp_session_backend.h index 57688e3..301eac8 100644 --- a/src/rdp_session_backend.h +++ b/src/rdp_session_backend.h @@ -19,6 +19,24 @@ public: explicit RdpSessionBackend(const Profile& profile, QObject* parent = nullptr); ~RdpSessionBackend() override; + // Pure, state-free helpers exposed as public statics purely so tests + // can exercise them without a live FreeRDP connection. UINT32 values + // are surfaced as quint32 here to keep FreeRDP/WinPR types out of this + // header (uint32_t is what UINT32 always is on every platform this + // project targets). + static QString normalizedRdpSecurityMode(const QString& value); + static QString normalizedRdpPerformanceProfile(const QString& value); + static quint32 nearestFreeRdpScaleValue(qreal ratio); + static quint32 scancodeFromNativeScanCode(quint32 nativeScanCode); + static quint32 scancodeForQtKey(int key, Qt::KeyboardModifiers modifiers, quint32 nativeScanCode); + static QString mapRdpError(quint32 code); + static bool isExpectedDisconnectCode(quint32 code); + static bool isExpectedConnectAbortCode(quint32 code); + static QString disconnectMessageForCode(quint32 code); + static QString rdpErrorRaw(quint32 code); + static int sanitizeDesktopWidth(int width); + static int sanitizeDesktopHeight(int height); + public slots: void connectSession(const SessionConnectOptions& options) override; void disconnectSession() override; @@ -124,8 +142,6 @@ public: private: void emitStateAsync(SessionState state, const QString& message); void emitConnectionFailureAsync(const QString& displayMessage, const QString& rawMessage); - int sanitizeDesktopWidth(int width) const; - int sanitizeDesktopHeight(int height) const; }; #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9cd2943..96d2b3d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,3 +17,26 @@ target_compile_definitions(test_ssh_session_backend PRIVATE ORBITHUB_TEST_FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures" ) add_test(NAME test_ssh_session_backend COMMAND test_ssh_session_backend) + +if(TARGET freerdp AND TARGET winpr) + add_executable(test_rdp_session_backend + test_rdp_session_backend.cpp + ${CMAKE_SOURCE_DIR}/src/rdp_session_backend.cpp + ${CMAKE_SOURCE_DIR}/src/session_backend.h + ) + target_include_directories(test_rdp_session_backend PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/third_party/FreeRDP/include + ${CMAKE_SOURCE_DIR}/third_party/FreeRDP/winpr/include + ${CMAKE_BINARY_DIR}/third_party/FreeRDP/include + ${CMAKE_BINARY_DIR}/third_party/FreeRDP/winpr/include + ) + target_compile_definitions(test_rdp_session_backend PRIVATE ORBITHUB_HAS_FREERDP) + target_link_libraries(test_rdp_session_backend PRIVATE Qt6::Core Qt6::Gui Qt6::Test freerdp winpr) + if(TARGET freerdp-client) + target_link_libraries(test_rdp_session_backend PRIVATE freerdp-client) + endif() + add_test(NAME test_rdp_session_backend COMMAND test_rdp_session_backend) +else() + message(STATUS "FreeRDP targets not available -- skipping test_rdp_session_backend") +endif() diff --git a/tests/test_rdp_session_backend.cpp b/tests/test_rdp_session_backend.cpp new file mode 100644 index 0000000..49b3c79 --- /dev/null +++ b/tests/test_rdp_session_backend.cpp @@ -0,0 +1,281 @@ +#include "rdp_session_backend.h" + +#include + +#include +#include +#include + +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(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"