Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAetf <aetf@unlimitedcodeworks.xyz>2019-12-17 00:49:58 +0300
committerAetf <aetf@unlimitedcodeworks.xyz>2020-05-28 05:07:25 +0300
commit44779bc862d9219ac3ab1b8e4537ec4fbf8e4239 (patch)
tree3055cbeca457a8a0a3dd1b2aeea334ba79ece859
parentaf6493b07b5ca9fd01da8324f589eaf3fc0dd000 (diff)
FdoSecrets: add unit tests
-rw-r--r--src/fdosecrets/FdoSecretsPlugin.cpp3
-rw-r--r--src/fdosecrets/objects/Collection.cpp15
-rw-r--r--src/fdosecrets/objects/Collection.h6
-rw-r--r--src/fdosecrets/objects/DBusObject.h10
-rw-r--r--src/fdosecrets/objects/DBusTypes.cpp10
-rw-r--r--src/fdosecrets/objects/Item.cpp24
-rw-r--r--src/fdosecrets/objects/Item.h8
-rw-r--r--src/fdosecrets/objects/Session.cpp4
-rw-r--r--src/fdosecrets/objects/SessionCipher.cpp33
-rw-r--r--src/fdosecrets/objects/SessionCipher.h14
-rw-r--r--src/fdosecrets/widgets/SettingsModels.cpp4
-rw-r--r--src/gui/DatabaseTabWidget.cpp2
-rw-r--r--src/gui/DatabaseTabWidget.h2
-rw-r--r--tests/CMakeLists.txt7
-rw-r--r--tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml33
-rw-r--r--tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml21
-rw-r--r--tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml11
-rw-r--r--tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml55
-rw-r--r--tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml4
-rw-r--r--tests/data/dbus/session.conf39
-rw-r--r--tests/gui/CMakeLists.txt9
-rw-r--r--tests/gui/TestGuiFdoSecrets.cpp1175
-rw-r--r--tests/gui/TestGuiFdoSecrets.h121
23 files changed, 1579 insertions, 31 deletions
diff --git a/src/fdosecrets/FdoSecretsPlugin.cpp b/src/fdosecrets/FdoSecretsPlugin.cpp
index 6c77e5dc6..9db35338d 100644
--- a/src/fdosecrets/FdoSecretsPlugin.cpp
+++ b/src/fdosecrets/FdoSecretsPlugin.cpp
@@ -31,7 +31,8 @@
using FdoSecrets::Service;
FdoSecretsPlugin::FdoSecretsPlugin(DatabaseTabWidget* tabWidget)
- : m_dbTabs(tabWidget)
+ : QObject(tabWidget)
+ , m_dbTabs(tabWidget)
{
FdoSecrets::registerDBusTypes();
}
diff --git a/src/fdosecrets/objects/Collection.cpp b/src/fdosecrets/objects/Collection.cpp
index c2fb29a08..3cac4815d 100644
--- a/src/fdosecrets/objects/Collection.cpp
+++ b/src/fdosecrets/objects/Collection.cpp
@@ -75,6 +75,8 @@ namespace FdoSecrets
m_registered = false;
}
+ Q_ASSERT(m_backend);
+
// make sure we have updated copy of the filepath, which is used to identify the database.
m_backendPath = m_backend->database()->filePath();
@@ -310,13 +312,13 @@ namespace FdoSecrets
QString itemPath;
StringStringMap attributes;
- // check existing item using attributes
auto iterAttr = properties.find(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes"));
if (iterAttr != properties.end()) {
- attributes = qdbus_cast<StringStringMap>(iterAttr.value().value<QDBusArgument>());
+ attributes = iterAttr.value().value<StringStringMap>();
itemPath = attributes.value(ItemAttributes::PathKey);
+ // check existing item using attributes
auto existings = searchItems(attributes);
if (existings.isError()) {
return existings;
@@ -614,6 +616,11 @@ namespace FdoSecrets
void Collection::doDelete()
{
+ if (!m_backend) {
+ // I'm already deleted
+ return;
+ }
+
emit collectionAboutToDelete();
unregisterCurrentPath();
@@ -623,7 +630,11 @@ namespace FdoSecrets
removeAlias(a).okOrDie();
}
+ // cleanup connection on Database
cleanupConnections();
+ // cleanup connection on Backend itself
+ m_backend->disconnect(this);
+ parent()->disconnect(this);
m_exposedGroup = nullptr;
diff --git a/src/fdosecrets/objects/Collection.h b/src/fdosecrets/objects/Collection.h
index 0be8b4239..6098880fc 100644
--- a/src/fdosecrets/objects/Collection.h
+++ b/src/fdosecrets/objects/Collection.h
@@ -59,9 +59,9 @@ namespace FdoSecrets
createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt);
signals:
- void itemCreated(const Item* item);
- void itemDeleted(const Item* item);
- void itemChanged(const Item* item);
+ void itemCreated(Item* item);
+ void itemDeleted(Item* item);
+ void itemChanged(Item* item);
void collectionChanged();
void collectionAboutToDelete();
diff --git a/src/fdosecrets/objects/DBusObject.h b/src/fdosecrets/objects/DBusObject.h
index 539a2dfd7..4cdaf5ced 100644
--- a/src/fdosecrets/objects/DBusObject.h
+++ b/src/fdosecrets/objects/DBusObject.h
@@ -51,6 +51,11 @@ namespace FdoSecrets
return m_objectPath;
}
+ QDBusAbstractAdaptor& dbusAdaptor() const
+ {
+ return *m_dbusAdaptor;
+ }
+
protected:
void registerWithPath(const QString& path, QDBusAbstractAdaptor* adaptor);
@@ -74,11 +79,6 @@ namespace FdoSecrets
QString callingPeerName() const;
- template <typename Adaptor> Adaptor& dbusAdaptor() const
- {
- return *static_cast<Adaptor*>(m_dbusAdaptor);
- }
-
DBusObject* p() const
{
return qobject_cast<DBusObject*>(parent());
diff --git a/src/fdosecrets/objects/DBusTypes.cpp b/src/fdosecrets/objects/DBusTypes.cpp
index fff25124e..c249eaeed 100644
--- a/src/fdosecrets/objects/DBusTypes.cpp
+++ b/src/fdosecrets/objects/DBusTypes.cpp
@@ -35,6 +35,16 @@ namespace FdoSecrets
qRegisterMetaType<ObjectPathSecretMap>();
qDBusRegisterMetaType<ObjectPathSecretMap>();
+ QMetaType::registerConverter<QDBusArgument, StringStringMap>([](const QDBusArgument& arg) {
+ if (arg.currentSignature() != "a{ss}") {
+ return StringStringMap{};
+ }
+ // QDBusArgument is COW and qdbus_cast modifies it by detaching even it is const.
+ // we don't want to modify the instance (arg) stored in the qvariant so we create a copy
+ const auto copy = arg; // NOLINT(performance-unnecessary-copy-initialization)
+ return qdbus_cast<StringStringMap>(copy);
+ });
+
// NOTE: this is already registered by Qt in qtextratypes.h
// qRegisterMetaType<QList<QDBusObjectPath > >();
// qDBusRegisterMetaType<QList<QDBusObjectPath> >();
diff --git a/src/fdosecrets/objects/Item.cpp b/src/fdosecrets/objects/Item.cpp
index f3b8bceb7..d58cbcc2c 100644
--- a/src/fdosecrets/objects/Item.cpp
+++ b/src/fdosecrets/objects/Item.cpp
@@ -26,6 +26,7 @@
#include "core/Entry.h"
#include "core/EntryAttributes.h"
#include "core/Group.h"
+#include "core/Metadata.h"
#include "core/Tools.h"
#include <QMimeDatabase>
@@ -274,8 +275,8 @@ namespace FdoSecrets
return ret;
}
- auto attributes = qdbus_cast<StringStringMap>(
- properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value<QDBusArgument>());
+ auto attributes =
+ properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value<StringStringMap>();
ret = setAttributes(attributes);
if (ret.isError()) {
return ret;
@@ -350,6 +351,13 @@ namespace FdoSecrets
return pathComponents.join('/');
}
+ bool Item::isDeletePermanent() const
+ {
+ auto recycleBin = backend()->database()->metadata()->recycleBin();
+ return (recycleBin && recycleBin->findEntryByUuid(backend()->uuid()))
+ || !backend()->database()->metadata()->recycleBinEnabled();
+ }
+
void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType)
{
auto mimeName = contentType.split(';').takeFirst().trimmed();
@@ -369,7 +377,8 @@ namespace FdoSecrets
}
if (!mimeType.isValid() || !mimeType.inherits(QStringLiteral("text/plain")) || !codec) {
- // we can't handle this content type, save the data as attachment
+ // we can't handle this content type, save the data as attachment, and clear the password field
+ entry->setPassword("");
entry->attachments()->set(FDO_SECRETS_DATA, data);
entry->attributes()->set(FDO_SECRETS_CONTENT_TYPE, contentType);
return;
@@ -393,8 +402,13 @@ namespace FdoSecrets
if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) {
ss.value = entry->attachments()->value(FDO_SECRETS_DATA);
- Q_ASSERT(entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE));
- ss.contentType = entry->attributes()->value(FDO_SECRETS_CONTENT_TYPE);
+ if (entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE)) {
+ ss.contentType = entry->attributes()->value(FDO_SECRETS_CONTENT_TYPE);
+ } else {
+ // the entry is somehow corrupted, maybe the user deleted it.
+ // set to binary and hope for the best...
+ ss.contentType = QStringLiteral("application/octet-stream");
+ }
return ss;
}
diff --git a/src/fdosecrets/objects/Item.h b/src/fdosecrets/objects/Item.h
index 39e83de74..99601d950 100644
--- a/src/fdosecrets/objects/Item.h
+++ b/src/fdosecrets/objects/Item.h
@@ -79,6 +79,14 @@ namespace FdoSecrets
*/
QString path() const;
+ /**
+ * If the containing db does not have recycle bin enabled,
+ * or the entry is already in the recycle bin (not possible for item, though),
+ * the delete is permanent
+ * @return true if delete is permanent
+ */
+ bool isDeletePermanent() const;
+
public slots:
void doDelete();
diff --git a/src/fdosecrets/objects/Session.cpp b/src/fdosecrets/objects/Session.cpp
index ad9ac4075..4845752a1 100644
--- a/src/fdosecrets/objects/Session.cpp
+++ b/src/fdosecrets/objects/Session.cpp
@@ -68,9 +68,9 @@ namespace FdoSecrets
incomplete = false;
std::unique_ptr<CipherPair> cipher{};
- if (algorithm == QLatin1Literal("plain")) {
+ if (algorithm == QLatin1String(PlainCipher::Algorithm)) {
cipher.reset(new PlainCipher);
- } else if (algorithm == QLatin1Literal("dh-ietf1024-sha256-aes128-cbc-pkcs7")) {
+ } else if (algorithm == QLatin1String(DhIetf1024Sha256Aes128CbcPkcs7::Algorithm)) {
QByteArray clientPublicKey = input.toByteArray();
cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(clientPublicKey));
} else {
diff --git a/src/fdosecrets/objects/SessionCipher.cpp b/src/fdosecrets/objects/SessionCipher.cpp
index dcdbc1932..26f080c3b 100644
--- a/src/fdosecrets/objects/SessionCipher.cpp
+++ b/src/fdosecrets/objects/SessionCipher.cpp
@@ -43,6 +43,9 @@ namespace
namespace FdoSecrets
{
+ // XXX: remove the redundant definitions once we are at C++17
+ constexpr char PlainCipher::Algorithm[];
+ constexpr char DhIetf1024Sha256Aes128CbcPkcs7::Algorithm[];
DhIetf1024Sha256Aes128CbcPkcs7::DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes)
: m_valid(false)
@@ -51,13 +54,24 @@ namespace FdoSecrets
auto clientPub = MpiFromBytes(clientPublicKeyBytes, false);
// generate server side private, 128 bytes
- GcryptMPI serverPrivate(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
- gcry_mpi_randomize(serverPrivate.get(), KEY_SIZE_BYTES * 8, GCRY_STRONG_RANDOM);
+ GcryptMPI serverPrivate = nullptr;
+ if (NextPrivKey) {
+ serverPrivate = std::move(NextPrivKey);
+ } else {
+ serverPrivate.reset(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
+ gcry_mpi_randomize(serverPrivate.get(), KEY_SIZE_BYTES * 8, GCRY_STRONG_RANDOM);
+ }
// generate server side public key
- GcryptMPI serverPublic(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
- // the generator of Second Oakley Group is 2
- gcry_mpi_powm(serverPublic.get(), GCRYMPI_CONST_TWO, serverPrivate.get(), IETF1024_SECOND_OAKLEY_GROUP_P.get());
+ GcryptMPI serverPublic = nullptr;
+ if (NextPubKey) {
+ serverPublic = std::move(NextPubKey);
+ } else {
+ serverPublic.reset(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
+ // the generator of Second Oakley Group is 2
+ gcry_mpi_powm(
+ serverPublic.get(), GCRYMPI_CONST_TWO, serverPrivate.get(), IETF1024_SECOND_OAKLEY_GROUP_P.get());
+ }
initialize(std::move(clientPub), std::move(serverPublic), std::move(serverPrivate));
}
@@ -216,4 +230,13 @@ namespace FdoSecrets
return m_publicKey;
}
+ void DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(GcryptMPI priv, GcryptMPI pub)
+ {
+ NextPrivKey = std::move(priv);
+ NextPubKey = std::move(pub);
+ }
+
+ GcryptMPI DhIetf1024Sha256Aes128CbcPkcs7::NextPrivKey = nullptr;
+ GcryptMPI DhIetf1024Sha256Aes128CbcPkcs7::NextPubKey = nullptr;
+
} // namespace FdoSecrets
diff --git a/src/fdosecrets/objects/SessionCipher.h b/src/fdosecrets/objects/SessionCipher.h
index 124636fdd..4d656c0af 100644
--- a/src/fdosecrets/objects/SessionCipher.h
+++ b/src/fdosecrets/objects/SessionCipher.h
@@ -22,6 +22,7 @@
#include "fdosecrets/objects/Session.h"
class TestFdoSecrets;
+class TestGuiFdoSecrets;
namespace FdoSecrets
{
@@ -42,6 +43,8 @@ namespace FdoSecrets
{
Q_DISABLE_COPY(PlainCipher)
public:
+ static constexpr const char Algorithm[] = "plain";
+
PlainCipher() = default;
SecretStruct encrypt(const SecretStruct& input) override
{
@@ -113,6 +116,8 @@ namespace FdoSecrets
}
public:
+ static constexpr const char Algorithm[] = "dh-ietf1024-sha256-aes128-cbc-pkcs7";
+
explicit DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes);
SecretStruct encrypt(const SecretStruct& input) override;
@@ -124,8 +129,17 @@ namespace FdoSecrets
QVariant negotiationOutput() const override;
private:
+ /**
+ * For test only, fix the server side private and public key.
+ */
+ static void fixNextServerKeys(GcryptMPI priv, GcryptMPI pub);
+ static GcryptMPI NextPrivKey;
+ static GcryptMPI NextPubKey;
+
+ private:
Q_DISABLE_COPY(DhIetf1024Sha256Aes128CbcPkcs7);
friend class ::TestFdoSecrets;
+ friend class ::TestGuiFdoSecrets;
};
} // namespace FdoSecrets
diff --git a/src/fdosecrets/widgets/SettingsModels.cpp b/src/fdosecrets/widgets/SettingsModels.cpp
index a8e513e29..3337ad8c5 100644
--- a/src/fdosecrets/widgets/SettingsModels.cpp
+++ b/src/fdosecrets/widgets/SettingsModels.cpp
@@ -335,9 +335,7 @@ namespace FdoSecrets
{
switch (role) {
case Qt::EditRole: {
- auto v = QVariant::fromValue(sess);
- qDebug() << v << v.type() << v.userType();
- return v;
+ return QVariant::fromValue(sess);
}
default:
return {};
diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
index 49bfc4580..7408c4900 100644
--- a/src/gui/DatabaseTabWidget.cpp
+++ b/src/gui/DatabaseTabWidget.cpp
@@ -49,7 +49,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
: QTabWidget(parent)
, m_dbWidgetStateSync(new DatabaseWidgetStateSync(this))
, m_dbWidgetPendingLock(nullptr)
- , m_databaseOpenDialog(new DatabaseOpenDialog())
+ , m_databaseOpenDialog(new DatabaseOpenDialog(this))
{
auto* tabBar = new DragTabBar(this);
setTabBar(tabBar);
diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h
index 64d154350..89086e5ec 100644
--- a/src/gui/DatabaseTabWidget.h
+++ b/src/gui/DatabaseTabWidget.h
@@ -108,7 +108,7 @@ private:
QPointer<DatabaseWidgetStateSync> m_dbWidgetStateSync;
QPointer<DatabaseWidget> m_dbWidgetPendingLock;
- QScopedPointer<DatabaseOpenDialog> m_databaseOpenDialog;
+ QPointer<DatabaseOpenDialog> m_databaseOpenDialog;
};
#endif // KEEPASSX_DATABASETABWIDGET_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e1bfaac13..28d8516cf 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -57,8 +57,9 @@ macro(parse_arguments prefix arg_names option_names)
endmacro(parse_arguments)
macro(add_unit_test)
- parse_arguments(TEST "NAME;SOURCES;LIBS" "" ${ARGN})
+ parse_arguments(TEST "NAME;SOURCES;LIBS;LAUNCHER" "" ${ARGN})
set(_test_NAME ${TEST_NAME})
+ set(_test_LAUNCHER ${TEST_LAUNCHER})
set(_srcList ${TEST_SOURCES})
add_executable(${_test_NAME} ${_srcList})
target_link_libraries(${_test_NAME} ${TEST_LIBS})
@@ -69,9 +70,9 @@ macro(add_unit_test)
set(TEST_OUTPUT ${TEST_OUTPUT} CACHE STRING "The output to generate when running the QTest unit tests")
if(KDE4_TEST_OUTPUT STREQUAL "xml")
- add_test(${_test_NAME} ${_test_NAME} -xml -o ${_test_NAME}.tml)
+ add_test(${_test_NAME} ${_test_LAUNCHER} ${_test_NAME} -xml -o ${_test_NAME}.tml)
else(KDE4_TEST_OUTPUT STREQUAL "xml")
- add_test(${_test_NAME} ${_test_NAME})
+ add_test(${_test_NAME} ${_test_LAUNCHER} ${_test_NAME})
endif(KDE4_TEST_OUTPUT STREQUAL "xml")
if(NOT MSVC_IDE) #not needed for the ide
diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml
new file mode 100644
index 000000000..3b5dd64fd
--- /dev/null
+++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml
@@ -0,0 +1,33 @@
+<interface name="org.freedesktop.Secret.Collection">
+ <property name="Items" type="ao" access="read"/>
+ <property name="Label" type="s" access="readwrite"/>
+ <property name="Locked" type="b" access="read"/>
+ <property name="Created" type="t" access="read"/>
+ <property name="Modified" type="t" access="read"/>
+ <signal name="ItemCreated">
+ <arg name="item" type="o" direction="out"/>
+ </signal>
+ <signal name="ItemDeleted">
+ <arg name="item" type="o" direction="out"/>
+ </signal>
+ <signal name="ItemChanged">
+ <arg name="item" type="o" direction="out"/>
+ </signal>
+ <method name="Delete">
+ <arg type="o" direction="out"/>
+ </method>
+ <method name="SearchItems">
+ <arg type="ao" direction="out"/>
+ <arg name="attributes" type="a{ss}" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="StringStringMap"/>
+ </method>
+ <method name="CreateItem">
+ <arg type="o" direction="out"/>
+ <arg name="properties" type="a{sv}" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
+ <arg name="secret" type="(oayays)" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="FdoSecrets::SecretStruct"/>
+ <arg name="replace" type="b" direction="in"/>
+ <arg name="prompt" type="o" direction="out"/>
+ </method>
+</interface>
diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml
new file mode 100644
index 000000000..d9c39a2e9
--- /dev/null
+++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml
@@ -0,0 +1,21 @@
+<interface name="org.freedesktop.Secret.Item">
+ <property name="Locked" type="b" access="read"/>
+ <property name="Attributes" type="a{ss}" access="readwrite">
+ <annotation name="org.qtproject.QtDBus.QtTypeName" value="StringStringMap"/>
+ </property>
+ <property name="Label" type="s" access="readwrite"/>
+ <property name="Created" type="t" access="read"/>
+ <property name="Modified" type="t" access="read"/>
+ <method name="Delete">
+ <arg type="o" direction="out"/>
+ </method>
+ <method name="GetSecret">
+ <arg type="(oayays)" direction="out"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="FdoSecrets::SecretStruct"/>
+ <arg name="session" type="o" direction="in"/>
+ </method>
+ <method name="SetSecret">
+ <arg name="secret" type="(oayays)" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="FdoSecrets::SecretStruct"/>
+ </method>
+</interface>
diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml
new file mode 100644
index 000000000..92aa8df84
--- /dev/null
+++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml
@@ -0,0 +1,11 @@
+<interface name="org.freedesktop.Secret.Prompt">
+ <signal name="Completed">
+ <arg name="dismissed" type="b" direction="out"/>
+ <arg name="result" type="v" direction="out"/>
+ </signal>
+ <method name="Prompt">
+ <arg name="windowId" type="s" direction="in"/>
+ </method>
+ <method name="Dismiss">
+ </method>
+</interface>
diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml
new file mode 100644
index 000000000..40240bb43
--- /dev/null
+++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml
@@ -0,0 +1,55 @@
+<interface name="org.freedesktop.Secret.Service">
+ <property name="Collections" type="ao" access="read"/>
+ <signal name="CollectionCreated">
+ <arg name="collection" type="o" direction="out"/>
+ </signal>
+ <signal name="CollectionDeleted">
+ <arg name="collection" type="o" direction="out"/>
+ </signal>
+ <signal name="CollectionChanged">
+ <arg name="collection" type="o" direction="out"/>
+ </signal>
+ <method name="OpenSession">
+ <arg type="v" direction="out"/>
+ <arg name="algorithm" type="s" direction="in"/>
+ <arg name="input" type="v" direction="in"/>
+ <arg name="result" type="o" direction="out"/>
+ </method>
+ <method name="CreateCollection">
+ <arg type="o" direction="out"/>
+ <arg name="properties" type="a{sv}" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
+ <arg name="alias" type="s" direction="in"/>
+ <arg name="prompt" type="o" direction="out"/>
+ </method>
+ <method name="SearchItems">
+ <arg type="ao" direction="out"/>
+ <arg name="attributes" type="a{ss}" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="StringStringMap"/>
+ <arg name="locked" type="ao" direction="out"/>
+ </method>
+ <method name="Unlock">
+ <arg type="ao" direction="out"/>
+ <arg name="paths" type="ao" direction="in"/>
+ <arg name="prompt" type="o" direction="out"/>
+ </method>
+ <method name="Lock">
+ <arg type="ao" direction="out"/>
+ <arg name="paths" type="ao" direction="in"/>
+ <arg name="prompt" type="o" direction="out"/>
+ </method>
+ <method name="GetSecrets">
+ <arg type="a{o(oayays)}" direction="out"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="ObjectPathSecretMap"/>
+ <arg name="items" type="ao" direction="in"/>
+ <arg name="session" type="o" direction="in"/>
+ </method>
+ <method name="ReadAlias">
+ <arg type="o" direction="out"/>
+ <arg name="name" type="s" direction="in"/>
+ </method>
+ <method name="SetAlias">
+ <arg name="name" type="s" direction="in"/>
+ <arg name="collection" type="o" direction="in"/>
+ </method>
+</interface>
diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml
new file mode 100644
index 000000000..7d358df7b
--- /dev/null
+++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml
@@ -0,0 +1,4 @@
+<interface name="org.freedesktop.Secret.Session">
+ <method name="Close">
+ </method>
+</interface>
diff --git a/tests/data/dbus/session.conf b/tests/data/dbus/session.conf
new file mode 100644
index 000000000..096da6d4c
--- /dev/null
+++ b/tests/data/dbus/session.conf
@@ -0,0 +1,39 @@
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+ <type>session</type>
+ <keep_umask/>
+ <listen>unix:tmpdir=/tmp</listen>
+ <auth>EXTERNAL</auth>
+ <standard_session_servicedirs />
+ <policy context="default">
+ <allow send_destination="*" eavesdrop="true"/>
+ <allow eavesdrop="true"/>
+ <allow own="*"/>
+ </policy>
+ <include ignore_missing="yes">/etc/dbus-1/session.conf</include>
+ <includedir>session.d</includedir>
+ <includedir>/etc/dbus-1/session.d</includedir>
+ <include ignore_missing="yes">/etc/dbus-1/session-local.conf</include>
+ <include if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include>
+ <limit name="max_incoming_bytes">1000000000</limit>
+ <limit name="max_incoming_unix_fds">250000000</limit>
+ <limit name="max_outgoing_bytes">1000000000</limit>
+ <limit name="max_outgoing_unix_fds">250000000</limit>
+ <limit name="max_message_size">1000000000</limit>
+ <limit name="auth_timeout">240000</limit>
+ <limit name="pending_fd_timeout">150000</limit>
+ <limit name="max_completed_connections">100000</limit>
+ <limit name="max_incomplete_connections">10000</limit>
+ <limit name="max_connections_per_user">100000</limit>
+ <limit name="max_pending_service_starts">10000</limit>
+ <limit name="max_names_per_connection">50000</limit>
+ <limit name="max_match_rules_per_connection">50000</limit>
+ <limit name="max_replies_per_connection">50000</limit>
+ <!-- The above is copied from session bus conf.
+ Our only intent here is to set a low service_start_timeout,
+ such that ctest can exit sooner when dbus-run-session is used
+ to launch tests and some service fails to start.
+ -->
+ <limit name="service_start_timeout">500</limit>
+</busconfig>
diff --git a/tests/gui/CMakeLists.txt b/tests/gui/CMakeLists.txt
index 6a8d21c4a..1d5822d20 100644
--- a/tests/gui/CMakeLists.txt
+++ b/tests/gui/CMakeLists.txt
@@ -21,3 +21,12 @@ add_unit_test(NAME testguipixmaps SOURCES TestGuiPixmaps.cpp LIBS ${TEST_LIBRARI
if(WITH_XC_BROWSER)
add_unit_test(NAME testguibrowser SOURCES TestGuiBrowser.cpp ../util/TemporaryFile.cpp LIBS ${TEST_LIBRARIES})
endif()
+
+if(WITH_XC_FDOSECRETS)
+ add_unit_test(NAME testguifdosecrets
+ SOURCES TestGuiFdoSecrets.cpp ../util/TemporaryFile.cpp
+ LIBS ${TEST_LIBRARIES}
+ # The following doesn't work because dbus-run-session expects execname to be in PATH
+ # dbus-run-session -- execname
+ LAUNCHER dbus-run-session --config-file ${CMAKE_CURRENT_SOURCE_DIR}/../data/dbus/session.conf -- sh -c "exec ./$0")
+endif()
diff --git a/tests/gui/TestGuiFdoSecrets.cpp b/tests/gui/TestGuiFdoSecrets.cpp
new file mode 100644
index 000000000..d223972c2
--- /dev/null
+++ b/tests/gui/TestGuiFdoSecrets.cpp
@@ -0,0 +1,1175 @@
+/*
+ * Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "TestGuiFdoSecrets.h"
+
+#include "fdosecrets/FdoSecretsPlugin.h"
+#include "fdosecrets/FdoSecretsSettings.h"
+#include "fdosecrets/objects/Collection.h"
+#include "fdosecrets/objects/Item.h"
+#include "fdosecrets/objects/Prompt.h"
+#include "fdosecrets/objects/Service.h"
+#include "fdosecrets/objects/Session.h"
+#include "fdosecrets/objects/SessionCipher.h"
+
+#include "TestGlobal.h"
+#include "config-keepassx-tests.h"
+
+#include "core/Bootstrap.h"
+#include "core/Config.h"
+#include "core/Tools.h"
+#include "crypto/Crypto.h"
+#include "gui/DatabaseTabWidget.h"
+#include "gui/DatabaseWidget.h"
+#include "gui/FileDialog.h"
+#include "gui/MessageBox.h"
+#include "gui/wizard/NewDatabaseWizard.h"
+#include "util/TemporaryFile.h"
+
+#include <QAbstractItemView>
+#include <QDBusConnection>
+#include <QDBusConnectionInterface>
+#include <QDBusInterface>
+#include <QLineEdit>
+#include <QPointer>
+#include <QSignalSpy>
+#include <QXmlStreamReader>
+
+#include <memory>
+#include <type_traits>
+
+int main(int argc, char* argv[])
+{
+#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
+ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
+#endif
+ Application app(argc, argv);
+ app.setApplicationName("KeePassXC");
+ app.setApplicationVersion(KEEPASSXC_VERSION);
+ app.setQuitOnLastWindowClosed(false);
+ app.setAttribute(Qt::AA_Use96Dpi, true);
+ QTEST_DISABLE_KEYPAD_NAVIGATION
+ TestGuiFdoSecrets tc;
+ QTEST_SET_MAIN_SOURCE_PATH
+ return QTest::qExec(&tc, argc, argv);
+}
+
+#define DBUS_PATH_DEFAULT_ALIAS "/org/freedesktop/secrets/aliases/default"
+
+#define VERIFY(statement) \
+ do { \
+ if (!QTest::qVerify(static_cast<bool>(statement), #statement, "", __FILE__, __LINE__)) \
+ return {}; \
+ } while (false)
+
+#define COMPARE(actual, expected) \
+ do { \
+ if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
+ return {}; \
+ } while (false)
+
+#define FAIL(message) \
+ do { \
+ QTest::qFail(static_cast<const char*>(message), __FILE__, __LINE__); \
+ return {}; \
+ } while (false)
+
+#define COMPARE_DBUS_LOCAL_CALL(actual, expected) \
+ do { \
+ const auto a = (actual); \
+ QVERIFY(!a.isError()); \
+ QCOMPARE(a.value(), (expected)); \
+ } while (false)
+
+#define CHECKED_DBUS_LOCAL_CALL(name, stmt) \
+ std::remove_cv<decltype(stmt)::value_type>::type name; \
+ do { \
+ const auto rep = stmt; \
+ QVERIFY(!rep.isError()); \
+ name = rep.value(); \
+ } while (false)
+
+namespace
+{
+ std::unique_ptr<QDBusInterface> interfaceOf(const QDBusObjectPath& objPath, const QString& interface)
+ {
+ std::unique_ptr<QDBusInterface> iface(new QDBusInterface(DBUS_SERVICE_SECRET, objPath.path(), interface));
+ iface->setTimeout(5);
+ VERIFY(iface->isValid());
+ return iface;
+ }
+
+ std::unique_ptr<QDBusInterface> interfaceOf(FdoSecrets::DBusObject* obj)
+ {
+ VERIFY(obj);
+ auto metaAdaptor = obj->dbusAdaptor().metaObject();
+ auto ifaceName = metaAdaptor->classInfo(metaAdaptor->indexOfClassInfo("D-Bus Interface")).value();
+
+ return interfaceOf(obj->objectPath(), ifaceName);
+ }
+
+ template <typename T> QString extractElement(const QString& doc, T cond)
+ {
+ QXmlStreamReader reader(doc);
+ while (!reader.atEnd()) {
+ int st = reader.characterOffset();
+
+ if (reader.readNext() != QXmlStreamReader::StartElement || !cond(reader)) {
+ continue;
+ }
+
+ reader.skipCurrentElement();
+ if (reader.hasError()) {
+ break;
+ }
+
+ // remove whitespaces between elements to be a little bit flexible
+ int ed = reader.characterOffset();
+ return doc.mid(st - 1, ed - st + 1).replace(QRegularExpression(R"(>[\s\n]+<)"), "><");
+ }
+ VERIFY(!reader.hasError());
+ return {};
+ }
+
+ bool checkDBusSpec(const QString& path, const QString& interface)
+ {
+ QFile f(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/dbus/interfaces/%1.xml").arg(interface));
+ VERIFY(f.open(QFile::ReadOnly | QFile::Text));
+ QTextStream in(&f);
+ auto spec = in.readAll().replace(QRegularExpression(R"(>[\s\n]+<)"), "><").trimmed();
+
+ auto bus = QDBusConnection::sessionBus();
+ auto msg = QDBusMessage::createMethodCall(
+ DBUS_SERVICE_SECRET, path, "org.freedesktop.DBus.Introspectable", "Introspect");
+
+ // BlockWithGui enters event loop
+ auto reply = QDBusPendingReply<QString>(bus.call(msg, QDBus::BlockWithGui, 5));
+ VERIFY(reply.isValid());
+ auto actual = extractElement(reply.argumentAt<0>(), [&](const QXmlStreamReader& reader) {
+ return reader.name() == "interface" && reader.attributes().value("name") == interface;
+ });
+
+ COMPARE(actual, spec);
+ return true;
+ }
+} // namespace
+
+using namespace FdoSecrets;
+
+// for use in QSignalSpy
+Q_DECLARE_METATYPE(Collection*);
+Q_DECLARE_METATYPE(Item*);
+
+TestGuiFdoSecrets::~TestGuiFdoSecrets() = default;
+
+void TestGuiFdoSecrets::initTestCase()
+{
+ // for use in QSignalSpy
+ qRegisterMetaType<Collection*>();
+ qRegisterMetaType<Item*>();
+
+ QVERIFY(Crypto::init());
+ Config::createTempFileInstance();
+ config()->set(Config::AutoSaveAfterEveryChange, false);
+ config()->set(Config::AutoSaveOnExit, false);
+ config()->set(Config::GUI_ShowTrayIcon, true);
+ config()->set(Config::UpdateCheckMessageShown, true);
+ // Disable secret service integration (activate within individual tests to test the plugin)
+ FdoSecrets::settings()->setEnabled(false);
+ // activate within individual tests
+ FdoSecrets::settings()->setShowNotification(false);
+
+ Bootstrap::bootstrapApplication();
+
+ m_mainWindow.reset(new MainWindow());
+ m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget");
+ QVERIFY(m_tabWidget);
+ m_plugin = m_mainWindow->findChild<FdoSecretsPlugin*>();
+ QVERIFY(m_plugin);
+ m_mainWindow->show();
+
+ // Load the NewDatabase.kdbx file into temporary storage
+ QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase.kdbx"));
+ QVERIFY(sourceDbFile.open(QIODevice::ReadOnly));
+ QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData));
+ sourceDbFile.close();
+
+ // set keys for session encryption
+ m_serverPublic = MpiFromHex("e407997e8b918419cf851cf3345358fdf"
+ "ffb9564a220ac9c3934efd277cea20d17"
+ "467ecdc56e817f75ac39501f38a4a04ff"
+ "64d627e16c09981c7ad876da255b61c8e"
+ "6a8408236c2a4523cfe6961c26dbdfc77"
+ "c1a27a5b425ca71a019e829fae32c0b42"
+ "0e1b3096b48bc2ce9ccab1d1ff13a5eb4"
+ "b263cee30bdb1a57af9bfa93f");
+ m_serverPrivate = MpiFromHex("013f4f3381ef0ca11c4c7363079577b56"
+ "99b238644e0aba47e24bdba6173590216"
+ "4f1e12dd0944800a373e090e63192f53b"
+ "93583e9a9e50bb9d792aafaa3a0f5ae77"
+ "de0c3423f5820848d88ee3bdd01c889f2"
+ "7af58a02f5b6693d422b9d189b300d7b1"
+ "be5076b5795cf8808c31e2e2898368d18"
+ "ab5c26b0ea3480c9aba8154cf");
+ // use the same cipher to do the client side encryption, but exchange the position of client/server keys
+ m_cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7);
+ QVERIFY(m_cipher->initialize(MpiFromBytes(MpiToBytes(m_serverPublic)),
+ MpiFromHex("30d18c6b328bac970c05bda6af2e708b9"
+ "d6bbbb6dc136c1a2d96e870fabc86ad74"
+ "1846a26a4197f32f65ea2e7580ad2afe3"
+ "dd5d6c1224b8368b0df2cd75d520a9ff9"
+ "7fe894cc7da71b7bd285b4633359c16c8"
+ "d341f822fa4f0fdf59b5d3448658c46a2"
+ "a86dbb14ff85823873f8a259ccc52bbb8"
+ "2b5a4c2a75447982553b42221"),
+ MpiFromHex("84aafe9c9f356f7762307f4d791acb59e"
+ "8e3fd562abdbb481d0587f8400ad6c51d"
+ "af561a1beb9a22c8cd4d2807367c5787b"
+ "2e06d631ccbb5194b6bb32211583ce688"
+ "f9c2cebc22a9e4d494d12ebdd570c61a1"
+ "62a94e88561d25ccd0415339d1f59e1b0"
+ "6bc6b6b5fde46e23b2410eb034be390d3"
+ "2407ec7ae90f0831f24afd5ac")));
+}
+
+// Every test starts with opening the temp database
+void TestGuiFdoSecrets::init()
+{
+ m_dbFile.reset(new TemporaryFile());
+ // Write the temp storage to a temp database file for use in our tests
+ QVERIFY(m_dbFile->open());
+ QCOMPARE(m_dbFile->write(m_dbData), static_cast<qint64>((m_dbData.size())));
+ m_dbFile->close();
+
+ // make sure window is activated or focus tests may fail
+ m_mainWindow->activateWindow();
+ QApplication::processEvents();
+
+ // open and unlock the database
+ m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a");
+ m_dbWidget = m_tabWidget->currentDatabaseWidget();
+ m_db = m_dbWidget->database();
+
+ // by default expsoe the root group
+ FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
+ QVERIFY(m_dbWidget->save());
+}
+
+// Every test ends with closing the temp database without saving
+void TestGuiFdoSecrets::cleanup()
+{
+ // restore to default settings
+ FdoSecrets::settings()->setShowNotification(false);
+ FdoSecrets::settings()->setEnabled(false);
+ if (m_plugin) {
+ m_plugin->updateServiceState();
+ }
+
+ // DO NOT save the database
+ for (int i = 0; i != m_tabWidget->count(); ++i) {
+ m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean();
+ }
+ QVERIFY(m_tabWidget->closeAllDatabaseTabs());
+ QApplication::processEvents();
+
+ if (m_dbFile) {
+ m_dbFile->remove();
+ }
+}
+
+void TestGuiFdoSecrets::cleanupTestCase()
+{
+ if (m_dbFile) {
+ m_dbFile->remove();
+ }
+}
+
+void TestGuiFdoSecrets::testDBusSpec()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ // service
+ QCOMPARE(service->objectPath().path(), QStringLiteral(DBUS_PATH_SECRETS));
+ QVERIFY(checkDBusSpec(service->objectPath().path(), DBUS_INTERFACE_SECRET_SERVICE));
+
+ // default alias
+ QVERIFY(checkDBusSpec(DBUS_PATH_DEFAULT_ALIAS, DBUS_INTERFACE_SECRET_COLLECTION));
+
+ // collection
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ QVERIFY(checkDBusSpec(coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION));
+
+ // item
+ auto item = getFirstItem(coll);
+ QVERIFY(item);
+ QVERIFY(checkDBusSpec(item->objectPath().path(), DBUS_INTERFACE_SECRET_ITEM));
+
+ // session
+ auto sess = openSession(service, PlainCipher::Algorithm);
+ QVERIFY(sess);
+ QVERIFY(checkDBusSpec(sess->objectPath().path(), DBUS_INTERFACE_SECRET_SESSION));
+
+ // prompt
+ FdoSecrets::settings()->setNoConfirmDeleteItem(true);
+ PromptBase* prompt = nullptr;
+ {
+ auto rep = item->deleteItem();
+ QVERIFY(!rep.isError());
+ prompt = rep.value();
+ }
+ QVERIFY(prompt);
+ QVERIFY(checkDBusSpec(prompt->objectPath().path(), DBUS_INTERFACE_SECRET_PROMPT));
+}
+
+void TestGuiFdoSecrets::testServiceEnable()
+{
+ QSignalSpy sigError(m_plugin, SIGNAL(error(QString)));
+ QVERIFY(sigError.isValid());
+
+ QSignalSpy sigStarted(m_plugin, SIGNAL(secretServiceStarted()));
+ QVERIFY(sigStarted.isValid());
+
+ // make sure no one else is holding the service
+ QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
+
+ // enable the service
+ auto service = enableService();
+ QVERIFY(service);
+
+ // service started without error
+ QVERIFY(sigError.isEmpty());
+ QCOMPARE(sigStarted.size(), 1);
+
+ QApplication::processEvents();
+
+ QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
+
+ // there will be one default collection
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ COMPARE_DBUS_LOCAL_CALL(coll->locked(), false);
+ COMPARE_DBUS_LOCAL_CALL(coll->label(), m_db->metadata()->name());
+ COMPARE_DBUS_LOCAL_CALL(
+ coll->created(),
+ static_cast<qulonglong>(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000));
+ COMPARE_DBUS_LOCAL_CALL(
+ coll->modified(),
+ static_cast<qulonglong>(m_db->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000));
+}
+
+void TestGuiFdoSecrets::testServiceEnableNoExposedDatabase()
+{
+ // reset the exposed group and then enable the service
+ FdoSecrets::settings()->setExposedGroup(m_db, {});
+ auto service = enableService();
+ QVERIFY(service);
+
+ // no collections
+ COMPARE_DBUS_LOCAL_CALL(service->collections(), QList<Collection*>{});
+}
+
+void TestGuiFdoSecrets::testServiceSearch()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto item = getFirstItem(coll);
+ QVERIFY(item);
+
+ item->backend()->attributes()->set("fdosecrets-test", "1");
+ item->backend()->attributes()->set("fdosecrets-test-protected", "2", true);
+ const QString crazyKey = "_a:bc&-+'-e%12df_d";
+ const QString crazyValue = "[v]al@-ue";
+ item->backend()->attributes()->set(crazyKey, crazyValue);
+
+ // search by title
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Title", item->backend()->title()}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked, {item});
+ }
+
+ // search by attribute
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test", "1"}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked, {item});
+ }
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{crazyKey, crazyValue}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked, {item});
+ }
+
+ // searching using empty terms returns nothing
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked.size(), 0);
+ }
+
+ // searching using protected attributes or password returns nothing
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Password", item->backend()->password()}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked.size(), 0);
+ }
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test-protected", "2"}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked.size(), 0);
+ }
+}
+
+void TestGuiFdoSecrets::testServiceUnlock()
+{
+ lockDatabaseInBackend();
+
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*)));
+ QVERIFY(spyCollectionCreated.isValid());
+ QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*)));
+ QVERIFY(spyCollectionDeleted.isValid());
+ QSignalSpy spyCollectionChanged(service, SIGNAL(collectionChanged(Collection*)));
+ QVERIFY(spyCollectionChanged.isValid());
+
+ PromptBase* prompt = nullptr;
+ {
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->unlock({coll.data()}, prompt));
+ // nothing is unlocked immediately without user's action
+ QVERIFY(unlocked.isEmpty());
+ }
+ QVERIFY(prompt);
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // nothing is unlocked yet
+ QCOMPARE(spyPromptCompleted.count(), 0);
+ QVERIFY(coll);
+ QVERIFY(coll->backend()->isLocked());
+
+ // drive the prompt
+ QVERIFY(!prompt->prompt("").isError());
+
+ // still not unlocked before user action
+ QCOMPARE(spyPromptCompleted.count(), 0);
+ QVERIFY(coll);
+ QVERIFY(coll->backend()->isLocked());
+
+ // interact with the dialog
+ QApplication::processEvents();
+ {
+ auto dbOpenDlg = m_tabWidget->findChild<DatabaseOpenDialog*>();
+ QVERIFY(dbOpenDlg);
+ auto editPassword = dbOpenDlg->findChild<QLineEdit*>("editPassword");
+ QVERIFY(editPassword);
+ editPassword->setFocus();
+ QTest::keyClicks(editPassword, "a");
+ QTest::keyClick(editPassword, Qt::Key_Enter);
+ }
+ QApplication::processEvents();
+
+ // unlocked
+ QVERIFY(coll);
+ QVERIFY(!coll->backend()->isLocked());
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ {
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.size(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {coll->objectPath()});
+ }
+ QCOMPARE(spyCollectionCreated.count(), 0);
+ QCOMPARE(spyCollectionChanged.count(), 1);
+ {
+ auto args = spyCollectionChanged.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Collection*>(), coll.data());
+ }
+ QCOMPARE(spyCollectionDeleted.count(), 0);
+}
+
+void TestGuiFdoSecrets::testServiceLock()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*)));
+ QVERIFY(spyCollectionCreated.isValid());
+ QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*)));
+ QVERIFY(spyCollectionDeleted.isValid());
+ QSignalSpy spyCollectionChanged(service, SIGNAL(collectionChanged(Collection*)));
+ QVERIFY(spyCollectionChanged.isValid());
+
+ // if the db is modified, prompt user
+ m_db->markAsModified();
+ {
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt));
+ QCOMPARE(locked.size(), 0);
+ QVERIFY(prompt);
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // prompt and click cancel
+ MessageBox::setNextAnswer(MessageBox::Cancel);
+ QVERIFY(!prompt->prompt("").isError());
+ QApplication::processEvents();
+
+ QVERIFY(!coll->backend()->isLocked());
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.count(), 2);
+ QCOMPARE(args.at(0).toBool(), true);
+ QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {});
+ }
+ {
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt));
+ QCOMPARE(locked.size(), 0);
+ QVERIFY(prompt);
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // prompt and click save
+ MessageBox::setNextAnswer(MessageBox::Save);
+ QVERIFY(!prompt->prompt("").isError());
+ QApplication::processEvents();
+
+ QVERIFY(coll->backend()->isLocked());
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.count(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {coll->objectPath()});
+ }
+
+ QCOMPARE(spyCollectionCreated.count(), 0);
+ QCOMPARE(spyCollectionChanged.count(), 1);
+ {
+ auto args = spyCollectionChanged.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Collection*>(), coll.data());
+ }
+ QCOMPARE(spyCollectionDeleted.count(), 0);
+
+ // locking item locks the whole db
+ unlockDatabaseInBackend();
+ {
+ auto item = getFirstItem(coll);
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(locked, service->lock({item}, prompt));
+ QCOMPARE(locked.size(), 0);
+ QVERIFY(prompt);
+
+ MessageBox::setNextAnswer(MessageBox::Save);
+ QVERIFY(!prompt->prompt("").isError());
+ QApplication::processEvents();
+
+ QVERIFY(coll->backend()->isLocked());
+ }
+}
+
+void TestGuiFdoSecrets::testSessionOpen()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ auto sess = openSession(service, PlainCipher::Algorithm);
+ QVERIFY(sess);
+ QCOMPARE(service->sessions().size(), 1);
+
+ sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
+ QVERIFY(sess);
+ QCOMPARE(service->sessions().size(), 2);
+}
+
+void TestGuiFdoSecrets::testSessionClose()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ auto sess = openSession(service, PlainCipher::Algorithm);
+ QVERIFY(sess);
+
+ QCOMPARE(service->sessions().size(), 1);
+
+ auto rep = sess->close();
+ QVERIFY(!rep.isError());
+
+ QCOMPARE(service->sessions().size(), 0);
+}
+
+void TestGuiFdoSecrets::testCollectionCreate()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*)));
+ QVERIFY(spyCollectionCreated.isValid());
+
+ // returns existing if alias is nonempty and exists
+ {
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(
+ coll, service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "NewDB"}}, "default", prompt));
+ QVERIFY(!prompt);
+ QCOMPARE(coll, getDefaultCollection(service).data());
+ }
+ QCOMPARE(spyCollectionCreated.count(), 0);
+
+ // create new one and set properties
+ {
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(
+ created,
+ service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "Test NewDB"}}, "mydatadb", prompt));
+ QVERIFY(!created);
+ QVERIFY(prompt);
+
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ QTimer::singleShot(50, this, SLOT(createDatabaseCallback()));
+ QVERIFY(!prompt->prompt("").isError());
+ QApplication::processEvents();
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.size(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ auto coll = FdoSecrets::pathToObject<Collection>(args.at(1).value<QDBusObjectPath>());
+ QVERIFY(coll);
+
+ QCOMPARE(coll->backend()->database()->metadata()->name(), QStringLiteral("Test NewDB"));
+
+ QCOMPARE(spyCollectionCreated.count(), 1);
+ {
+ auto args = spyCollectionCreated.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Collection*>(), coll);
+ }
+ }
+}
+
+void TestGuiFdoSecrets::createDatabaseCallback()
+{
+ auto wizard = m_tabWidget->findChild<NewDatabaseWizard*>();
+ QVERIFY(wizard);
+
+ QCOMPARE(wizard->currentId(), 0);
+ wizard->next();
+ wizard->next();
+ QCOMPARE(wizard->currentId(), 2);
+
+ // enter password
+ auto* passwordEdit = wizard->findChild<QLineEdit*>("enterPasswordEdit");
+ auto* passwordRepeatEdit = wizard->findChild<QLineEdit*>("repeatPasswordEdit");
+ QTest::keyClicks(passwordEdit, "test");
+ QTest::keyClick(passwordEdit, Qt::Key::Key_Tab);
+ QTest::keyClicks(passwordRepeatEdit, "test");
+
+ // save database to temporary file
+ TemporaryFile tmpFile;
+ QVERIFY(tmpFile.open());
+ tmpFile.close();
+ fileDialog()->setNextFileName(tmpFile.fileName());
+
+ wizard->accept();
+
+ tmpFile.remove();
+}
+
+void TestGuiFdoSecrets::testCollectionDelete()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ // closing the tab calls coll->deleteLater()
+ // but deleteLater is not processed in QApplication::processEvent
+ // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents
+ auto rawColl = coll.data();
+
+ QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*)));
+ QVERIFY(spyCollectionDeleted.isValid());
+
+ m_db->markAsModified();
+ CHECKED_DBUS_LOCAL_CALL(prompt, coll->deleteCollection());
+ QVERIFY(prompt);
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // prompt and click save
+ MessageBox::setNextAnswer(MessageBox::Save);
+ QVERIFY(!prompt->prompt("").isError());
+
+ QApplication::processEvents();
+
+ // closing the tab should have deleted coll if not in testing
+ // but deleteLater is not processed in QApplication::processEvent
+ // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents
+ // QVERIFY(!coll);
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.count(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ QCOMPARE(args.at(1).toString(), QStringLiteral(""));
+
+ QCOMPARE(spyCollectionDeleted.count(), 1);
+ {
+ auto args = spyCollectionDeleted.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Collection*>(), rawColl);
+ }
+}
+
+void TestGuiFdoSecrets::testItemCreate()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
+ QVERIFY(sess);
+
+ // create item
+ StringStringMap attributes{
+ {"application", "fdosecrets-test"},
+ {"attr-i[bute]", "![some] -value*"},
+ };
+
+ auto item = createItem(sess, coll, "abc", "Password", attributes, false);
+ QVERIFY(item);
+
+ // attributes
+ {
+ CHECKED_DBUS_LOCAL_CALL(actual, item->attributes());
+ for (const auto& key : attributes.keys()) {
+ QVERIFY(actual.contains(key));
+ QCOMPARE(actual[key], attributes[key]);
+ }
+ }
+
+ // label
+ COMPARE_DBUS_LOCAL_CALL(item->label(), QStringLiteral("abc"));
+
+ // secrets
+ {
+ CHECKED_DBUS_LOCAL_CALL(ss, item->getSecret(sess));
+ ss = m_cipher->decrypt(ss);
+ QCOMPARE(ss.value, QByteArray("Password"));
+ }
+
+ // searchable
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems(attributes, locked));
+ QCOMPARE(locked, QList<Item*>{});
+ QCOMPARE(unlocked, QList<Item*>{item});
+ }
+ {
+ CHECKED_DBUS_LOCAL_CALL(unlocked, coll->searchItems(attributes));
+ QVERIFY(unlocked.contains(item));
+ }
+}
+
+void TestGuiFdoSecrets::testItemReplace()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
+ QVERIFY(sess);
+
+ // create item
+ StringStringMap attr1{
+ {"application", "fdosecrets-test"},
+ {"attr-i[bute]", "![some] -value*"},
+ {"fdosecrets-attr", "1"},
+ };
+ StringStringMap attr2{
+ {"application", "fdosecrets-test"},
+ {"attr-i[bute]", "![some] -value*"},
+ {"fdosecrets-attr", "2"},
+ };
+
+ auto item1 = createItem(sess, coll, "abc1", "Password", attr1, false);
+ QVERIFY(item1);
+ auto item2 = createItem(sess, coll, "abc2", "Password", attr2, false);
+ QVERIFY(item2);
+
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked));
+ QCOMPARE(unlocked.size(), 2);
+ }
+
+ {
+ // when replace, existing item with matching attr is updated
+ auto item3 = createItem(sess, coll, "abc3", "Password", attr2, true);
+ QVERIFY(item3);
+ QCOMPARE(item2, item3);
+ COMPARE_DBUS_LOCAL_CALL(item3->label(), QStringLiteral("abc3"));
+ // there is still 2 entries
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked));
+ QCOMPARE(unlocked.size(), 2);
+ }
+
+ {
+ // when NOT replace, another entry is created
+ auto item4 = createItem(sess, coll, "abc4", "Password", attr2, false);
+ QVERIFY(item4);
+ COMPARE_DBUS_LOCAL_CALL(item2->label(), QStringLiteral("abc3"));
+ COMPARE_DBUS_LOCAL_CALL(item4->label(), QStringLiteral("abc4"));
+ // there is 3 entries
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked));
+ QCOMPARE(unlocked.size(), 3);
+ }
+}
+
+void TestGuiFdoSecrets::testItemSecret()
+{
+ const QString TEXT_PLAIN = "text/plain";
+ const QString APPLICATION_OCTET_STREAM = "application/octet-stream";
+
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto item = getFirstItem(coll);
+ QVERIFY(item);
+ auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
+ QVERIFY(sess);
+
+ // plain text secret
+ {
+ CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess));
+ auto ss = m_cipher->decrypt(encrypted);
+ QCOMPARE(ss.contentType, TEXT_PLAIN);
+ QCOMPARE(ss.value, item->backend()->password().toUtf8());
+ }
+
+ // get secret with notification (only works when called from DBUS)
+ FdoSecrets::settings()->setShowNotification(true);
+ {
+ QSignalSpy spyShowNotification(m_plugin, SIGNAL(requestShowNotification(QString, QString, int)));
+ QVERIFY(spyShowNotification.isValid());
+
+ auto iitem = interfaceOf(item);
+ QVERIFY(static_cast<bool>(iitem));
+
+ auto replyMsg = iitem->call(QDBus::BlockWithGui, "GetSecret", QVariant::fromValue(sess->objectPath()));
+ auto reply = QDBusPendingReply<SecretStruct>(replyMsg);
+ QVERIFY(reply.isValid());
+ auto ss = m_cipher->decrypt(reply.argumentAt<0>());
+
+ QCOMPARE(ss.contentType, TEXT_PLAIN);
+ QCOMPARE(ss.value, item->backend()->password().toUtf8());
+
+ QCOMPARE(spyShowNotification.count(), 1);
+ }
+ FdoSecrets::settings()->setShowNotification(false);
+
+ // set secret with plain text
+ {
+ SecretStruct ss;
+ ss.contentType = TEXT_PLAIN;
+ ss.value = "NewPassword";
+ ss.session = sess->objectPath();
+ QVERIFY(!item->setSecret(m_cipher->encrypt(ss)).isError());
+
+ QCOMPARE(item->backend()->password().toUtf8(), ss.value);
+ }
+
+ // set secret with something else is saved as attachment
+ {
+ SecretStruct expected;
+ expected.contentType = APPLICATION_OCTET_STREAM;
+ expected.value = "NewPasswordBinary";
+ expected.session = sess->objectPath();
+ QVERIFY(!item->setSecret(m_cipher->encrypt(expected)).isError());
+
+ QCOMPARE(item->backend()->password(), QStringLiteral(""));
+
+ CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess));
+ auto ss = m_cipher->decrypt(encrypted);
+ QCOMPARE(ss.contentType, expected.contentType);
+ QCOMPARE(ss.value, expected.value);
+ }
+}
+
+void TestGuiFdoSecrets::testItemDelete()
+{
+ FdoSecrets::settings()->setNoConfirmDeleteItem(false);
+
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto item = getFirstItem(coll);
+ QVERIFY(item);
+ auto rawItem = item.data();
+
+ QSignalSpy spyItemDeleted(coll, SIGNAL(itemDeleted(Item*)));
+ QVERIFY(spyItemDeleted.isValid());
+
+ CHECKED_DBUS_LOCAL_CALL(prompt, item->deleteItem());
+ QVERIFY(prompt);
+
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // prompt and click save
+ if (item->isDeletePermanent()) {
+ MessageBox::setNextAnswer(MessageBox::Delete);
+ } else {
+ MessageBox::setNextAnswer(MessageBox::Move);
+ }
+ QVERIFY(!prompt->prompt("").isError());
+
+ QApplication::processEvents();
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.count(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ QCOMPARE(args.at(1).toString(), QStringLiteral(""));
+
+ QCOMPARE(spyItemDeleted.count(), 1);
+ {
+ auto args = spyItemDeleted.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Item*>(), rawItem);
+ }
+}
+
+void TestGuiFdoSecrets::testAlias()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ // read default alias
+ CHECKED_DBUS_LOCAL_CALL(coll, service->readAlias("default"));
+ QVERIFY(coll);
+
+ // set extra alias
+ QVERIFY(!service->setAlias("another", coll).isError());
+
+ // get using extra alias
+ CHECKED_DBUS_LOCAL_CALL(coll2, service->readAlias("another"));
+ QVERIFY(coll2);
+ QCOMPARE(coll, coll2);
+}
+
+void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ // one collection, which is default alias
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ // after locking, the collection is still there, but locked
+ lockDatabaseInBackend();
+
+ coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ COMPARE_DBUS_LOCAL_CALL(coll->locked(), true);
+
+ // unlock the database, the alias and collection is present
+ unlockDatabaseInBackend();
+
+ coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ COMPARE_DBUS_LOCAL_CALL(coll->locked(), false);
+}
+
+void TestGuiFdoSecrets::testExposeSubgroup()
+{
+ auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
+ QVERIFY(subgroup);
+ FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
+ auto service = enableService();
+ QVERIFY(service);
+
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ // exposing subgroup does not expose entries in other groups
+ auto items = coll->items();
+ QVERIFY(!items.isError());
+ QList<Entry*> exposedEntries;
+ for (const auto& item : items.value()) {
+ exposedEntries << item->backend();
+ }
+ QCOMPARE(exposedEntries, subgroup->entries());
+}
+
+void TestGuiFdoSecrets::testModifiyingExposedGroup()
+{
+ // test when exposed group is removed the collection is not exposed anymore
+ auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking");
+ QVERIFY(subgroup);
+ FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
+ auto service = enableService();
+ QVERIFY(service);
+
+ {
+ CHECKED_DBUS_LOCAL_CALL(colls, service->collections());
+ QCOMPARE(colls.size(), 1);
+ }
+
+ m_db->metadata()->setRecycleBinEnabled(true);
+ m_db->recycleGroup(subgroup);
+ QApplication::processEvents();
+
+ {
+ CHECKED_DBUS_LOCAL_CALL(colls, service->collections());
+ QCOMPARE(colls.size(), 0);
+ }
+
+ // test setting another exposed group, the collection will be exposed again
+ FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
+ QApplication::processEvents();
+ {
+ CHECKED_DBUS_LOCAL_CALL(colls, service->collections());
+ QCOMPARE(colls.size(), 1);
+ }
+}
+
+QPointer<Service> TestGuiFdoSecrets::enableService()
+{
+ FdoSecrets::settings()->setEnabled(true);
+ VERIFY(m_plugin);
+ m_plugin->updateServiceState();
+ return m_plugin->serviceInstance();
+}
+
+QPointer<Session> TestGuiFdoSecrets::openSession(Service* service, const QString& algo)
+{
+ // open session has to be called actually over DBUS to get peer info
+
+ VERIFY(service);
+ auto iservice = interfaceOf(service);
+ VERIFY(iservice);
+
+ if (algo == PlainCipher::Algorithm) {
+ auto replyMsg = iservice->call(QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant("")));
+ auto reply = QDBusPendingReply<QDBusVariant, QDBusObjectPath>(replyMsg);
+
+ VERIFY(reply.isValid());
+ return FdoSecrets::pathToObject<Session>(reply.argumentAt<1>());
+ } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) {
+
+ DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(MpiFromBytes(MpiToBytes(m_serverPrivate)),
+ MpiFromBytes(MpiToBytes(m_serverPublic)));
+
+ auto replyMsg = iservice->call(
+ QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant(m_cipher->m_publicKey)));
+ auto reply = QDBusPendingReply<QDBusVariant, QDBusObjectPath>(replyMsg);
+ VERIFY(reply.isValid());
+ COMPARE(qvariant_cast<QByteArray>(reply.argumentAt<0>().variant()), MpiToBytes(m_serverPublic));
+ return FdoSecrets::pathToObject<Session>(reply.argumentAt<1>());
+ }
+ FAIL("Unsupported algorithm");
+}
+
+QPointer<Collection> TestGuiFdoSecrets::getDefaultCollection(Service* service)
+{
+ VERIFY(service);
+ auto coll = service->readAlias("default");
+ VERIFY(!coll.isError());
+ return coll.value();
+}
+
+QPointer<Item> TestGuiFdoSecrets::getFirstItem(Collection* coll)
+{
+ VERIFY(coll);
+ auto items = coll->items();
+ VERIFY(!items.isError());
+ VERIFY(!items.value().isEmpty());
+ return items.value().at(0);
+}
+
+QPointer<Item> TestGuiFdoSecrets::createItem(Session* sess,
+ Collection* coll,
+ const QString& label,
+ const QString& pass,
+ const StringStringMap& attr,
+ bool replace)
+{
+ VERIFY(sess);
+ VERIFY(coll);
+
+ QVariantMap properties{
+ {DBUS_INTERFACE_SECRET_ITEM ".Label", QVariant::fromValue(label)},
+ {DBUS_INTERFACE_SECRET_ITEM ".Attributes", QVariant::fromValue(attr)},
+ };
+
+ SecretStruct ss;
+ ss.session = sess->objectPath();
+ ss.value = pass.toLocal8Bit();
+ ss.contentType = "plain/text";
+ ss = m_cipher->encrypt(ss);
+
+ PromptBase* prompt = nullptr;
+ auto item = coll->createItem(properties, ss, replace, prompt);
+ VERIFY(!item.isError());
+ // creating item does not have a prompt to show
+ VERIFY(!prompt);
+ return item.value();
+}
+
+void TestGuiFdoSecrets::lockDatabaseInBackend()
+{
+ m_dbWidget->lock();
+ m_db.reset();
+ QApplication::processEvents();
+}
+
+void TestGuiFdoSecrets::unlockDatabaseInBackend()
+{
+ m_dbWidget->performUnlockDatabase("a");
+ m_db = m_dbWidget->database();
+ QApplication::processEvents();
+}
diff --git a/tests/gui/TestGuiFdoSecrets.h b/tests/gui/TestGuiFdoSecrets.h
new file mode 100644
index 000000000..8a961eb8e
--- /dev/null
+++ b/tests/gui/TestGuiFdoSecrets.h
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_TESTGUIFDOSECRETS_H
+#define KEEPASSXC_TESTGUIFDOSECRETS_H
+
+#include <QByteArray>
+#include <QObject>
+#include <QPointer>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QString>
+
+#include "fdosecrets/GcryptMPI.h"
+#include "fdosecrets/objects/DBusTypes.h"
+
+class MainWindow;
+class Database;
+class DatabaseTabWidget;
+class DatabaseWidget;
+class TemporaryFile;
+class FdoSecretsPlugin;
+namespace FdoSecrets
+{
+ class Service;
+ class Session;
+ class Collection;
+ class Item;
+ class Prompt;
+ class DhIetf1024Sha256Aes128CbcPkcs7;
+} // namespace FdoSecrets
+
+class QAbstractItemView;
+
+class TestGuiFdoSecrets : public QObject
+{
+ Q_OBJECT
+
+public:
+ ~TestGuiFdoSecrets() override;
+
+private slots:
+ void initTestCase();
+ void init();
+ void cleanup();
+ void cleanupTestCase();
+
+ void testDBusSpec();
+
+ void testServiceEnable();
+ void testServiceEnableNoExposedDatabase();
+ void testServiceSearch();
+ void testServiceUnlock();
+ void testServiceLock();
+
+ void testSessionOpen();
+ void testSessionClose();
+
+ void testCollectionCreate();
+ void testCollectionDelete();
+
+ void testItemCreate();
+ void testItemReplace();
+ void testItemSecret();
+ void testItemDelete();
+
+ void testAlias();
+ void testDefaultAliasAlwaysPresent();
+
+ void testExposeSubgroup();
+ void testModifiyingExposedGroup();
+
+protected slots:
+ void createDatabaseCallback();
+
+private:
+ void lockDatabaseInBackend();
+ void unlockDatabaseInBackend();
+ QPointer<FdoSecrets::Service> enableService();
+ QPointer<FdoSecrets::Session> openSession(FdoSecrets::Service* service, const QString& algo);
+ QPointer<FdoSecrets::Collection> getDefaultCollection(FdoSecrets::Service* service);
+ QPointer<FdoSecrets::Item> getFirstItem(FdoSecrets::Collection* coll);
+ QPointer<FdoSecrets::Item> createItem(FdoSecrets::Session* sess,
+ FdoSecrets::Collection* coll,
+ const QString& label,
+ const QString& pass,
+ const StringStringMap& attr,
+ bool replace);
+
+private:
+ QScopedPointer<MainWindow> m_mainWindow;
+ QPointer<DatabaseTabWidget> m_tabWidget;
+ QPointer<DatabaseWidget> m_dbWidget;
+ QSharedPointer<Database> m_db;
+
+ QPointer<FdoSecretsPlugin> m_plugin;
+
+ // For DH session tests
+ GcryptMPI m_serverPrivate;
+ GcryptMPI m_serverPublic;
+ std::unique_ptr<FdoSecrets::DhIetf1024Sha256Aes128CbcPkcs7> m_cipher;
+
+ QByteArray m_dbData;
+ QScopedPointer<TemporaryFile> m_dbFile;
+};
+
+#endif // KEEPASSXC_TESTGUIFDOSECRETS_H