Internal
Public Access
Wire up CTest and add unit test coverage for ProfileRepository
Adds a Qt6::Test-based unit test target (tests/test_profile_repository.cpp, 21 cases), gated behind an ORBITHUB_BUILD_TESTS option that no-ops gracefully if Qt6::Test isn't available, so it can't break app-only builds. Covers profile CRUD, validation rules, search/sort, tag normalization, and folder handling, each against an isolated temporary SQLite file (new ProfileRepository(databasePathOverride) constructor overload added for exactly this). Caught and fixed a real bug along the way: normalizedTags()'s result was bound directly without the nonNullTrimmed() null-guard every other field already uses, so creating a profile with no tags at all hit the `tags NOT NULL` constraint and silently failed -- including via the Import Profiles feature for any export where a profile has no tags key. Partial progress on #1 (RdpSessionBackend/SshSessionBackend state-machine coverage still open -- much larger lift, needs a testability pass on those backends first). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
#include "profile_repository.h"
|
||||
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
Profile makeSshProfile(const QString& name = QStringLiteral("Prod SSH Box"))
|
||||
{
|
||||
Profile profile;
|
||||
profile.name = name;
|
||||
profile.host = QStringLiteral("prod.example.com");
|
||||
profile.port = 22;
|
||||
profile.username = QStringLiteral("deploy");
|
||||
profile.protocol = QStringLiteral("SSH");
|
||||
profile.authMode = QStringLiteral("Password");
|
||||
profile.tags = QStringLiteral("prod,linux");
|
||||
return profile;
|
||||
}
|
||||
|
||||
Profile makeRdpProfile(const QString& name = QStringLiteral("Windows RDP Box"))
|
||||
{
|
||||
Profile profile;
|
||||
profile.name = name;
|
||||
profile.host = QStringLiteral("win.example.com");
|
||||
profile.port = 3389;
|
||||
profile.username = QStringLiteral("admin");
|
||||
profile.domain = QStringLiteral("CORP");
|
||||
profile.protocol = QStringLiteral("RDP");
|
||||
profile.rdpSecurityMode = QStringLiteral("NLA");
|
||||
profile.rdpPerformanceProfile = QStringLiteral("Best Performance");
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
class TestProfileRepository : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void init();
|
||||
void cleanup();
|
||||
|
||||
void initializesCleanly();
|
||||
void createAndGetSshProfile();
|
||||
void createAndGetRdpProfile();
|
||||
void createProfileRejectsMissingName();
|
||||
void createProfileRejectsMissingHost();
|
||||
void createProfileRejectsInvalidPort();
|
||||
void createProfileRejectsMissingUsernameForSsh();
|
||||
void createProfileRejectsMissingPrivateKeyForKeyAuth();
|
||||
void createProfileRejectsDuplicateName();
|
||||
void updateProfilePersistsChanges();
|
||||
void deleteProfileRemovesIt();
|
||||
void getProfileReturnsNulloptForUnknownId();
|
||||
void listProfilesFiltersBySearchQuery();
|
||||
void listProfilesSortsByRequestedOrder();
|
||||
void tagsAreTrimmedDedupedAndJoined();
|
||||
void emptyTagsRoundTripAsEmpty();
|
||||
void folderCreateAndListRoundTrips();
|
||||
void folderCreateIgnoresDuplicates();
|
||||
void folderPathIsNormalized();
|
||||
|
||||
private:
|
||||
std::unique_ptr<QTemporaryDir> m_tempDir;
|
||||
std::unique_ptr<ProfileRepository> m_repo;
|
||||
};
|
||||
|
||||
void TestProfileRepository::init()
|
||||
{
|
||||
m_tempDir = std::make_unique<QTemporaryDir>();
|
||||
QVERIFY(m_tempDir->isValid());
|
||||
m_repo = std::make_unique<ProfileRepository>(m_tempDir->filePath(QStringLiteral("test.sqlite")));
|
||||
}
|
||||
|
||||
void TestProfileRepository::cleanup()
|
||||
{
|
||||
m_repo.reset();
|
||||
m_tempDir.reset();
|
||||
}
|
||||
|
||||
void TestProfileRepository::initializesCleanly()
|
||||
{
|
||||
QCOMPARE(m_repo->initError(), QString());
|
||||
QCOMPARE(m_repo->listProfiles().size(), size_t(0));
|
||||
QCOMPARE(m_repo->listFolders().size(), size_t(0));
|
||||
}
|
||||
|
||||
void TestProfileRepository::createAndGetSshProfile()
|
||||
{
|
||||
const Profile input = makeSshProfile();
|
||||
const std::optional<Profile> created = m_repo->createProfile(input);
|
||||
QVERIFY(created.has_value());
|
||||
QVERIFY(created->id > 0);
|
||||
|
||||
const std::optional<Profile> fetched = m_repo->getProfile(created->id);
|
||||
QVERIFY(fetched.has_value());
|
||||
QCOMPARE(fetched->name, input.name);
|
||||
QCOMPARE(fetched->host, input.host);
|
||||
QCOMPARE(fetched->port, input.port);
|
||||
QCOMPARE(fetched->username, input.username);
|
||||
QCOMPARE(fetched->protocol, QStringLiteral("SSH"));
|
||||
QCOMPARE(fetched->authMode, QStringLiteral("Password"));
|
||||
QCOMPARE(fetched->tags, QStringLiteral("prod, linux"));
|
||||
}
|
||||
|
||||
void TestProfileRepository::createAndGetRdpProfile()
|
||||
{
|
||||
const Profile input = makeRdpProfile();
|
||||
const std::optional<Profile> created = m_repo->createProfile(input);
|
||||
QVERIFY(created.has_value());
|
||||
|
||||
const std::optional<Profile> fetched = m_repo->getProfile(created->id);
|
||||
QVERIFY(fetched.has_value());
|
||||
QCOMPARE(fetched->protocol, QStringLiteral("RDP"));
|
||||
QCOMPARE(fetched->domain, QStringLiteral("CORP"));
|
||||
QCOMPARE(fetched->rdpSecurityMode, QStringLiteral("NLA"));
|
||||
QCOMPARE(fetched->rdpPerformanceProfile, QStringLiteral("Best Performance"));
|
||||
QCOMPARE(fetched->port, 3389);
|
||||
// Auth-mode/private-key fields are SSH-only and must not leak through
|
||||
// for a non-SSH protocol.
|
||||
QCOMPARE(fetched->authMode, QStringLiteral("Password"));
|
||||
QCOMPARE(fetched->privateKeyPath, QString());
|
||||
}
|
||||
|
||||
void TestProfileRepository::createProfileRejectsMissingName()
|
||||
{
|
||||
Profile profile = makeSshProfile();
|
||||
profile.name.clear();
|
||||
QVERIFY(!m_repo->createProfile(profile).has_value());
|
||||
QVERIFY(!m_repo->lastError().isEmpty());
|
||||
}
|
||||
|
||||
void TestProfileRepository::createProfileRejectsMissingHost()
|
||||
{
|
||||
Profile profile = makeSshProfile();
|
||||
profile.host.clear();
|
||||
QVERIFY(!m_repo->createProfile(profile).has_value());
|
||||
QVERIFY(!m_repo->lastError().isEmpty());
|
||||
}
|
||||
|
||||
void TestProfileRepository::createProfileRejectsInvalidPort()
|
||||
{
|
||||
Profile profile = makeSshProfile();
|
||||
profile.port = 0;
|
||||
QVERIFY(!m_repo->createProfile(profile).has_value());
|
||||
|
||||
profile.port = 70000;
|
||||
QVERIFY(!m_repo->createProfile(profile).has_value());
|
||||
}
|
||||
|
||||
void TestProfileRepository::createProfileRejectsMissingUsernameForSsh()
|
||||
{
|
||||
Profile profile = makeSshProfile();
|
||||
profile.username.clear();
|
||||
QVERIFY(!m_repo->createProfile(profile).has_value());
|
||||
}
|
||||
|
||||
void TestProfileRepository::createProfileRejectsMissingPrivateKeyForKeyAuth()
|
||||
{
|
||||
Profile profile = makeSshProfile();
|
||||
profile.authMode = QStringLiteral("Private Key");
|
||||
profile.privateKeyPath.clear();
|
||||
QVERIFY(!m_repo->createProfile(profile).has_value());
|
||||
}
|
||||
|
||||
void TestProfileRepository::createProfileRejectsDuplicateName()
|
||||
{
|
||||
QVERIFY(m_repo->createProfile(makeSshProfile(QStringLiteral("Same Name"))).has_value());
|
||||
QVERIFY(!m_repo->createProfile(makeSshProfile(QStringLiteral("Same Name"))).has_value());
|
||||
}
|
||||
|
||||
void TestProfileRepository::updateProfilePersistsChanges()
|
||||
{
|
||||
const std::optional<Profile> created = m_repo->createProfile(makeSshProfile());
|
||||
QVERIFY(created.has_value());
|
||||
|
||||
Profile updated = created.value();
|
||||
updated.host = QStringLiteral("new-host.example.com");
|
||||
updated.port = 2222;
|
||||
updated.tags = QStringLiteral("updated");
|
||||
QVERIFY(m_repo->updateProfile(updated));
|
||||
|
||||
const std::optional<Profile> fetched = m_repo->getProfile(created->id);
|
||||
QVERIFY(fetched.has_value());
|
||||
QCOMPARE(fetched->host, QStringLiteral("new-host.example.com"));
|
||||
QCOMPARE(fetched->port, 2222);
|
||||
QCOMPARE(fetched->tags, QStringLiteral("updated"));
|
||||
}
|
||||
|
||||
void TestProfileRepository::deleteProfileRemovesIt()
|
||||
{
|
||||
const std::optional<Profile> created = m_repo->createProfile(makeSshProfile());
|
||||
QVERIFY(created.has_value());
|
||||
QVERIFY(m_repo->deleteProfile(created->id));
|
||||
QVERIFY(!m_repo->getProfile(created->id).has_value());
|
||||
}
|
||||
|
||||
void TestProfileRepository::getProfileReturnsNulloptForUnknownId()
|
||||
{
|
||||
QVERIFY(!m_repo->getProfile(999999).has_value());
|
||||
}
|
||||
|
||||
void TestProfileRepository::listProfilesFiltersBySearchQuery()
|
||||
{
|
||||
QVERIFY(m_repo->createProfile(makeSshProfile(QStringLiteral("Alpha"))).has_value());
|
||||
QVERIFY(m_repo->createProfile(makeRdpProfile(QStringLiteral("Beta"))).has_value());
|
||||
|
||||
const auto byName = m_repo->listProfiles(QStringLiteral("Alpha"));
|
||||
QCOMPARE(byName.size(), size_t(1));
|
||||
QCOMPARE(byName[0].name, QStringLiteral("Alpha"));
|
||||
|
||||
const auto byHost = m_repo->listProfiles(QStringLiteral("win.example"));
|
||||
QCOMPARE(byHost.size(), size_t(1));
|
||||
QCOMPARE(byHost[0].name, QStringLiteral("Beta"));
|
||||
|
||||
const auto byTag = m_repo->listProfiles(QStringLiteral("linux"));
|
||||
QCOMPARE(byTag.size(), size_t(1));
|
||||
QCOMPARE(byTag[0].name, QStringLiteral("Alpha"));
|
||||
|
||||
QCOMPARE(m_repo->listProfiles(QStringLiteral("nonexistent")).size(), size_t(0));
|
||||
}
|
||||
|
||||
void TestProfileRepository::listProfilesSortsByRequestedOrder()
|
||||
{
|
||||
QVERIFY(m_repo->createProfile(makeSshProfile(QStringLiteral("Zeta"))).has_value());
|
||||
QVERIFY(m_repo->createProfile(makeRdpProfile(QStringLiteral("Alpha"))).has_value());
|
||||
|
||||
const auto byName = m_repo->listProfiles(QString(), ProfileSortOrder::NameAsc);
|
||||
QCOMPARE(byName.size(), size_t(2));
|
||||
QCOMPARE(byName[0].name, QStringLiteral("Alpha"));
|
||||
QCOMPARE(byName[1].name, QStringLiteral("Zeta"));
|
||||
|
||||
const auto byProtocol = m_repo->listProfiles(QString(), ProfileSortOrder::ProtocolAsc);
|
||||
QCOMPARE(byProtocol[0].protocol, QStringLiteral("RDP"));
|
||||
QCOMPARE(byProtocol[1].protocol, QStringLiteral("SSH"));
|
||||
}
|
||||
|
||||
void TestProfileRepository::tagsAreTrimmedDedupedAndJoined()
|
||||
{
|
||||
Profile profile = makeSshProfile();
|
||||
profile.tags = QStringLiteral(" prod ,, Prod , linux ,linux");
|
||||
const std::optional<Profile> created = m_repo->createProfile(profile);
|
||||
QVERIFY(created.has_value());
|
||||
|
||||
// createProfile()'s return value echoes the input as-is; normalization
|
||||
// only happens on the DB round trip, so re-fetch to observe it.
|
||||
const std::optional<Profile> fetched = m_repo->getProfile(created->id);
|
||||
QVERIFY(fetched.has_value());
|
||||
// Case-insensitive de-dup keeps the first-seen casing of each tag.
|
||||
QCOMPARE(fetched->tags, QStringLiteral("prod, linux"));
|
||||
}
|
||||
|
||||
void TestProfileRepository::emptyTagsRoundTripAsEmpty()
|
||||
{
|
||||
const std::optional<Profile> created = m_repo->createProfile(makeRdpProfile());
|
||||
QVERIFY(created.has_value());
|
||||
const std::optional<Profile> fetched = m_repo->getProfile(created->id);
|
||||
QVERIFY(fetched.has_value());
|
||||
QCOMPARE(fetched->tags, QString());
|
||||
}
|
||||
|
||||
void TestProfileRepository::folderCreateAndListRoundTrips()
|
||||
{
|
||||
QVERIFY(m_repo->createFolder(QStringLiteral("Work/Servers")));
|
||||
const auto folders = m_repo->listFolders();
|
||||
QCOMPARE(folders.size(), size_t(1));
|
||||
QCOMPARE(folders[0], QStringLiteral("Work/Servers"));
|
||||
}
|
||||
|
||||
void TestProfileRepository::folderCreateIgnoresDuplicates()
|
||||
{
|
||||
QVERIFY(m_repo->createFolder(QStringLiteral("Work")));
|
||||
QVERIFY(m_repo->createFolder(QStringLiteral("Work")));
|
||||
QCOMPARE(m_repo->listFolders().size(), size_t(1));
|
||||
}
|
||||
|
||||
void TestProfileRepository::folderPathIsNormalized()
|
||||
{
|
||||
QVERIFY(m_repo->createFolder(QStringLiteral("\\Work\\\\Servers\\")));
|
||||
const auto folders = m_repo->listFolders();
|
||||
QCOMPARE(folders.size(), size_t(1));
|
||||
QCOMPARE(folders[0], QStringLiteral("Work/Servers"));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestProfileRepository)
|
||||
#include "test_profile_repository.moc"
|
||||
Reference in New Issue
Block a user