From 44366feda7dda98b693045ea6f3b5807de518144 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:08:36 +0000 Subject: [PATCH 1/4] Initial plan for issue From b60b2420c937f0d1c52f55b4334edb4ba907a540 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:08:44 +0000 Subject: [PATCH 2/4] Implement cross-database reference resolution and add test Co-authored-by: droidmonkey <2809491+droidmonkey@users.noreply.github.com> --- src/core/Entry.cpp | 29 +++++++++++++++++++++++++ src/core/Entry.h | 2 ++ tests/TestEntry.cpp | 52 +++++++++++++++++++++++++++++++++++++++++++++ tests/TestEntry.h | 1 + 4 files changed, 84 insertions(+) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 0147ec0f9..ddca7b4fe 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -1369,6 +1369,9 @@ void Entry::setGroup(Group* group, bool trackPrevious) setPreviousParentGroup(nullptr); m_group->database()->addDeletedObject(m_uuid); + // Resolve references before moving to a different database + resolveReferencesBeforeDatabaseMove(); + // copy custom icon to the new database if (!iconUuid().isNull() && group->database() && m_group->database()->metadata()->hasCustomIcon(iconUuid()) && !group->database()->metadata()->hasCustomIcon(iconUuid())) { @@ -1411,6 +1414,32 @@ Database* Entry::database() return nullptr; } +void Entry::resolveReferencesBeforeDatabaseMove() +{ + if (!m_group || !m_group->database()) { + return; + } + + // Resolve references in all default attributes + for (const QString& key : EntryAttributes::DefaultAttributes) { + if (m_attributes->contains(key) && m_attributes->isReference(key)) { + QString resolvedValue = resolveMultiplePlaceholdersRecursive(m_attributes->value(key), 10); + bool isProtected = m_attributes->isProtected(key); + m_attributes->set(key, resolvedValue, isProtected); + } + } + + // Resolve references in custom attributes + const QList customKeys = m_attributes->customKeys(); + for (const QString& key : customKeys) { + if (m_attributes->isReference(key)) { + QString resolvedValue = resolveMultiplePlaceholdersRecursive(m_attributes->value(key), 10); + bool isProtected = m_attributes->isProtected(key); + m_attributes->set(key, resolvedValue, isProtected); + } + } +} + QString Entry::maskPasswordPlaceholders(const QString& str) const { return QString{str}.replace(QStringLiteral("{PASSWORD}"), QStringLiteral("******"), Qt::CaseInsensitive); diff --git a/src/core/Entry.h b/src/core/Entry.h index 3c9a0f3de..3fe28e583 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -273,6 +273,8 @@ public: bool canUpdateTimeinfo() const; void setUpdateTimeinfo(bool value); + void resolveReferencesBeforeDatabaseMove(); + signals: /** * Emitted when a default attribute has been changed. diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 51cb4799c..493f75650 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -20,6 +20,9 @@ #include "TestEntry.h" #include "core/Clock.h" +#include "core/Database.h" +#include "core/Entry.h" +#include "core/EntryAttributes.h" #include "core/Group.h" #include "core/Metadata.h" #include "core/TimeInfo.h" @@ -672,6 +675,55 @@ void TestEntry::testResolveClonedEntry() QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->password()), original->password()); } +void TestEntry::testCrossDatabaseReferences() +{ + // Test that references are resolved when moving entries between databases + Database db1; + auto* root1 = db1.rootGroup(); + Database db2; + auto* root2 = db2.rootGroup(); + + // Create original entry in database 1 + auto* originalEntry = new Entry(); + originalEntry->setGroup(root1); + originalEntry->setUuid(QUuid::createUuid()); + originalEntry->setTitle("OriginalTitle"); + originalEntry->setUsername("OriginalUsername"); + originalEntry->setPassword("OriginalPassword"); + originalEntry->setUrl("http://original.com"); + + // Create entry with references to original entry in database 1 + auto* refEntry = new Entry(); + refEntry->setGroup(root1); + refEntry->setUuid(QUuid::createUuid()); + refEntry->setTitle(QString("{REF:T@I:%1}").arg(originalEntry->uuidToHex())); + refEntry->setUsername(QString("{REF:U@I:%1}").arg(originalEntry->uuidToHex())); + refEntry->setPassword(QString("{REF:P@I:%1}").arg(originalEntry->uuidToHex())); + refEntry->setUrl(QString("{REF:A@I:%1}").arg(originalEntry->uuidToHex())); + + // Verify references work within same database + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->title()), QString("OriginalTitle")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->username()), QString("OriginalUsername")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->password()), QString("OriginalPassword")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->url()), QString("http://original.com")); + + // Move the referenced entry to database 2 + // This should resolve the references before the move + refEntry->setGroup(root2); + + // After move, the entry should have resolved values instead of references + QCOMPARE(refEntry->title(), QString("OriginalTitle")); + QCOMPARE(refEntry->username(), QString("OriginalUsername")); + QCOMPARE(refEntry->password(), QString("OriginalPassword")); + QCOMPARE(refEntry->url(), QString("http://original.com")); + + // Verify that the references have been replaced with actual values + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::TitleKey)); + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::UserNameKey)); + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::PasswordKey)); + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::URLKey)); +} + void TestEntry::testIsRecycled() { auto entry = new Entry(); diff --git a/tests/TestEntry.h b/tests/TestEntry.h index 69f5b0d46..e1a4a6478 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -39,6 +39,7 @@ private slots: void testResolveConversionPlaceholders(); void testResolveReplacePlaceholders(); void testResolveClonedEntry(); + void testCrossDatabaseReferences(); void testIsRecycled(); void testMoveUpDown(); void testPreviousParentGroup(); From b0f90f370551c6bfe5a9786e7da31f96916b0f87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:10:53 +0000 Subject: [PATCH 3/4] Enhance cross-database reference test with comprehensive coverage Co-authored-by: droidmonkey <2809491+droidmonkey@users.noreply.github.com> --- tests/TestEntry.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 493f75650..c9b91a64a 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -691,6 +691,7 @@ void TestEntry::testCrossDatabaseReferences() originalEntry->setUsername("OriginalUsername"); originalEntry->setPassword("OriginalPassword"); originalEntry->setUrl("http://original.com"); + originalEntry->setNotes("OriginalNotes"); // Create entry with references to original entry in database 1 auto* refEntry = new Entry(); @@ -700,12 +701,26 @@ void TestEntry::testCrossDatabaseReferences() refEntry->setUsername(QString("{REF:U@I:%1}").arg(originalEntry->uuidToHex())); refEntry->setPassword(QString("{REF:P@I:%1}").arg(originalEntry->uuidToHex())); refEntry->setUrl(QString("{REF:A@I:%1}").arg(originalEntry->uuidToHex())); + refEntry->setNotes(QString("{REF:N@I:%1}").arg(originalEntry->uuidToHex())); + + // Add custom attribute with reference + refEntry->attributes()->set("CustomRef", QString("{REF:T@I:%1}").arg(originalEntry->uuidToHex())); // Verify references work within same database QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->title()), QString("OriginalTitle")); QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->username()), QString("OriginalUsername")); QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->password()), QString("OriginalPassword")); QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->url()), QString("http://original.com")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->notes()), QString("OriginalNotes")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->attributes()->value("CustomRef")), QString("OriginalTitle")); + + // Verify the attributes still contain references (not yet resolved) + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::TitleKey)); + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::UserNameKey)); + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::PasswordKey)); + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::URLKey)); + QVERIFY(refEntry->attributes()->isReference(EntryAttributes::NotesKey)); + QVERIFY(refEntry->attributes()->isReference("CustomRef")); // Move the referenced entry to database 2 // This should resolve the references before the move @@ -716,12 +731,26 @@ void TestEntry::testCrossDatabaseReferences() QCOMPARE(refEntry->username(), QString("OriginalUsername")); QCOMPARE(refEntry->password(), QString("OriginalPassword")); QCOMPARE(refEntry->url(), QString("http://original.com")); + QCOMPARE(refEntry->notes(), QString("OriginalNotes")); + QCOMPARE(refEntry->attributes()->value("CustomRef"), QString("OriginalTitle")); // Verify that the references have been replaced with actual values QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::TitleKey)); QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::UserNameKey)); QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::PasswordKey)); QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::URLKey)); + QVERIFY(!refEntry->attributes()->isReference(EntryAttributes::NotesKey)); + QVERIFY(!refEntry->attributes()->isReference("CustomRef")); + + // Test case where original entry doesn't exist (should keep the reference string) + auto* orphanEntry = new Entry(); + orphanEntry->setGroup(root1); + orphanEntry->setUuid(QUuid::createUuid()); + orphanEntry->setTitle("{REF:T@I:NONEXISTENTUUID}"); + + // Move orphan entry - the unresolvable reference should remain unchanged + orphanEntry->setGroup(root2); + QCOMPARE(orphanEntry->title(), QString("{REF:T@I:NONEXISTENTUUID}")); } void TestEntry::testIsRecycled() From af5261d093665ac33909f009f85347665b77b49f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:41:05 +0000 Subject: [PATCH 4/4] Fix cross-database reference resolution to handle unresolvable references correctly Co-authored-by: droidmonkey <2809491+droidmonkey@users.noreply.github.com> --- src/core/Entry.cpp | 24 ++++++++++++++++++------ tests/TestEntry.cpp | 7 ++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index ddca7b4fe..1c370a622 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -1423,9 +1423,15 @@ void Entry::resolveReferencesBeforeDatabaseMove() // Resolve references in all default attributes for (const QString& key : EntryAttributes::DefaultAttributes) { if (m_attributes->contains(key) && m_attributes->isReference(key)) { - QString resolvedValue = resolveMultiplePlaceholdersRecursive(m_attributes->value(key), 10); - bool isProtected = m_attributes->isProtected(key); - m_attributes->set(key, resolvedValue, isProtected); + QString originalValue = m_attributes->value(key); + QString resolvedValue = resolveMultiplePlaceholdersRecursive(originalValue, 10); + + // Only replace if the resolution produced a different value and it's not empty + // Empty resolution means the reference couldn't be resolved, so keep original + if (!resolvedValue.isEmpty() && resolvedValue != originalValue) { + bool isProtected = m_attributes->isProtected(key); + m_attributes->set(key, resolvedValue, isProtected); + } } } @@ -1433,9 +1439,15 @@ void Entry::resolveReferencesBeforeDatabaseMove() const QList customKeys = m_attributes->customKeys(); for (const QString& key : customKeys) { if (m_attributes->isReference(key)) { - QString resolvedValue = resolveMultiplePlaceholdersRecursive(m_attributes->value(key), 10); - bool isProtected = m_attributes->isProtected(key); - m_attributes->set(key, resolvedValue, isProtected); + QString originalValue = m_attributes->value(key); + QString resolvedValue = resolveMultiplePlaceholdersRecursive(originalValue, 10); + + // Only replace if the resolution produced a different value and it's not empty + // Empty resolution means the reference couldn't be resolved, so keep original + if (!resolvedValue.isEmpty() && resolvedValue != originalValue) { + bool isProtected = m_attributes->isProtected(key); + m_attributes->set(key, resolvedValue, isProtected); + } } } } diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index c9b91a64a..1f2e4e711 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -702,7 +702,7 @@ void TestEntry::testCrossDatabaseReferences() refEntry->setPassword(QString("{REF:P@I:%1}").arg(originalEntry->uuidToHex())); refEntry->setUrl(QString("{REF:A@I:%1}").arg(originalEntry->uuidToHex())); refEntry->setNotes(QString("{REF:N@I:%1}").arg(originalEntry->uuidToHex())); - + // Add custom attribute with reference refEntry->attributes()->set("CustomRef", QString("{REF:T@I:%1}").arg(originalEntry->uuidToHex())); @@ -712,7 +712,8 @@ void TestEntry::testCrossDatabaseReferences() QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->password()), QString("OriginalPassword")); QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->url()), QString("http://original.com")); QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->notes()), QString("OriginalNotes")); - QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->attributes()->value("CustomRef")), QString("OriginalTitle")); + QCOMPARE(refEntry->resolveMultiplePlaceholders(refEntry->attributes()->value("CustomRef")), + QString("OriginalTitle")); // Verify the attributes still contain references (not yet resolved) QVERIFY(refEntry->attributes()->isReference(EntryAttributes::TitleKey)); @@ -747,7 +748,7 @@ void TestEntry::testCrossDatabaseReferences() orphanEntry->setGroup(root1); orphanEntry->setUuid(QUuid::createUuid()); orphanEntry->setTitle("{REF:T@I:NONEXISTENTUUID}"); - + // Move orphan entry - the unresolvable reference should remain unchanged orphanEntry->setGroup(root2); QCOMPARE(orphanEntry->title(), QString("{REF:T@I:NONEXISTENTUUID}"));