commit 23e10a7a4e8d14fb9d870d8cbe53002abdeeddfb branch master Author: Michael Leupold Date: Sat Oct 30 17:38:47 2010 +0000 (Almost) complete the test for the ksecret backend. Also introduce a sync-timer which syncs collections to disk when changed. svn path=/trunk/playground/base/ksecretservice/; revision=1191341 diff --git a/backend/ksecret/ksecretcollection.cpp b/backend/ksecret/ksecretcollection.cpp index 4303d96..4369c57 100644 --- a/backend/ksecret/ksecretcollection.cpp +++ b/backend/ksecret/ksecretcollection.cpp @@ -25,14 +25,15 @@ #include "../securebuffer.h" #include +#include +#include +#include + #include #include #include #include -#include -#include - // this must not be changed or else file compatibility is gone! #define VERIFIER_LENGTH 64 @@ -67,6 +68,7 @@ KSecretCollection *KSecretCollection::create(const QString &id, const QCA::Secur { KSecretCollection *coll = new KSecretCollection(parent); coll->m_id = id; + coll->m_path = KGlobal::dirs()->saveLocation("ksecret") + '/' + id + ".ksecret"; coll->m_algoHash = KSecretFile::SHA256; coll->m_algoCipher = KSecretFile::AES256; @@ -107,14 +109,23 @@ KSecretCollection *KSecretCollection::create(const QString &id, const QCA::Secur coll->m_cipher->setup(QCA::Encode, *coll->m_symmetricKey, coll->m_verInitVector); coll->m_verEncryptedRandom = coll->m_cipher->update(randomData); coll->m_verEncryptedRandom.append(coll->m_cipher->final()); + + // write new collection to disk + if (!coll->serialize(errorMessage)) { + delete coll; + return 0; + } + coll->m_dirty = false; return coll; } KSecretCollection::KSecretCollection(BackendCollectionManager *parent) - : BackendCollection(parent), m_hash(0), - m_mac(0), m_cipher(0), m_symmetricKey(0) + : BackendCollection(parent), m_hash(0), m_mac(0), m_cipher(0), + m_symmetricKey(0), m_dirty(true) { + m_syncTimer.setSingleShot(true); + connect(&m_syncTimer, SIGNAL(timeout()), SLOT(sync())); } KSecretCollection::~KSecretCollection() @@ -147,6 +158,8 @@ BackendReturn KSecretCollection::setLabel(const QString &label) } m_label = label; + m_dirty = true; + startSyncTimer(); emit collectionChanged(this); return NoError; } @@ -222,6 +235,8 @@ DeleteCollectionJob *KSecretCollection::createDeleteJob(const CollectionDeleteIn BackendReturn KSecretCollection::deleteCollection() { + // stop any sync timers in progress + m_syncTimer.stop(); // remove the ksecret file if(!QFile::remove(m_path)) { return false; @@ -296,8 +311,13 @@ BackendReturn KSecretCollection::createItem(const QString &label, // new item, signals need to be wired connect(item, SIGNAL(itemDeleted(BackendItem*)), SLOT(slotItemDeleted(BackendItem*))); connect(item, SIGNAL(itemChanged(BackendItem*)), SIGNAL(itemChanged(BackendItem*))); + connect(item, SIGNAL(itemChanged(BackendItem*)), SLOT(startSyncTimer())); emit itemCreated(item); } + + // sync + m_dirty = true; + startSyncTimer(); return item; } @@ -321,6 +341,10 @@ void KSecretCollection::slotItemDeleted(BackendItem *item) m_reverseItemHashes.remove(kitem); } m_items.remove(kitem->id()); + + // sync + m_dirty = true; + startSyncTimer(); emit itemDeleted(item); } @@ -345,6 +369,29 @@ void KSecretCollection::changeAttributeHashes(KSecretItem *item) m_reverseItemHashes.insert(item, attributeHashes); } +void KSecretCollection::startSyncTimer() +{ + // mark collection dirty and start sync in 3 seconds. + // make sure it's really synced to constant changes to the collection don't + // leave it unsaved for too long. + if (!m_syncTimer.isActive()) { + m_syncTimer.start(3000); + } +} + +void KSecretCollection::sync() +{ + if (m_dirty) { + QString errorMessage; + if (!serialize(errorMessage)) { + // try to resync if syncing failed + startSyncTimer(); + } else { + m_dirty = false; + } + } +} + KSecretCollection *KSecretCollection::deserialize(const QString &path, KSecretCollectionManager *parent, QString &errorMessage) @@ -450,10 +497,12 @@ BackendReturn KSecretCollection::lock() if(isLocked()) { return true; } else { - // TODO: only serialize if dirty - QString errorMessage; - if(!serialize(errorMessage)) { - return BackendReturn(false, ErrorOther, errorMessage); + if (m_dirty) { + QString errorMessage; + if(!serialize(errorMessage)) { + return BackendReturn(false, ErrorOther, errorMessage); + } + m_dirty = false; } // remove individual item secrets @@ -929,7 +978,7 @@ bool KSecretCollection::serialize(QString &errorMessage) const errorMessage = genericSavingErrorMessage(); return false; } - + return true; } @@ -1297,6 +1346,11 @@ bool KSecretCollection::setApplicationPermission(const QString& path, BackendCol { // TODO: implement Q_ASSERT(0); + + // sync + m_dirty = true; + startSyncTimer(); + return true; } diff --git a/backend/ksecret/ksecretcollection.h b/backend/ksecret/ksecretcollection.h index 1e2e896..a9dd303 100644 --- a/backend/ksecret/ksecretcollection.h +++ b/backend/ksecret/ksecretcollection.h @@ -25,6 +25,8 @@ #include "ksecretitem.h" #include "ksecretfile.h" +#include + class KSecretCollectionManager; class KSecretUnlockCollectionJob; class KSecretLockCollectionJob; @@ -209,7 +211,20 @@ private Q_SLOTS: * @param item Item whose attributes changed */ void changeAttributeHashes(KSecretItem *item); - + + /** + * This slot can be called to start the sync timer (if it's not running + * yet). It's supposed to be called after every change to the collection + * or one of its items. + */ + void startSyncTimer(); + + /** + * This slot is called by the sync timer to sync the collection + * on changes. + */ + void sync(); + public: /** * Deserialize a ksecret collection from a KSecretFile. @@ -457,6 +472,12 @@ private: */ bool serializeAuthenticated(const QByteArray &data, KSecretFile &file) const; + // flag which is set when the collection was changed and which + // denotes that the collection should be synced back to disk. + bool m_dirty; + // timer for syncing the collection on changes + QTimer m_syncTimer; + QString m_id; QString m_label; QDateTime m_created; diff --git a/backend/ksecret/ksecretcollectionmanager.cpp b/backend/ksecret/ksecretcollectionmanager.cpp index ad627a8..650d507 100644 --- a/backend/ksecret/ksecretcollectionmanager.cpp +++ b/backend/ksecret/ksecretcollectionmanager.cpp @@ -23,14 +23,19 @@ #include "ksecretjobs.h" #include +#include +#include #include #include KSecretCollectionManager::KSecretCollectionManager(const QString &path, QObject *parent) - : BackendCollectionManager(parent), m_watcher(QStringList(path)) + : BackendCollectionManager(parent), m_watcher(this) { - connect(&m_watcher, SIGNAL(directoryChanged(QString)), SLOT(slotDirectoryChanged(QString))); + KGlobal::dirs()->addResourceType("ksecret", 0, path); + m_watcher.addDir(KGlobal::dirs()->saveLocation("ksecret")); + connect(&m_watcher, SIGNAL(dirty(QString)), SLOT(slotDirectoryChanged(QString))); + m_watcher.startScan(); // list directory contents to discover existing collections on startup QTimer::singleShot(0, this, SLOT(slotStartupDiscovery())); } @@ -72,8 +77,7 @@ void KSecretCollectionManager::slotDirectoryChanged(const QString &path) void KSecretCollectionManager::slotStartupDiscovery() { - Q_ASSERT(m_watcher.directories().count() == 1); - slotDirectoryChanged(m_watcher.directories().at(0)); + slotDirectoryChanged(KGlobal::dirs()->saveLocation("ksecret")); } void KSecretCollectionManager::createCollectionJobResult(QueuedJob *job) diff --git a/backend/ksecret/ksecretcollectionmanager.h b/backend/ksecret/ksecretcollectionmanager.h index 50cc76e..ade1a31 100644 --- a/backend/ksecret/ksecretcollectionmanager.h +++ b/backend/ksecret/ksecretcollectionmanager.h @@ -23,8 +23,9 @@ #include "../backendcollectionmanager.h" +#include + #include -#include class KSecretCollection; class KSecretCreateCollectionJob; @@ -40,7 +41,6 @@ public: /** * Constructor * - * @param path Path to detect ksecret files in * @param parent parent object */ explicit KSecretCollectionManager(const QString &path, QObject *parent = 0); @@ -92,7 +92,7 @@ private: friend class KSecretCreateCollectionJob; // filesystem watcher to detect new/removed ksecret files - QFileSystemWatcher m_watcher; + KDirWatch m_watcher; // map of paths pointing to the respective collection objects QMap m_collections; diff --git a/backend/tests/ksecrettest.cpp b/backend/tests/ksecrettest.cpp index 7a17ebc..9397cba 100644 --- a/backend/tests/ksecrettest.cpp +++ b/backend/tests/ksecrettest.cpp @@ -26,6 +26,8 @@ #include #include +#include + Q_DECLARE_METATYPE(BackendCollection*) Q_DECLARE_METATYPE(BackendItem*) @@ -36,7 +38,15 @@ void KSecretTest::initTestCase() QCA::init(); BackendMaster *master = BackendMaster::instance(); master->setUiManager(new NoUiManager); - m_manager = new KSecretCollectionManager("/tmp", master); + // use special test-directory for the .ksecret files + m_manager = new KSecretCollectionManager("share/apps/ksecretservice-test", master); + // remove all files in the resource directory so no previous + // collections are present when performing the test! + QDir dir = QDir(KGlobal::dirs()->saveLocation("ksecret")); + QStringList entries = dir.entryList(QStringList("*.ksecret"), QDir::Files); + Q_FOREACH(const QString &file, entries) { + QVERIFY(dir.remove(file)); + } master->addManager(m_manager); } @@ -72,7 +82,11 @@ void KSecretTest::testCreateCollectionAsync() // TODO: check collection attributes (eg. timestamps) - // TODO: check if the collection has been written to disk + // check that the collection has been written to disk + QStringList entries = QDir(KGlobal::dirs()->saveLocation("ksecret")).entryList( + QStringList("*.ksecret"), QDir::Files); + QCOMPARE(entries.count(), 1); + QCOMPARE(entries.at(0), createColl->collection()->id() + ".ksecret"); // remember the collection m_collection = createColl->collection(); @@ -118,7 +132,7 @@ void KSecretTest::testUnlockCollectionAsync() QTestEventLoop loop; QVERIFY(loop.connect(unlockColl, SIGNAL(result(QueuedJob*)), SLOT(exitLoop()))); unlockColl->enqueue(); - if(!unlockColl->isFinished()) { + if (!unlockColl->isFinished()) { loop.enterLoop(5); } @@ -139,26 +153,155 @@ void KSecretTest::testUnlockCollectionAsync() void KSecretTest::testCreateItemAsync() { + QMap attr; + attr["mainattr"] = "haha"; + QCA::SecureArray array(4, 'c'); + ItemCreateInfo createInfo("testitem", attr, array, false, false, Peer()); + CreateItemJob *createItem = m_collection->createCreateItemJob(createInfo); + QSignalSpy collectionSpy(m_collection, SIGNAL(itemCreated(BackendItem*))); + QTestEventLoop loop; + QVERIFY(loop.connect(createItem, SIGNAL(result(QueuedJob*)), SLOT(exitLoop()))); + createItem->enqueue(); + if (!createItem->isFinished()) { + loop.enterLoop(5); + } + + QVERIFY(createItem->isFinished()); + QCOMPARE(createItem->error(), NoError); + QVERIFY(!createItem->isDismissed()); + QVERIFY(createItem->item() != 0); + + // Verify signals + QCOMPARE(collectionSpy.count(), 1); + QCOMPARE(collectionSpy.takeFirst().at(0).value(), createItem->item()); + + // Check the item is present and alive + QCOMPARE(m_collection->items().value().size(), 1); + QCOMPARE(m_collection->items().value().first(), createItem->item()); + QCOMPARE(m_collection->items().value().first()->secret().value().toByteArray(), QByteArray("cccc")); } void KSecretTest::testReplaceItemAsync() { + QMap attr; + attr["mainattr"] = "haha"; + QCA::SecureArray array(QByteArray("arealsecrete243")); + ItemCreateInfo createInfo("testitem2", attr, array, true, false, Peer()); + CreateItemJob *createItem = m_collection->createCreateItemJob(createInfo); + QSignalSpy collectionSpy(m_collection, SIGNAL(itemChanged(BackendItem*))); + QTestEventLoop loop; + QVERIFY(loop.connect(createItem, SIGNAL(result(QueuedJob*)), SLOT(exitLoop()))); + createItem->enqueue(); + if (!createItem->isFinished()) { + loop.enterLoop(5); + } + + QVERIFY(createItem->isFinished()); + QCOMPARE(createItem->error(), NoError); + QVERIFY(!createItem->isDismissed()); + QVERIFY(createItem->item() != 0); + + // Verify signals + QCOMPARE(collectionSpy.count(), 1); + QCOMPARE(collectionSpy.takeFirst().at(0).value(), createItem->item()); + + // Check the item is present and alive + QCOMPARE(m_collection->items().value().size(), 1); + QCOMPARE(m_collection->items().value().first(), createItem->item()); + QCOMPARE(m_collection->items().value().first()->secret().value().toByteArray(), QByteArray("arealsecrete243")); } void KSecretTest::testDoNotReplaceItemAsync() { + QMap attr; + attr["mainattr"] = "haha"; + QCA::SecureArray array(QByteArray("anothersecret")); + ItemCreateInfo createInfo("testitem3", attr, array, false, false, Peer()); + CreateItemJob *createItem = m_collection->createCreateItemJob(createInfo); + QSignalSpy collectionSpy(m_collection, SIGNAL(itemChanged(BackendItem*))); + QTestEventLoop loop; + QVERIFY(loop.connect(createItem, SIGNAL(result(QueuedJob*)), SLOT(exitLoop()))); + createItem->enqueue(); + if (!createItem->isFinished()) { + loop.enterLoop(5); + } + + QVERIFY(createItem->isFinished()); + QCOMPARE(createItem->error(), ErrorAlreadyExists); + QVERIFY(!createItem->isDismissed()); + QVERIFY(createItem->item() == 0); + + // Verify signals + QCOMPARE(collectionSpy.count(), 0); + + // Check the item is present and alive + QCOMPARE(m_collection->items().value().size(), 1); + QCOMPARE(m_collection->items().value().first()->secret().value().toByteArray(), QByteArray("arealsecrete243")); } void KSecretTest::testDeleteItemAsync() { + BackendItem *item = m_collection->items().value().first(); + ItemDeleteInfo deleteInfo = ItemDeleteInfo(Peer()); + DeleteItemJob *deleteItem = item->createDeleteJob(deleteInfo); + QSignalSpy collectionSpy(m_collection, SIGNAL(itemDeleted(BackendItem*))); + QTestEventLoop loop; + QVERIFY(loop.connect(deleteItem, SIGNAL(result(QueuedJob*)), SLOT(exitLoop()))); + deleteItem->enqueue(); + if (!deleteItem->isFinished()) { + loop.enterLoop(5); + } + + QVERIFY(deleteItem->isFinished()); + QCOMPARE(deleteItem->error(), NoError); + QVERIFY(deleteItem->result()); + QVERIFY(!deleteItem->isDismissed()); + + // Verify signals + QCOMPARE(collectionSpy.count(), 1); + QCOMPARE(collectionSpy.takeFirst().at(0).value()->label().value(), QString("testitem2")); + + // check if the item is gone + QVERIFY(m_collection->items().value().isEmpty()); } void KSecretTest::testDeleteCollectionAsync() { + BackendMaster *master = BackendMaster::instance(); + CollectionDeleteInfo deleteInfo = CollectionDeleteInfo(Peer()); + DeleteCollectionJob *deleteCollection = m_collection->createDeleteJob(deleteInfo); + QSignalSpy managerSpy(m_manager, SIGNAL(collectionDeleted(BackendCollection*))); + QSignalSpy masterSpy(master, SIGNAL(collectionDeleted(BackendCollection*))); + QTestEventLoop loop; + QVERIFY(loop.connect(deleteCollection, SIGNAL(result(QueuedJob*)), SLOT(exitLoop()))); + deleteCollection->enqueue(); + if (!deleteCollection->isFinished()) { + loop.enterLoop(5); + } + + QVERIFY(deleteCollection->isFinished()); + QCOMPARE(deleteCollection->error(), NoError); + QVERIFY(deleteCollection->result()); + QVERIFY(!deleteCollection->isDismissed()); + + // Verify signals + QCOMPARE(managerSpy.count(), 1); + QCOMPARE(managerSpy.takeFirst().at(0).value(), m_collection); + QCOMPARE(masterSpy.count(), 1); + QCOMPARE(masterSpy.takeFirst().at(0).value(), m_collection); + + // check the collection is dead + QVERIFY(master->collections().isEmpty()); + + // check that the collection has been removed from disk + QStringList entries = QDir(KGlobal::dirs()->saveLocation("ksecret")).entryList( + QStringList("*.ksecret"), QDir::Files); + QCOMPARE(entries.count(), 0); } void KSecretTest::cleanupTestCase() { + // TODO: delete stuff so this can also be used for valgrind leak-checking } QTEST_MAIN(KSecretTest) diff --git a/backend/tests/temporarytest.cpp b/backend/tests/temporarytest.cpp index 2a895a3..ba5ac6c 100644 --- a/backend/tests/temporarytest.cpp +++ b/backend/tests/temporarytest.cpp @@ -363,7 +363,7 @@ void TemporaryTest::testDeleteItemAsync() QCOMPARE(collectionSpy.count(), 1); QCOMPARE(collectionSpy.takeFirst().at(0).value()->label().value(), QString("testitem2")); - // Check the item is present and alive + // Check if item is gone QVERIFY(collection->items().value().isEmpty()); }