[prev in list] [next in list] [prev in thread] [next in thread]
List: kde-commits
Subject: [ksecrets] /: Adding the ksecrets_store tests
From: Valentin Rusu <kde () rusu ! info>
Date: 2015-08-13 15:47:31
Message-ID: E1ZPuj5-00028K-Ew () scm ! kde ! org
[Download RAW message or body]
Git commit 92288f8c476933dfb7d5e37f753c801564c74ea9 by Valentin Rusu.
Committed on 13/08/2015 at 15:47.
Pushed by vrusu into branch 'master'.
Adding the ksecrets_store tests
All now set for secrets store development
M +302 -16 autotests/ksecrets_store/ksecrets_store_test.cpp
M +7 -2 autotests/ksecrets_store/ksecrets_store_test.h
M +1 -0 src/runtime/ksecrets_store/ksecrets_credentials.cpp
M +91 -0 src/runtime/ksecrets_store/ksecrets_store.cpp
M +42 -16 src/runtime/ksecrets_store/ksecrets_store.h
http://commits.kde.org/ksecrets/92288f8c476933dfb7d5e37f753c801564c74ea9
diff --git a/autotests/ksecrets_store/ksecrets_store_test.cpp \
b/autotests/ksecrets_store/ksecrets_store_test.cpp index 1a27431..821699e 100644
--- a/autotests/ksecrets_store/ksecrets_store_test.cpp
+++ b/autotests/ksecrets_store/ksecrets_store_test.cpp
@@ -54,22 +54,308 @@ void KSecretServiceStoreTest::initTestCase()
QVERIFY(credfut.get());
}
-void KSecretServiceStoreTest::testCreateCollection() { QVERIFY(false); }
-void KSecretServiceStoreTest::testCreateItem() { QVERIFY(false); }
-void KSecretServiceStoreTest::testSearchItem() { QVERIFY(false); }
-void KSecretServiceStoreTest::testItem() { QVERIFY(false); }
-void KSecretServiceStoreTest::testDeleteItem() { QVERIFY(false); }
-void KSecretServiceStoreTest::testDirCollections() { QVERIFY(false); }
-void KSecretServiceStoreTest::testReadCollection() { QVERIFY(false); }
-void KSecretServiceStoreTest::testDeleteCollection() { QVERIFY(false); }
-void KSecretServiceStoreTest::cleanupTestCase() { QDir::home().remove(secretsFilePath); }
+static const char* collName1 = "test collection1";
+static const char* collName2 = "test collection2";
+std::time_t createTimeMark;
+
+void KSecretServiceStoreTest::testCreateCollection()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData(), false);
+
+ createTimeMark = std::time(nullptr);
+ auto crval1 = backend.createCollection(collName1);
+ QVERIFY(crval1);
+
+ auto crval2 = backend.createCollection(collName2);
+ QVERIFY(crval2);
+
+ auto crval3 = backend.createCollection(collName1);
+ QVERIFY(!crval3); // should not work as a collection named collName1 is already present
+
+ auto coll2 = crval2.result_;
+ QVERIFY(coll2.get() != nullptr);
+ QVERIFY(coll2->createdTime() > createTimeMark);
+ QVERIFY(coll2->modifiedTime() > createTimeMark);
+ QVERIFY(coll2->createdTime() == coll2->modifiedTime());
+ QVERIFY(coll2->label() == collName2);
+
+ auto sres = coll2->dirItems();
+ QVERIFY(sres.size() == 0);
+
+ KSecretsStore::AttributesMap noAttrs;
+ sres = coll2->searchItems(noAttrs);
+ QVERIFY(sres.size() == 0);
+
+ sres = coll2->searchItems("", noAttrs);
+ QVERIFY(sres.size() == 0);
+
+ sres = coll2->searchItems(nullptr);
+ QVERIFY(sres.size() == 0);
+
+ sres = coll2->searchItems(nullptr, noAttrs);
+ QVERIFY(sres.size() == 0);
+}
+
+void KSecretServiceStoreTest::testCreateCollectionFailOnReadonly()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData());
+
+ createTimeMark = std::time(nullptr);
+ auto crval1 = backend.createCollection(collName1);
+ QVERIFY(!crval1);
+}
+
+void KSecretServiceStoreTest::testDirCollections()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData(), false);
+
+ auto dirres = backend.dirCollections();
+ QVERIFY(dirres);
+ auto colList = dirres.result_;
+ QVERIFY(colList.size() == 2); // we created two collections at testCreateCollection
+ QVERIFY(colList[0] == collName1);
+ QVERIFY(colList[1] == collName2);
+}
+
+void KSecretServiceStoreTest::testReadCollection()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData());
+
+ auto rres = backend.readCollection(collName2);
+ QVERIFY(rres);
+ auto coll = rres.result_;
+ QVERIFY(coll.get() != nullptr);
+ QVERIFY(coll->label() == collName2);
+}
+
+const char *itemName1 = "item1";
+const char *itemName2 = "item2";
+const char *itemName3 = "item3";
+const char *itemNamesWildcard = "item*";
+KSecretsStore::ItemValue emptyValue;
+
+void KSecretServiceStoreTest::testCreateItem()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData(), false);
+
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll = rres.result_;
+ QVERIFY(coll.get() != nullptr);
+
+ auto sres = coll->dirItems();
+ QVERIFY(sres.size() == 0);
+
+ KSecretsStore::AttributesMap emptyAttrs;
+ auto item = coll->createItem(itemName1, emptyAttrs, emptyValue);
+ QVERIFY(item.get() != nullptr);
+
+ auto cres1 = coll->createItem(itemName1, emptyAttrs, emptyValue);
+ QVERIFY(cres1.get() == nullptr); // second time should fail
+
+ QVERIFY(item->label() == collName1);
+ QVERIFY(item->value() == emptyValue);
+ QVERIFY(item->attributes() == emptyAttrs);
+
+ auto item2 = coll->createItem(itemName2, emptyValue);
+ QVERIFY(item2.get() != nullptr);
+ QVERIFY(item->label() == itemName2);
+ QVERIFY(item->value() == emptyValue);
+ QVERIFY(item->attributes() == emptyAttrs);
+
+ std::string testContents = std::string("some test contents");
+ KSecretsStore::ItemValue someValue;
+ someValue.contentType = "test-data";
+ someValue.contents.assign(testContents.begin(), testContents.end());
+ auto item3 = coll->createItem(itemName3, someValue);
+ QVERIFY(item3.get() != nullptr);
+ QVERIFY(item->label() == itemName3);
+ QVERIFY(item->value() == someValue);
+ QVERIFY(item->attributes() == emptyAttrs);
+}
+
+void KSecretServiceStoreTest::testCreateItemFailOnReadonly()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData());
+
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll = rres.result_;
+ QVERIFY(coll.get() != nullptr);
+
+ KSecretsStore::AttributesMap emptyAttrs;
+ auto item = coll->createItem(itemName1, emptyAttrs, emptyValue);
+ QVERIFY(item.get() == nullptr);
+}
+
+void KSecretServiceStoreTest::testSearchItem()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData());
+ // this test expends previous testCreateItem put in some items in collection named \
collName1 + // so first load that collection
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll = rres.result_;
+ QVERIFY(coll.get() != nullptr);
+
+ auto sres = coll->dirItems();
+ QVERIFY(sres.size() == 0);
-// void KSecretServiceStoreTest::testOpen()
-// {
-// KSecretsStore backend;
-// auto setupfut =
-// backend.setup(secretsFilePath.toLocal8Bit().constData());
-// QVERIFY(setupfut.get());
-// }
+ // ok, now the test
+ auto list1 = coll->searchItems(itemName1);
+ QVERIFY(list1.size() == 1); // we used exact match so it should find one item
+ QVERIFY(list1.front()->label() == itemName1);
+
+ auto listN = coll->searchItems(itemNamesWildcard);
+ QVERIFY(listN.size() == 3); // we should find all 3 items here
+
+ auto list3 = coll->searchItems(itemName3);
+ QVERIFY(list3.size() == 1);
+ QVERIFY(list1.front()->label() == itemName3);
+}
+
+void KSecretServiceStoreTest::testItem() {
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData(), false);
+ // this test expends previous testCreateItem put in some items in collection named \
collName1 + // so first load that collection
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll = rres.result_;
+ QVERIFY(coll.get() != nullptr);
+
+ auto list1 = coll->searchItems(itemName1);
+ QVERIFY(list1.size() == 1); // we used exact match so it should find one item
+ QVERIFY(list1.front()->label() == itemName1);
+
+ // ok, now the test
+ auto item = list1.front();
+
+ QVERIFY(item->createdTime() > createTimeMark);
+ QVERIFY(item->modifiedTime() > createTimeMark);
+ const char* newLabel1 = "new label 1";
+ QVERIFY(item->setLabel(newLabel1));
+ QVERIFY(item->label() == newLabel1);
+
+ const char *newContentType = "changed-content-type";
+ std::string newContents = "some other contents";
+ auto val = item->value();
+ val.contentType = newContentType;
+ val.contents.assign(newContents.begin(), newContents.end());
+ QVERIFY(item->setValue(val));
+
+ auto val1 = item->value();
+ QVERIFY(val1 == val);
+
+}
+
+void KSecretServiceStoreTest::testItemModifyFailOnReadonly()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData());
+ // this test expends previous testCreateItem put in some items in collection named \
collName1 + // so first load that collection
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll = rres.result_;
+ QVERIFY(coll.get() != nullptr);
+
+ auto list1 = coll->searchItems(itemName1);
+ QVERIFY(list1.size() == 1); // we used exact match so it should find one item
+ QVERIFY(list1.front()->label() == itemName1);
+
+ // ok, now the test
+ auto item = list1.front();
+ QVERIFY(!item->setLabel("dummy"));
+
+ auto val = item->value();
+ val.contentType = "new-content-type";
+ QVERIFY(!item->setValue(val));
+
+ auto attrs = item->attributes();
+ attrs.emplace("test.kde.org", "test attr");
+ QVERIFY(!item->setAttributes(attrs));
+}
+
+void KSecretServiceStoreTest::testDeleteItem() {
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData(), false);
+
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll1 = rres.result_;
+
+ auto dres1 = coll1->dirItems();
+
+ auto il1 = coll1->searchItems(itemName2);
+ QVERIFY(il1.size() == 1);
+ QVERIFY(coll1->deleteItem(il1.front()));
+
+ auto dres2 = coll1->dirItems();
+ QVERIFY(dres2.size() < dres1.size());
+
+ il1 = coll1->searchItems(itemName2);
+ QVERIFY(il1.size() == 0);
+}
+
+void KSecretServiceStoreTest::testDeleteItemFailOnReadonly() {
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData());
+
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll1 = rres.result_;
+
+ auto dres1 = coll1->dirItems();
+
+ auto il1 = coll1->searchItems(itemName2);
+ QVERIFY(il1.size() == 1);
+ QVERIFY(!coll1->deleteItem(il1.front()));
+
+ auto dres2 = coll1->dirItems();
+ QVERIFY(dres2.size() == dres1.size());
+}
+
+void KSecretServiceStoreTest::testDeleteCollection()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData(), false);
+
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll1 = rres.result_;
+ auto dres = backend.deleteCollection(coll1);
+ QVERIFY(dres);
+
+ // try to read it again
+ rres = backend.readCollection(collName1);
+ QVERIFY(!rres);
+}
+
+void KSecretServiceStoreTest::testDeleteCollectionFailOnReadonly()
+{
+ KSecretsStore backend;
+ auto setupfut = backend.setup(secretsFilePath.toLocal8Bit().constData());
+
+ auto list1 = backend.dirCollections().result_;
+
+ auto rres = backend.readCollection(collName1);
+ QVERIFY(rres);
+ auto coll1 = rres.result_;
+ auto dres = backend.deleteCollection(coll1);
+ QVERIFY(!dres);
+
+ auto list2 = backend.dirCollections().result_;
+ QVERIFY(list1 == list2);
+}
+
+void KSecretServiceStoreTest::cleanupTestCase() { QDir::home().remove(secretsFilePath); }
// vim: tw=220 ts=4
diff --git a/autotests/ksecrets_store/ksecrets_store_test.h \
b/autotests/ksecrets_store/ksecrets_store_test.h index a9a68ed..5e332a0 100644
--- a/autotests/ksecrets_store/ksecrets_store_test.h
+++ b/autotests/ksecrets_store/ksecrets_store_test.h
@@ -30,13 +30,18 @@ public:
private Q_SLOTS:
void initTestCase();
void testCreateCollection();
+ void testCreateCollectionFailOnReadonly();
+ void testDirCollections();
+ void testReadCollection();
void testCreateItem();
+ void testCreateItemFailOnReadonly();
void testSearchItem();
void testItem();
+ void testItemModifyFailOnReadonly();
void testDeleteItem();
- void testDirCollections();
- void testReadCollection();
+ void testDeleteItemFailOnReadonly();
void testDeleteCollection();
+ void testDeleteCollectionFailOnReadonly();
void cleanupTestCase();
};
diff --git a/src/runtime/ksecrets_store/ksecrets_credentials.cpp \
b/src/runtime/ksecrets_store/ksecrets_credentials.cpp index b59352e..135db0a 100644
--- a/src/runtime/ksecrets_store/ksecrets_credentials.cpp
+++ b/src/runtime/ksecrets_store/ksecrets_credentials.cpp
@@ -191,6 +191,7 @@ int KSECRETS_STORE_EXPORT kss_can_change_password()
extern "C"
int KSECRETS_STORE_EXPORT kss_change_password(const char* new_password)
{
+ UNUSED(new_password);
syslog(LOG_INFO, "kss_change_password");
return TRUE;
}
diff --git a/src/runtime/ksecrets_store/ksecrets_store.cpp \
b/src/runtime/ksecrets_store/ksecrets_store.cpp index edeb777..290ee00 100644
--- a/src/runtime/ksecrets_store/ksecrets_store.cpp
+++ b/src/runtime/ksecrets_store/ksecrets_store.cpp
@@ -230,4 +230,95 @@ KSecretsStore::DeleteCollectionResult \
KSecretsStore::deleteCollection(const char // TODO
return DeleteCollectionResult();
}
+
+std::time_t KSecretsStore::Collection::createdTime() const {
+ // TODO
+ return std::time_t();
+}
+
+std::time_t KSecretsStore::Collection::modifiedTime() const {
+ //TODO
+ return std::time_t();
+}
+
+std::string KSecretsStore::Collection::label() const {
+ // TODO
+ return "";
+}
+
+KSecretsStore::Collection::ItemList KSecretsStore::Collection::dirItems() const {
+ // TODO
+ return ItemList();
+}
+
+KSecretsStore::Collection::ItemList KSecretsStore::Collection::searchItems(const AttributesMap \
&) const { + // TODO
+ return ItemList();
+}
+
+KSecretsStore::Collection::ItemList KSecretsStore::Collection::searchItems(const char*, const \
AttributesMap &) const { + // TODO
+ return ItemList();
+}
+
+KSecretsStore::Collection::ItemList KSecretsStore::Collection::searchItems(const char*) const \
{ + // TODO
+ return ItemList();
+}
+
+KSecretsStore::ItemPtr KSecretsStore::Collection::createItem(const char*, AttributesMap, \
ItemValue) { + // TODO
+ return ItemPtr();
+}
+
+bool KSecretsStore::Collection::deleteItem(ItemPtr) {
+ // TODO
+ return false;
+}
+
+KSecretsStore::ItemPtr KSecretsStore::Collection::createItem(const char*, ItemValue) {
+ // TODO
+ return ItemPtr();
+}
+
+std::time_t KSecretsStore::Item::createdTime() const {
+ // TODO
+ return std::time_t();
+}
+
+std::time_t KSecretsStore::Item::modifiedTime() const {
+ // TODO
+ return std::time_t();
+}
+
+std::string KSecretsStore::Item::label() const {
+ // TODO
+ return "";
+}
+
+bool KSecretsStore::Item::setLabel(const char*) {
+ // TODO
+ return false;
+}
+
+KSecretsStore::ItemValue KSecretsStore::Item::value() const {
+ // TODO
+ return ItemValue();
+}
+
+bool KSecretsStore::Item::setValue(ItemValue) {
+ // TODO
+ return false;
+}
+
+KSecretsStore::AttributesMap KSecretsStore::Item::attributes() const {
+ // TODO
+ return AttributesMap();
+}
+
+bool KSecretsStore::Item::setAttributes(AttributesMap) {
+ // TODO
+ return false;
+}
+
// vim: tw=220:ts=4
diff --git a/src/runtime/ksecrets_store/ksecrets_store.h \
b/src/runtime/ksecrets_store/ksecrets_store.h index dacdedc..19b8632 100644
--- a/src/runtime/ksecrets_store/ksecrets_store.h
+++ b/src/runtime/ksecrets_store/ksecrets_store.h
@@ -81,6 +81,7 @@ public:
struct ItemValue {
std::string contentType;
std::vector<char> contents;
+ bool operator == (const ItemValue& that) const noexcept { return contentType == \
that.contentType && contents == that.contents; } };
/* Holds a secret value.
@@ -92,6 +93,7 @@ public:
* @see Collection
*/
class Item {
+ public:
Item(const Item&) = default;
Item& operator=(const Item&) = default;
@@ -99,10 +101,18 @@ public:
bool setLabel(const char*) noexcept;
AttributesMap attributes() const;
- void setAttributes(AttributesMap&&) noexcept;
+ /**
+ * @brief
+ *
+ * @note This method uses C++11 move semantics so the AttributesMap local variable \
used to call this member + * will no longer be valid upon call return.
+ *
+ * @param AttributesMap
+ */
+ bool setAttributes(AttributesMap) noexcept;
ItemValue value() const noexcept;
- bool setValue(ItemValue&&) noexcept;
+ bool setValue(ItemValue) noexcept;
std::time_t createdTime() const noexcept;
std::time_t modifiedTime() const noexcept;
@@ -132,9 +142,7 @@ public:
* the items having attribute value containing that partially specified value.
*/
class Collection {
- Collection(const Collection&) = default;
- Collection& operator=(const Collection&) = default;
-
+ public:
std::string label() const noexcept;
bool setLabel(const char*) noexcept;
@@ -142,9 +150,10 @@ public:
std::time_t modifiedTime() const noexcept;
using ItemList = std::vector<ItemPtr>;
- ItemList searchItems(AttributesMap&&) noexcept;
- ItemList searchItems(const char*) noexcept;
- ItemList searchItems(const char*, AttributesMap&&) noexcept;
+ ItemList dirItems() const noexcept;
+ ItemList searchItems(const AttributesMap&) const noexcept;
+ ItemList searchItems(const char*) const noexcept;
+ ItemList searchItems(const char*, const AttributesMap&) const noexcept;
/**
* Creates an item in the collection in one go. This is more efficient than
@@ -152,11 +161,14 @@ public:
* the value. The returned item may still be modified, keep in mind that each
* method call will trigger an store file update.
*
+ * @note This member uses C++11 move semantics for the AttributesMap and ItemValue
+ *
* @return ItemPtr which can be empty if creating the item was not
* possible. So please check it via it's operator bool() before using
- * it.
+ * it. Open possible failure reason is that an item with the same label already
+ * exists in the collection.
*/
- ItemPtr createItem(const char*, AttributesMap&&, ItemValue&&) noexcept;
+ ItemPtr createItem(const char*, AttributesMap, ItemValue) noexcept;
/**
* Convenience method for creating items without supplemental
* attributes.
@@ -165,12 +177,14 @@ public:
* possible. So please check it via it's operator bool() before using
* it.
*/
- ItemPtr createItem(const char* label, ItemValue&&) noexcept;
+ ItemPtr createItem(const char* label, ItemValue) noexcept;
bool deleteItem(ItemPtr) noexcept;
protected:
Collection();
+ Collection(const Collection&) = default;
+ Collection& operator=(const Collection&) = default;
friend class KSecretsStore;
private:
@@ -222,11 +236,15 @@ public:
operator bool() const { return status_ == G; }
};
+ template <typename T> struct AlwaysGoodPred { bool operator()(const T&) const noexcept { \
return true; }}; /**
- * @brief Small structure returned by API calls that create things
+ * @brief Small structure returned by API calls that create things. It's possible to check \
the returned + * value by giving this template a custom OK_PRED of type equivalent \
to std::function<bool(const R&)>
*/
- template <StoreStatus G, typename R> struct CallResultWithValue : public CallResult<G> {
+ template <StoreStatus G, typename R, typename OK_PRED = AlwaysGoodPred<R> >
+ struct CallResultWithValue : public CallResult<G> {
R result_;
+ operator bool() const noexcept { return CallResult<G>::operator bool() && \
OK_PRED()(result_); } };
using SetupResult = CallResult<StoreStatus::Good>;
@@ -259,7 +277,8 @@ public:
using DirCollectionsResult = CallResultWithValue<StoreStatus::Good, CollectionNames>;
DirCollectionsResult dirCollections() const noexcept;
- using CreateCollectionResult = CallResultWithValue<StoreStatus::Good, CollectionPtr>;
+ template <typename P> struct IsGoodSmartPtr { bool operator()(const P& p) { return p.get() \
!= nullptr; }}; + using CreateCollectionResult = CallResultWithValue<StoreStatus::Good, \
CollectionPtr, IsGoodSmartPtr<CollectionPtr> >; /**
* @return CollectionPtr which can empty if the call did not succeed
* Please check that with operator bool()
@@ -268,7 +287,7 @@ public:
*/
CreateCollectionResult createCollection(const char*) noexcept;
- using ReadCollectionResult = CallResultWithValue<StoreStatus::Good, CollectionPtr>;
+ using ReadCollectionResult = CallResultWithValue<StoreStatus::Good, CollectionPtr, \
IsGoodSmartPtr<CollectionPtr> >; /**
* @return CollectionPtr which can empty if the call did not succeed, e.g.
* the collection was not found
@@ -276,7 +295,12 @@ public:
*/
ReadCollectionResult readCollection(const char*) const noexcept;
- using DeleteCollectionResult = CallResultWithValue<StoreStatus::Good, bool>;
+ /**
+ * @brief Return value for deleteCollection method variants. Please note the operator \
bool() can be used to both + * check the collection's status after the deleteCollection \
call, and the result of the delete operation, as it's + * using the `bool` specialized \
version of the IsGoodPred + */
+ using DeleteCollectionResult = CallResultWithValue<StoreStatus::Good, bool, \
AlwaysGoodPred<bool> >; DeleteCollectionResult deleteCollection(CollectionPtr) noexcept;
DeleteCollectionResult deleteCollection(const char*) noexcept;
@@ -284,5 +308,7 @@ private:
std::unique_ptr<KSecretsStorePrivate> d;
};
+template <> struct KSecretsStore::AlwaysGoodPred<bool> { bool operator()(const bool& b) const \
noexcept { return b; }}; +
#endif
// vim: tw=220:ts=4
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic