Internal
Public Access
Add File -> Import from mRemoteNG...
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>
This commit is contained in:
@@ -98,6 +98,8 @@ set(ORBITHUB_SOURCES
|
||||
src/app_icon.cpp
|
||||
src/app_icon.h
|
||||
src/main.cpp
|
||||
src/mremoteng_importer.cpp
|
||||
src/mremoteng_importer.h
|
||||
src/profile_dialog.cpp
|
||||
src/profile_dialog.h
|
||||
src/profile_repository.cpp
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "mremoteng_importer.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
namespace {
|
||||
QString joinFolderPath(const QStringList& parts)
|
||||
{
|
||||
QStringList trimmed;
|
||||
for (const QString& part : parts) {
|
||||
const QString t = part.trimmed();
|
||||
if (!t.isEmpty()) {
|
||||
trimmed.push_back(t);
|
||||
}
|
||||
}
|
||||
return trimmed.join(QStringLiteral("/"));
|
||||
}
|
||||
|
||||
// Maps mRemoteNG's Protocol attribute (an enum: RDP, VNC, SSH1, SSH2,
|
||||
// Telnet, Rlogin, RAW, HTTP, HTTPS, PowerShell, IntApp, Winbox) onto what
|
||||
// OrbitHub actually supports. *supported is set to false for anything
|
||||
// OrbitHub can't yet connect to (VNC included -- see issue #3).
|
||||
QString mappedProtocol(const QString& mRemoteNGProtocol, bool* supported)
|
||||
{
|
||||
const QString p = mRemoteNGProtocol.trimmed();
|
||||
if (p.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
*supported = true;
|
||||
return QStringLiteral("RDP");
|
||||
}
|
||||
if (p.compare(QStringLiteral("SSH1"), Qt::CaseInsensitive) == 0
|
||||
|| p.compare(QStringLiteral("SSH2"), Qt::CaseInsensitive) == 0) {
|
||||
*supported = true;
|
||||
return QStringLiteral("SSH");
|
||||
}
|
||||
*supported = false;
|
||||
return p;
|
||||
}
|
||||
|
||||
int defaultPortFor(const QString& orbitHubProtocol)
|
||||
{
|
||||
return orbitHubProtocol == QStringLiteral("RDP") ? 3389 : 22;
|
||||
}
|
||||
}
|
||||
|
||||
MRemoteNGImportResult parseMRemoteNGConnections(const QByteArray& xmlData)
|
||||
{
|
||||
MRemoteNGImportResult result;
|
||||
|
||||
QXmlStreamReader reader(xmlData);
|
||||
|
||||
bool foundRoot = false;
|
||||
while (!reader.atEnd() && !foundRoot) {
|
||||
if (reader.readNext() == QXmlStreamReader::StartElement) {
|
||||
if (reader.name().compare(QStringLiteral("Connections"), Qt::CaseInsensitive) != 0) {
|
||||
result.errorMessage = QStringLiteral(
|
||||
"This doesn't look like an mRemoteNG connections file (expected a "
|
||||
"<Connections> root element).");
|
||||
return result;
|
||||
}
|
||||
foundRoot = true;
|
||||
|
||||
const auto attrs = reader.attributes();
|
||||
if (attrs.value(QStringLiteral("FullFileEncryption"))
|
||||
.compare(QStringLiteral("true"), Qt::CaseInsensitive)
|
||||
== 0) {
|
||||
result.errorMessage = QStringLiteral(
|
||||
"This file has \"full file encryption\" enabled in mRemoteNG, which "
|
||||
"encrypts the entire connection list with your master password. "
|
||||
"OrbitHub has no way to read that. In mRemoteNG, re-export with full "
|
||||
"file encryption turned off (only the saved passwords need stay "
|
||||
"encrypted -- OrbitHub never imports those anyway).");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundRoot) {
|
||||
result.errorMessage = QStringLiteral("Not a valid XML file, or the file is empty.");
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList folderStack;
|
||||
std::vector<bool> openNodeIsContainer;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
const QXmlStreamReader::TokenType token = reader.readNext();
|
||||
|
||||
if (token == QXmlStreamReader::StartElement
|
||||
&& reader.name().compare(QStringLiteral("Node"), Qt::CaseInsensitive) == 0) {
|
||||
const auto attrs = reader.attributes();
|
||||
const QString type = attrs.value(QStringLiteral("Type")).toString();
|
||||
const bool isContainer =
|
||||
type.compare(QStringLiteral("Container"), Qt::CaseInsensitive) == 0;
|
||||
const QString name = attrs.value(QStringLiteral("Name")).toString().trimmed();
|
||||
|
||||
if (isContainer) {
|
||||
folderStack.push_back(name);
|
||||
const QString path = joinFolderPath(folderStack);
|
||||
if (!path.isEmpty()) {
|
||||
result.folders.push_back(path);
|
||||
}
|
||||
} else {
|
||||
bool supported = false;
|
||||
const QString protocolLabel = attrs.value(QStringLiteral("Protocol")).toString();
|
||||
const QString mappedProto = mappedProtocol(protocolLabel, &supported);
|
||||
|
||||
if (!supported) {
|
||||
result.skippedUnsupportedProtocol.push_back(QStringLiteral("%1 (%2)").arg(
|
||||
name.isEmpty() ? QStringLiteral("(unnamed)") : name,
|
||||
protocolLabel.isEmpty() ? QStringLiteral("unknown") : protocolLabel));
|
||||
} else {
|
||||
MRemoteNGImportedProfile imported;
|
||||
imported.profile.name = name;
|
||||
imported.profile.host = attrs.value(QStringLiteral("Hostname")).toString().trimmed();
|
||||
const QString portText = attrs.value(QStringLiteral("Port")).toString();
|
||||
imported.profile.port =
|
||||
portText.isEmpty() ? defaultPortFor(mappedProto) : portText.toInt();
|
||||
if (imported.profile.port <= 0) {
|
||||
imported.profile.port = defaultPortFor(mappedProto);
|
||||
}
|
||||
imported.profile.username = attrs.value(QStringLiteral("Username")).toString();
|
||||
imported.profile.protocol = mappedProto;
|
||||
imported.profile.authMode = QStringLiteral("Password");
|
||||
if (mappedProto == QStringLiteral("RDP")) {
|
||||
imported.profile.domain = attrs.value(QStringLiteral("Domain")).toString();
|
||||
}
|
||||
imported.folderPath = joinFolderPath(folderStack);
|
||||
result.profiles.push_back(imported);
|
||||
}
|
||||
}
|
||||
|
||||
openNodeIsContainer.push_back(isContainer);
|
||||
} else if (token == QXmlStreamReader::EndElement
|
||||
&& reader.name().compare(QStringLiteral("Node"), Qt::CaseInsensitive) == 0) {
|
||||
if (!openNodeIsContainer.empty()) {
|
||||
if (openNodeIsContainer.back()) {
|
||||
folderStack.removeLast();
|
||||
}
|
||||
openNodeIsContainer.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.hasError()) {
|
||||
result.errorMessage = QStringLiteral("Failed to parse XML: %1").arg(reader.errorString());
|
||||
result.folders.clear();
|
||||
result.profiles.clear();
|
||||
result.skippedUnsupportedProtocol.clear();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef ORBITHUB_MREMOTENG_IMPORTER_H
|
||||
#define ORBITHUB_MREMOTENG_IMPORTER_H
|
||||
|
||||
#include "profile_repository.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <vector>
|
||||
|
||||
// A profile parsed out of an mRemoteNG confCons.xml export, plus which
|
||||
// folder (if any) it belongs to, expressed the same way ProfilesWindow's
|
||||
// own folder paths are ("Parent/Child").
|
||||
struct MRemoteNGImportedProfile
|
||||
{
|
||||
Profile profile;
|
||||
QString folderPath;
|
||||
};
|
||||
|
||||
struct MRemoteNGImportResult
|
||||
{
|
||||
// Non-empty only when parsing failed outright (malformed XML, or a
|
||||
// FullFileEncryption="true" export -- that encrypts the whole node
|
||||
// tree with the user's master password, which OrbitHub has no way to
|
||||
// ask for or use, so those files can't be read at all here).
|
||||
QString errorMessage;
|
||||
|
||||
// Every folder path that appeared, including ones with no directly
|
||||
// imported profile in them (e.g. a container that held only
|
||||
// unsupported-protocol connections).
|
||||
std::vector<QString> folders;
|
||||
|
||||
std::vector<MRemoteNGImportedProfile> profiles;
|
||||
|
||||
// "<name> (<mRemoteNG protocol>)" for each connection whose protocol
|
||||
// OrbitHub doesn't support (VNC, Telnet, HTTP, ...) -- skipped, never
|
||||
// silently dropped.
|
||||
QStringList skippedUnsupportedProtocol;
|
||||
};
|
||||
|
||||
// Passwords are never read from the file, even for the common case where
|
||||
// they're readable without a master password (mRemoteNG only encrypts the
|
||||
// Password attribute itself, not the surrounding plaintext fields) --
|
||||
// OrbitHub never persists credentials on a Profile in the first place.
|
||||
MRemoteNGImportResult parseMRemoteNGConnections(const QByteArray& xmlData);
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "profiles_window.h"
|
||||
|
||||
#include "mremoteng_importer.h"
|
||||
#include "profile_dialog.h"
|
||||
#include "profile_repository.h"
|
||||
#include "profiles_tree_widget.h"
|
||||
@@ -1183,3 +1184,72 @@ void ProfilesWindow::importProfiles()
|
||||
QMessageBox::information(this, QStringLiteral("Import Profiles"), summary);
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilesWindow::importFromMRemoteNG()
|
||||
{
|
||||
const QString fileName = QFileDialog::getOpenFileName(
|
||||
this, QStringLiteral("Import from mRemoteNG"), QString(),
|
||||
QStringLiteral("mRemoteNG Connections (*.xml)"));
|
||||
if (fileName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Import from mRemoteNG"),
|
||||
QStringLiteral("Failed to read %1: %2")
|
||||
.arg(fileName, file.errorString()));
|
||||
return;
|
||||
}
|
||||
const QByteArray xmlData = file.readAll();
|
||||
file.close();
|
||||
|
||||
const MRemoteNGImportResult parsed = parseMRemoteNGConnections(xmlData);
|
||||
if (!parsed.errorMessage.isEmpty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("Import from mRemoteNG"), parsed.errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QString& folderPath : parsed.folders) {
|
||||
m_repository->createFolder(folderPath);
|
||||
}
|
||||
|
||||
int imported = 0;
|
||||
QStringList failures;
|
||||
for (const MRemoteNGImportedProfile& item : parsed.profiles) {
|
||||
Profile profile = item.profile;
|
||||
profile.folderPath = item.folderPath;
|
||||
if (profile.name.isEmpty() || profile.host.isEmpty()) {
|
||||
failures.push_back(QStringLiteral("(unnamed connection): missing name or host"));
|
||||
continue;
|
||||
}
|
||||
if (m_repository->createProfile(profile).has_value()) {
|
||||
++imported;
|
||||
} else {
|
||||
failures.push_back(QStringLiteral("%1: %2").arg(profile.name, m_repository->lastError()));
|
||||
}
|
||||
}
|
||||
|
||||
loadProfiles();
|
||||
|
||||
QString summary = QStringLiteral("Imported %1 of %2 connection(s) from %3.\n\n"
|
||||
"Passwords are never imported -- you'll be prompted the "
|
||||
"first time you connect each profile, same as a new one.")
|
||||
.arg(imported)
|
||||
.arg(static_cast<int>(parsed.profiles.size()))
|
||||
.arg(fileName);
|
||||
if (!parsed.skippedUnsupportedProtocol.isEmpty()) {
|
||||
summary += QStringLiteral(
|
||||
"\n\nSkipped %1 connection(s) using a protocol OrbitHub doesn't support "
|
||||
"yet:\n%2")
|
||||
.arg(static_cast<int>(parsed.skippedUnsupportedProtocol.size()))
|
||||
.arg(parsed.skippedUnsupportedProtocol.join(QChar::fromLatin1('\n')));
|
||||
}
|
||||
if (!failures.isEmpty()) {
|
||||
summary += QStringLiteral("\n\nFailed:\n%1").arg(failures.join(QChar::fromLatin1('\n')));
|
||||
QMessageBox::warning(this, QStringLiteral("Import from mRemoteNG"), summary);
|
||||
} else {
|
||||
QMessageBox::information(this, QStringLiteral("Import from mRemoteNG"), summary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
void createFolderInCurrentContext();
|
||||
void exportProfiles();
|
||||
void importProfiles();
|
||||
void importFromMRemoteNG();
|
||||
|
||||
signals:
|
||||
void connectRequested(const Profile& profile);
|
||||
|
||||
@@ -172,6 +172,7 @@ SessionWindow::SessionWindow(QWidget* parent)
|
||||
fileMenu->addSeparator();
|
||||
QAction* importProfilesAction = fileMenu->addAction(QStringLiteral("Import Profiles..."));
|
||||
QAction* exportProfilesAction = fileMenu->addAction(QStringLiteral("Export Profiles..."));
|
||||
QAction* importMRemoteNGAction = fileMenu->addAction(QStringLiteral("Import from mRemoteNG..."));
|
||||
fileMenu->addSeparator();
|
||||
QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit"));
|
||||
|
||||
@@ -191,6 +192,10 @@ SessionWindow::SessionWindow(QWidget* parent)
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() { m_profilesWidget->exportProfiles(); });
|
||||
connect(importMRemoteNGAction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() { m_profilesWidget->importFromMRemoteNG(); });
|
||||
connect(quitAction, &QAction::triggered, this, []() { qApp->quit(); });
|
||||
|
||||
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
|
||||
|
||||
@@ -6,6 +6,14 @@ target_include_directories(test_profile_repository PRIVATE ${CMAKE_SOURCE_DIR}/s
|
||||
target_link_libraries(test_profile_repository PRIVATE Qt6::Core Qt6::Sql Qt6::Test)
|
||||
add_test(NAME test_profile_repository COMMAND test_profile_repository)
|
||||
|
||||
add_executable(test_mremoteng_importer
|
||||
test_mremoteng_importer.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/mremoteng_importer.cpp
|
||||
)
|
||||
target_include_directories(test_mremoteng_importer PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||
target_link_libraries(test_mremoteng_importer PRIVATE Qt6::Core Qt6::Test)
|
||||
add_test(NAME test_mremoteng_importer COMMAND test_mremoteng_importer)
|
||||
|
||||
add_executable(test_ssh_session_backend
|
||||
test_ssh_session_backend.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/ssh_session_backend.cpp
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#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"
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef ORBITHUB_TEST_MREMOTENG_IMPORTER_FIXTURES_H
|
||||
#define ORBITHUB_TEST_MREMOTENG_IMPORTER_FIXTURES_H
|
||||
|
||||
// Kept out of the moc-processed test .cpp: a literal "//" inside a raw
|
||||
// string literal (as in the xmlns URL below) confuses moc's lexer into
|
||||
// thinking a line comment started there, which silently desyncs the rest
|
||||
// of its parse and drops the QObject-derived test class entirely (no
|
||||
// error, just "No relevant classes found" and a missing vtable at link
|
||||
// time). Plain, non-QObject headers are never moc-scanned, so this is
|
||||
// immune to that.
|
||||
|
||||
// Modeled on a real mRemoteNG confCons.xml export (attribute names and
|
||||
// root-element shape verified against mRemoteNG's own
|
||||
// XmlConnectionsDeserializer.cs and a real exported sample), covering:
|
||||
// nested folders, an RDP connection, an SSH2 connection (protocol must
|
||||
// map to "SSH"), and a VNC connection (unsupported -- must be skipped,
|
||||
// not dropped silently).
|
||||
inline const char* mRemoteNGSampleXml()
|
||||
{
|
||||
return R"(<?xml version="1.0" encoding="utf-8"?>
|
||||
<mrng:Connections xmlns:mrng="http:)" R"(//mremoteng.org" Name="Connections" Export="false" EncryptionEngine="AES" BlockCipherMode="GCM" KdfIterations="1000" FullFileEncryption="false" Protected="" ConfVersion="2.6">
|
||||
<Node Name="Work" Type="Container" Descr="" Expanded="true">
|
||||
<Node Name="DC" Type="Connection" Descr="" Username="Administrator" Domain="CORP" Password="aEWNFV5uGcjUHF0uS17QTdT9kVqtKCPeoC0Nw5dmaPFjNQ2kt/zO5xDqE4HdVmHAowVRdC7emf7lWWA10dQKiw==" Hostname="10.0.0.5" Protocol="RDP" Port="3389" />
|
||||
<Node Name="Servers" Type="Container" Descr="" Expanded="true">
|
||||
<Node Name="build-box" Type="Connection" Descr="" Username="deploy" Domain="" Password="yhgmiu5bbuamU3qMUKc/uYDdmbMrJZ" Hostname="build.internal" Protocol="SSH2" Port="22" />
|
||||
</Node>
|
||||
</Node>
|
||||
<Node Name="oldkiosk" Type="Connection" Descr="" Username="" Domain="" Password="" Hostname="kiosk.internal" Protocol="VNC" Port="5900" />
|
||||
</mrng:Connections>
|
||||
)";
|
||||
}
|
||||
|
||||
inline const char* mRemoteNGFullFileEncryptedXml()
|
||||
{
|
||||
return R"(<?xml version="1.0" encoding="utf-8"?>
|
||||
<mrng:Connections xmlns:mrng="http:)" R"(//mremoteng.org" Name="Connections" Export="false" EncryptionEngine="AES" BlockCipherMode="GCM" KdfIterations="1000" FullFileEncryption="true" Protected="somehash" ConfVersion="2.6">SomeOpaqueBase64Blob==</mrng:Connections>
|
||||
)";
|
||||
}
|
||||
|
||||
inline const char* mRemoteNGWrongRootXml()
|
||||
{
|
||||
return R"(<?xml version="1.0" encoding="utf-8"?>
|
||||
<SomeOtherFormat/>
|
||||
)";
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user