#include "profile_repository.h" #include #include #include 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(); void deleteEmptyFolderRemovesIt(); void deleteFolderMovesDirectProfilesToParent(); void deleteRootLevelFolderMovesProfilesToRoot(); void deleteFolderShiftsSubfoldersAndTheirProfilesUp(); void deleteFolderRejectsEmptyPath(); void deleteNonexistentFolderSucceedsAsNoOp(); private: std::unique_ptr m_tempDir; std::unique_ptr m_repo; }; void TestProfileRepository::init() { m_tempDir = std::make_unique(); QVERIFY(m_tempDir->isValid()); m_repo = std::make_unique(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 created = m_repo->createProfile(input); QVERIFY(created.has_value()); QVERIFY(created->id > 0); const std::optional 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 created = m_repo->createProfile(input); QVERIFY(created.has_value()); const std::optional 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 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 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 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 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 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 created = m_repo->createProfile(makeRdpProfile()); QVERIFY(created.has_value()); const std::optional 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")); } void TestProfileRepository::deleteEmptyFolderRemovesIt() { QVERIFY(m_repo->createFolder(QStringLiteral("Empty"))); QVERIFY(m_repo->deleteFolder(QStringLiteral("Empty"))); QCOMPARE(m_repo->listFolders().size(), size_t(0)); } void TestProfileRepository::deleteFolderMovesDirectProfilesToParent() { QVERIFY(m_repo->createFolder(QStringLiteral("Work"))); Profile profile = makeSshProfile(); profile.folderPath = QStringLiteral("Work"); const std::optional created = m_repo->createProfile(profile); QVERIFY(created.has_value()); QVERIFY(m_repo->deleteFolder(QStringLiteral("Work"))); QCOMPARE(m_repo->listFolders().size(), size_t(0)); const std::optional fetched = m_repo->getProfile(created->id); QVERIFY(fetched.has_value()); // Deleting a folder is never destructive to profiles -- they shift up // to take its place, here landing at the root since "Work" had no // parent of its own. QCOMPARE(fetched->folderPath, QString()); } void TestProfileRepository::deleteRootLevelFolderMovesProfilesToRoot() { Profile profile = makeRdpProfile(); profile.folderPath = QStringLiteral("Solo"); const std::optional created = m_repo->createProfile(profile); QVERIFY(created.has_value()); QVERIFY(m_repo->deleteFolder(QStringLiteral("Solo"))); const std::optional fetched = m_repo->getProfile(created->id); QVERIFY(fetched.has_value()); QCOMPARE(fetched->folderPath, QString()); } void TestProfileRepository::deleteFolderShiftsSubfoldersAndTheirProfilesUp() { QVERIFY(m_repo->createFolder(QStringLiteral("Work/Servers"))); Profile inTarget = makeSshProfile(QStringLiteral("InWork")); inTarget.folderPath = QStringLiteral("Work"); const std::optional createdInTarget = m_repo->createProfile(inTarget); QVERIFY(createdInTarget.has_value()); Profile inSubfolder = makeRdpProfile(QStringLiteral("InServers")); inSubfolder.folderPath = QStringLiteral("Work/Servers"); const std::optional createdInSubfolder = m_repo->createProfile(inSubfolder); QVERIFY(createdInSubfolder.has_value()); QVERIFY(m_repo->deleteFolder(QStringLiteral("Work"))); // "Work/Servers" shifts up to become root-level "Servers"; the profile // that was directly in "Work" moves to root; nothing is deleted. const auto folders = m_repo->listFolders(); QCOMPARE(folders.size(), size_t(1)); QCOMPARE(folders[0], QStringLiteral("Servers")); const std::optional fetchedInTarget = m_repo->getProfile(createdInTarget->id); QVERIFY(fetchedInTarget.has_value()); QCOMPARE(fetchedInTarget->folderPath, QString()); const std::optional fetchedInSubfolder = m_repo->getProfile(createdInSubfolder->id); QVERIFY(fetchedInSubfolder.has_value()); QCOMPARE(fetchedInSubfolder->folderPath, QStringLiteral("Servers")); } void TestProfileRepository::deleteFolderRejectsEmptyPath() { QVERIFY(!m_repo->deleteFolder(QString())); QVERIFY(!m_repo->lastError().isEmpty()); } void TestProfileRepository::deleteNonexistentFolderSucceedsAsNoOp() { QVERIFY(m_repo->deleteFolder(QStringLiteral("Never/Created"))); } QTEST_GUILESS_MAIN(TestProfileRepository) #include "test_profile_repository.moc"