Internal
Public Access
Parses mRemoteNG's confCons.xml export format directly -- schema verified against mRemoteNG's own XmlConnectionsDeserializer.cs source and a real exported sample, not guessed at. Maps nested <Node Type="Container"> folders and <Node Type="Connection"> entries onto OrbitHub profiles: RDP stays RDP, SSH1/SSH2 collapse to OrbitHub's single SSH protocol, anything else (VNC, Telnet, HTTP, PowerShell, ...) is skipped and listed in the import summary rather than silently dropped. Passwords are never read, not even for the common case where they're technically readable without a master password (mRemoteNG only encrypts the Password attribute itself, everything else -- Hostname, Username, Domain, Protocol -- is plaintext). A FullFileEncryption="true" export encrypts the whole node tree instead and genuinely can't be read without the user's master password; that case is detected and refused with a clear message rather than failing confusingly. The parser (src/mremoteng_importer.h/.cpp) is a pure function decoupled from any file/UI I/O, matching this session's established pattern of keeping business logic separately testable from the Qt Widgets shell that calls it (ProfilesWindow::importFromMRemoteNG() is the thin wrapper: QFileDialog, call the parser, write results via ProfileRepository, show a summary). 11 test cases against realistic sample XML. Hit a real moc gotcha along the way: a literal "//" inside a raw string literal (the xmlns URL) makes moc's lexer think a line comment started there, silently desyncing its parse so it never finds the QObject-derived test class at all (no error, just a missing vtable at link time). Fixed by moving the XML fixtures into a plain non-QObject header moc never scans, split across two adjacent literals as a second safeguard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
125 lines
5.0 KiB
C++
125 lines
5.0 KiB
C++
#include "mremoteng_importer.h"
|
|
|
|
#include "test_mremoteng_importer_fixtures.h"
|
|
|
|
#include <QTest>
|
|
|
|
#include <algorithm>
|
|
|
|
class TestMRemoteNGImporter : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void parsesFoldersAndProfilesFromRealisticSample();
|
|
void mapsRdpConnectionFieldsCorrectly();
|
|
void mapsSshConnectionFieldsCorrectly();
|
|
void skipsUnsupportedProtocolWithoutDroppingSilently();
|
|
void neverImportsPasswordField();
|
|
void refusesFullFileEncryptedExports();
|
|
void refusesUnrecognizedRootElement();
|
|
void refusesMalformedXml();
|
|
void refusesEmptyInput();
|
|
};
|
|
|
|
void TestMRemoteNGImporter::parsesFoldersAndProfilesFromRealisticSample()
|
|
{
|
|
const MRemoteNGImportResult result = parseMRemoteNGConnections(QByteArray(mRemoteNGSampleXml()));
|
|
QVERIFY(result.errorMessage.isEmpty());
|
|
|
|
QCOMPARE(result.folders.size(), size_t(2));
|
|
QCOMPARE(result.folders[0], QStringLiteral("Work"));
|
|
QCOMPARE(result.folders[1], QStringLiteral("Work/Servers"));
|
|
|
|
// 3 connection nodes in the sample: DC (RDP), build-box (SSH2),
|
|
// oldkiosk (VNC, unsupported) -- only the first two should come
|
|
// through as imported profiles.
|
|
QCOMPARE(result.profiles.size(), size_t(2));
|
|
QCOMPARE(result.skippedUnsupportedProtocol.size(), 1);
|
|
}
|
|
|
|
void TestMRemoteNGImporter::mapsRdpConnectionFieldsCorrectly()
|
|
{
|
|
const MRemoteNGImportResult result = parseMRemoteNGConnections(QByteArray(mRemoteNGSampleXml()));
|
|
const auto it = std::find_if(result.profiles.begin(), result.profiles.end(),
|
|
[](const MRemoteNGImportedProfile& p) {
|
|
return p.profile.name == QStringLiteral("DC");
|
|
});
|
|
QVERIFY(it != result.profiles.end());
|
|
QCOMPARE(it->profile.host, QStringLiteral("10.0.0.5"));
|
|
QCOMPARE(it->profile.port, 3389);
|
|
QCOMPARE(it->profile.username, QStringLiteral("Administrator"));
|
|
QCOMPARE(it->profile.protocol, QStringLiteral("RDP"));
|
|
QCOMPARE(it->profile.domain, QStringLiteral("CORP"));
|
|
QCOMPARE(it->folderPath, QStringLiteral("Work"));
|
|
}
|
|
|
|
void TestMRemoteNGImporter::mapsSshConnectionFieldsCorrectly()
|
|
{
|
|
const MRemoteNGImportResult result = parseMRemoteNGConnections(QByteArray(mRemoteNGSampleXml()));
|
|
const auto it = std::find_if(result.profiles.begin(), result.profiles.end(),
|
|
[](const MRemoteNGImportedProfile& p) {
|
|
return p.profile.name == QStringLiteral("build-box");
|
|
});
|
|
QVERIFY(it != result.profiles.end());
|
|
QCOMPARE(it->profile.host, QStringLiteral("build.internal"));
|
|
QCOMPARE(it->profile.port, 22);
|
|
QCOMPARE(it->profile.username, QStringLiteral("deploy"));
|
|
// SSH1/SSH2 both collapse to OrbitHub's single "SSH" protocol.
|
|
QCOMPARE(it->profile.protocol, QStringLiteral("SSH"));
|
|
QCOMPARE(it->profile.authMode, QStringLiteral("Password"));
|
|
// Domain is RDP-only; must not leak through for SSH.
|
|
QCOMPARE(it->profile.domain, QString());
|
|
QCOMPARE(it->folderPath, QStringLiteral("Work/Servers"));
|
|
}
|
|
|
|
void TestMRemoteNGImporter::skipsUnsupportedProtocolWithoutDroppingSilently()
|
|
{
|
|
const MRemoteNGImportResult result = parseMRemoteNGConnections(QByteArray(mRemoteNGSampleXml()));
|
|
QCOMPARE(result.skippedUnsupportedProtocol.size(), 1);
|
|
QVERIFY(result.skippedUnsupportedProtocol[0].contains(QStringLiteral("oldkiosk")));
|
|
QVERIFY(result.skippedUnsupportedProtocol[0].contains(QStringLiteral("VNC")));
|
|
}
|
|
|
|
void TestMRemoteNGImporter::neverImportsPasswordField()
|
|
{
|
|
// Profile has no password-storing field at all -- this test exists to
|
|
// document that guarantee, not to probe internals that don't exist.
|
|
const MRemoteNGImportResult result = parseMRemoteNGConnections(QByteArray(mRemoteNGSampleXml()));
|
|
QVERIFY(!result.profiles.empty());
|
|
for (const MRemoteNGImportedProfile& item : result.profiles) {
|
|
QCOMPARE(item.profile.authMode, QStringLiteral("Password"));
|
|
QVERIFY(item.profile.privateKeyPath.isEmpty());
|
|
}
|
|
}
|
|
|
|
void TestMRemoteNGImporter::refusesFullFileEncryptedExports()
|
|
{
|
|
const MRemoteNGImportResult result = parseMRemoteNGConnections(QByteArray(mRemoteNGFullFileEncryptedXml()));
|
|
QVERIFY(!result.errorMessage.isEmpty());
|
|
QVERIFY(result.errorMessage.contains(QStringLiteral("full file encryption"), Qt::CaseInsensitive));
|
|
QVERIFY(result.profiles.empty());
|
|
}
|
|
|
|
void TestMRemoteNGImporter::refusesUnrecognizedRootElement()
|
|
{
|
|
const MRemoteNGImportResult result = parseMRemoteNGConnections(QByteArray(mRemoteNGWrongRootXml()));
|
|
QVERIFY(!result.errorMessage.isEmpty());
|
|
}
|
|
|
|
void TestMRemoteNGImporter::refusesMalformedXml()
|
|
{
|
|
const MRemoteNGImportResult result =
|
|
parseMRemoteNGConnections(QByteArray("<mrng:Connections><Node "));
|
|
QVERIFY(!result.errorMessage.isEmpty());
|
|
}
|
|
|
|
void TestMRemoteNGImporter::refusesEmptyInput()
|
|
{
|
|
const MRemoteNGImportResult result = parseMRemoteNGConnections(QByteArray());
|
|
QVERIFY(!result.errorMessage.isEmpty());
|
|
}
|
|
|
|
QTEST_APPLESS_MAIN(TestMRemoteNGImporter)
|
|
#include "test_mremoteng_importer.moc"
|