diff --git a/discover/autotests/updateandinstall.qml b/discover/autotests/updateandinstall.qml index 8c0b9bca..46a918fe 100644 --- a/discover/autotests/updateandinstall.qml +++ b/discover/autotests/updateandinstall.qml @@ -1,43 +1,45 @@ import QtQuick 2.0 import org.kde.discover.app 1.0 import QtTest 1.1 DiscoverTest { function test_openResource() { app.openMode("Update"); {// we start an upate var updatePage = appRoot.stack.currentItem; compare(typeName(updatePage), "UpdatesPage") compare(updatePage.state, "has-updates", "to update") var button = findChild(updatePage, "Button") verify(button); button.clicked(); compare(updatePage.state, "has-updates", "updating") } {//we start installing a resource app.openApplication("dummy://dummy.1"); verify(waitForSignal(appRoot.stack, "currentItemChanged")) var button = findChild(appRoot.stack.currentItem, "InstallApplicationButton") console.log("button", appRoot.stack.currentItem, button) verify(button) verify(!button.isActive) button.click() } app.openMode("Update"); { var updatePage = appRoot.stack.currentItem; compare(typeName(updatePage), "UpdatesPage") - compare(updatePage.state, "has-updates", "to update") + while(updatePage.state === "fetching") + waitForSignal(updatePage, "stateChanged") + compare(updatePage.state, "now-uptodate", "to update") var button = findChild(updatePage, "Button") verify(!button.isActive) } while(updatePage.state != "now-uptodate") waitForSignal(updatePage, "stateChanged") } } diff --git a/libdiscover/appstream/OdrsReviewsBackend.cpp b/libdiscover/appstream/OdrsReviewsBackend.cpp index b009e711..dc7073ff 100644 --- a/libdiscover/appstream/OdrsReviewsBackend.cpp +++ b/libdiscover/appstream/OdrsReviewsBackend.cpp @@ -1,364 +1,364 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * Copyright © 2017 Jan Grulich * * * * 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 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of 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 . * ***************************************************************************/ #include "OdrsReviewsBackend.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // #define APIURL "http://127.0.0.1:5000/1.0/reviews/api" #define APIURL "https://odrs.gnome.org/1.0/reviews/api" OdrsReviewsBackend::OdrsReviewsBackend(AbstractResourcesBackend *parent) : AbstractReviewsBackend(parent) , m_isFetching(false) , m_nam(new QNetworkAccessManager(this)) { bool fetchRatings = false; const QUrl ratingsUrl(QStringLiteral(APIURL "/ratings")); const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/ratings/ratings")); const QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); // Create $HOME/.cache/discover/ratings folder cacheDir.mkdir(QStringLiteral("ratings")); if (QFileInfo::exists(fileUrl.toLocalFile())) { QFileInfo file(fileUrl.toLocalFile()); // Refresh the cached ratings if they are older than one day if (file.lastModified().msecsTo(QDateTime::currentDateTime()) > 1000 * 60 * 60 * 24) { fetchRatings = true; } } else { fetchRatings = true; } if (fetchRatings) { m_isFetching = true; KIO::FileCopyJob *getJob = KIO::file_copy(ratingsUrl, fileUrl, -1, KIO::Overwrite | KIO::HideProgressInfo); connect(getJob, &KIO::FileCopyJob::result, this, &OdrsReviewsBackend::ratingsFetched); } else { parseRatings(); } } void OdrsReviewsBackend::ratingsFetched(KJob *job) { m_isFetching = false; if (job->error()) { qWarning() << "Failed to fetch ratings " << job->errorString(); } else { parseRatings(); } } static QString osName() { QString osReleaseFilename; if (QFileInfo::exists(QStringLiteral("/etc/os-release"))) { osReleaseFilename = QStringLiteral("/etc/os-release"); } else if (QFileInfo::exists(QStringLiteral("/usr/lib/os-release"))) { osReleaseFilename = QStringLiteral("/usr/lib/os-release"); } if (osReleaseFilename.isEmpty()) { return QStringLiteral("Unknown"); } QFile osReleaseFile(osReleaseFilename); if (osReleaseFile.open(QIODevice::ReadOnly)) { QString line; QTextStream stream(&osReleaseFile); while (stream.readLineInto(&line)) { if (line.startsWith(QStringLiteral("NAME"))) { QStringRef name = line.midRef(5).trimmed(); if (name.startsWith(QLatin1Char('\"')) && name.endsWith(QLatin1Char('\"'))) name = name.mid(1, name.size()-2); return name.toString(); } } } return QStringLiteral("Unknown"); } static QString userHash() { QString machineId; QFile file(QStringLiteral("/etc/machine-id")); if (file.open(QIODevice::ReadOnly)) { machineId = QString::fromUtf8(file.readAll()); file.close(); } if (machineId.isEmpty()) { return QString(); } QString salted = QStringLiteral("gnome-software[%1:%2]").arg(KUser().loginName()).arg(machineId); return QString::fromUtf8(QCryptographicHash::hash(salted.toUtf8(), QCryptographicHash::Sha1).toHex()); } void OdrsReviewsBackend::fetchReviews(AbstractResource *app, int page) { Q_UNUSED(page) m_isFetching = true; // Check cached reviews const QString fileName = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/reviews/%1.json").arg(app->appstreamId()); if (QFileInfo::exists(fileName)) { QFileInfo file(fileName); // Check if the reviews are not older than a week msecs * secs * hours * days if (file.lastModified().msecsTo(QDateTime::currentDateTime()) < 1000 * 60 * 60 * 24 * 7 ) { QFile reviewFile(fileName); if (reviewFile.open(QIODevice::ReadOnly)) { QByteArray reviews = reviewFile.readAll(); QJsonDocument document = QJsonDocument::fromJson(reviews); parseReviews(document, app); return; } } } const QJsonDocument document(QJsonObject{ {QStringLiteral("app_id"), app->appstreamId()}, {QStringLiteral("distro"), osName()}, {QStringLiteral("user_hash"), userHash()}, {QStringLiteral("version"), app->isInstalled() ? app->installedVersion() : app->availableVersion()}, {QStringLiteral("locale"), QLocale::system().name()}, {QStringLiteral("limit"), 0} }); QNetworkRequest request(QUrl(QStringLiteral(APIURL "/fetch"))); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); request.setHeader(QNetworkRequest::ContentLengthHeader, document.toJson().size()); // Store reference to the app for which we request reviews request.setOriginatingObject(app); auto reply = m_nam->post(request, document.toJson()); connect(reply, &QNetworkReply::finished, this, &OdrsReviewsBackend::reviewsFetched); } void OdrsReviewsBackend::reviewsFetched() { QNetworkReply* reply = qobject_cast(sender()); if (reply->error() == QNetworkReply::NoError) { QByteArray data = reply->readAll(); const QJsonDocument document = QJsonDocument::fromJson(data); AbstractResource *resource = qobject_cast(reply->request().originatingObject()); Q_ASSERT(resource); parseReviews(document, resource); // Store reviews to cache so we don't need to download them all the time if (document.array().isEmpty()) { return; } QJsonObject jsonObject = document.array().first().toObject(); if (jsonObject.isEmpty()) { return; } QFile file(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/reviews/%1.json").arg(jsonObject.value(QStringLiteral("app_id")).toString())); if (file.open(QIODevice::WriteOnly)) { file.write(document.toJson()); file.close(); } } else { m_isFetching = false; } } Rating * OdrsReviewsBackend::ratingForApplication(AbstractResource *app) const { - if (app->isTechnical()) { + if (app->appstreamId().isEmpty()) { return nullptr; } return m_ratings[app->appstreamId()]; } void OdrsReviewsBackend::submitUsefulness(Review *review, bool useful) { const QJsonDocument document(QJsonObject{ {QStringLiteral("app_id"), review->applicationName()}, {QStringLiteral("user_skey"), review->getMetadata(QStringLiteral("ODRS::user_skey")).toString()}, {QStringLiteral("user_hash"), userHash()}, {QStringLiteral("distro"), osName()}, {QStringLiteral("review_id"), QJsonValue(double(review->id()))} //if we really need uint64 we should get it in QJsonValue }); QNetworkRequest request(QUrl(QStringLiteral(APIURL "/%1").arg(useful ? QStringLiteral("upvote") : QStringLiteral("downvote")))); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); request.setHeader(QNetworkRequest::ContentLengthHeader, document.toJson().size()); auto reply = m_nam->post(request, document.toJson()); connect(reply, &QNetworkReply::finished, this, &OdrsReviewsBackend::usefulnessSubmitted); } void OdrsReviewsBackend::usefulnessSubmitted() { QNetworkReply* reply = qobject_cast(sender()); if (reply->error() == QNetworkReply::NoError) { qWarning() << "Usefullness submitted"; } else { qWarning() << "Failed to submit usefulness: " << reply->errorString(); } } QString OdrsReviewsBackend::userName() const { return i18n("%1 (%2)", KUser().property(KUser::FullName).toString(), KUser().loginName()); } void OdrsReviewsBackend::submitReview(AbstractResource *res, const QString &summary, const QString &description, const QString &rating) { QJsonObject map = {{QStringLiteral("app_id"), res->appstreamId()}, {QStringLiteral("user_skey"), res->getMetadata(QStringLiteral("ODRS::user_skey")).toString()}, {QStringLiteral("user_hash"), userHash()}, {QStringLiteral("version"), res->isInstalled() ? res->installedVersion() : res->availableVersion()}, {QStringLiteral("locale"), QLocale::system().name()}, {QStringLiteral("distro"), osName()}, {QStringLiteral("user_display"), QJsonValue::fromVariant(KUser().property(KUser::FullName))}, {QStringLiteral("summary"), summary}, {QStringLiteral("description"), description}, {QStringLiteral("rating"), rating.toInt() * 10}}; const QJsonDocument document(map); QNetworkAccessManager *accessManager = new QNetworkAccessManager(this); QNetworkRequest request(QUrl(QStringLiteral(APIURL "/submit"))); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); request.setHeader(QNetworkRequest::ContentLengthHeader, document.toJson().size()); // Store what we need so we can immediately show our review once it is submitted // Use review_id 0 for now as odrs starts numbering from 1 and once reviews are re-downloaded we get correct id map.insert(QStringLiteral("review_id"), 0); res->addMetadata(QStringLiteral("ODRS::review_map"), map); request.setOriginatingObject(res); accessManager->post(request, document.toJson()); connect(accessManager, &QNetworkAccessManager::finished, this, &OdrsReviewsBackend::reviewSubmitted); } void OdrsReviewsBackend::reviewSubmitted(QNetworkReply *reply) { if (reply->error() == QNetworkReply::NoError) { qWarning() << "Review submitted"; AbstractResource *resource = qobject_cast(reply->request().originatingObject()); const QJsonArray array = {resource->getMetadata(QStringLiteral("ODRS::review_map")).toObject()}; QJsonDocument document = QJsonDocument(array); // Remove local file with reviews so we can re-download it next time to get our review QFile file(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/reviews/%1.json").arg(array.first().toObject().value(QStringLiteral("app_id")).toString())); file.remove(); parseReviews(document, resource); } else { qWarning() << "Failed to submit review: " << reply->errorString(); } } void OdrsReviewsBackend::parseRatings() { QFile ratingsDocument(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/ratings/ratings")); if (ratingsDocument.open(QIODevice::ReadOnly)) { QJsonDocument jsonDocument = QJsonDocument::fromJson(ratingsDocument.readAll()); QJsonObject jsonObject = jsonDocument.object(); for (auto it = jsonObject.begin(); it != jsonObject.end(); it++) { QJsonObject appJsonObject = it.value().toObject(); const int ratingCount = appJsonObject.value(QLatin1String("total")).toInt(); QVariantMap ratingMap = { { QStringLiteral("star0"), appJsonObject.value(QLatin1String("star0")).toInt() }, { QStringLiteral("star1"), appJsonObject.value(QLatin1String("star1")).toInt() }, { QStringLiteral("star2"), appJsonObject.value(QLatin1String("star2")).toInt() }, { QStringLiteral("star3"), appJsonObject.value(QLatin1String("star3")).toInt() }, { QStringLiteral("star4"), appJsonObject.value(QLatin1String("star4")).toInt() }, { QStringLiteral("star5"), appJsonObject.value(QLatin1String("star5")).toInt() } }; Rating *rating = new Rating(it.key(), ratingCount, ratingMap); m_ratings.insert(it.key(), rating); } ratingsDocument.close(); Q_EMIT ratingsReady(); } } void OdrsReviewsBackend::parseReviews(const QJsonDocument &document, AbstractResource *resource) { m_isFetching = false; QJsonArray reviews = document.array(); if (!reviews.isEmpty()) { QVector reviewList; for (auto it = reviews.begin(); it != reviews.end(); it++) { QJsonObject review = it->toObject(); if (!review.isEmpty()) { const int usefulFavorable = review.value(QStringLiteral("karma_up")).toInt(); const int usefulTotal = review.value(QStringLiteral("karma_down")).toInt() + usefulFavorable; QDateTime dateTime; dateTime.setTime_t(review.value(QStringLiteral("date_created")).toInt()); ReviewPtr r(new Review(review.value(QStringLiteral("app_id")).toString(), resource->packageName(), review.value(QStringLiteral("locale")).toString(), review.value(QStringLiteral("summary")).toString(), review.value(QStringLiteral("description")).toString(), review.value(QStringLiteral("user_display")).toString(), dateTime, true, review.value(QStringLiteral("review_id")).toInt(), review.value(QStringLiteral("rating")).toInt() / 10, usefulTotal, usefulFavorable, review.value(QStringLiteral("version")).toString())); // We can also receive just a json with app name and user info so filter these out as there is no review if (!r->summary().isEmpty() && !r->reviewText().isEmpty()) { reviewList << r; // Needed for submitting usefulness r->addMetadata(QStringLiteral("ODRS::user_skey"), review.value(QStringLiteral("user_skey")).toString()); } // We should get at least user_skey needed for posting reviews resource->addMetadata(QStringLiteral("ODRS::user_skey"), review.value(QStringLiteral("user_skey")).toString()); } } Q_EMIT reviewsReady(resource, reviewList, false); } } bool OdrsReviewsBackend::isResourceSupported(AbstractResource* res) const { return !res->appstreamId().isEmpty(); } diff --git a/libdiscover/backends/DummyBackend/tests/DummyTest.cpp b/libdiscover/backends/DummyBackend/tests/DummyTest.cpp index 2186045d..d068f870 100644 --- a/libdiscover/backends/DummyBackend/tests/DummyTest.cpp +++ b/libdiscover/backends/DummyBackend/tests/DummyTest.cpp @@ -1,298 +1,298 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * * * 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 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of 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 . * ***************************************************************************/ #include "DummyTest.h" #include "DiscoverBackendsFactory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include QTEST_MAIN(DummyTest) AbstractResourcesBackend* backendByName(ResourcesModel* m, const QString& name) { QVector backends = m->backends(); foreach(AbstractResourcesBackend* backend, backends) { if(QString::fromLatin1(backend->metaObject()->className()) == name) { return backend; } } return nullptr; } DummyTest::DummyTest(QObject* parent): QObject(parent) { DiscoverBackendsFactory::setRequestedBackends({ QStringLiteral("dummy-backend") }); m_model = new ResourcesModel(QStringLiteral("dummy-backend"), this); m_appBackend = backendByName(m_model, QStringLiteral("DummyBackend")); CategoryModel::global()->populateCategories(); } void DummyTest::initTestCase() { QVERIFY(m_appBackend); while(m_appBackend->isFetching()) { QSignalSpy spy(m_appBackend, &AbstractResourcesBackend::fetchingChanged); QVERIFY(spy.wait()); } } QVector fetchResources(ResultsStream* stream) { QVector ret; QObject::connect(stream, &ResultsStream::resourcesFound, stream, [&ret](const QVector& res) { ret += res; }); QSignalSpy spy(stream, &ResultsStream::destroyed); Q_ASSERT(spy.wait()); return ret; } void DummyTest::testReadData() { const auto resources = fetchResources(m_appBackend->search({})); - QCOMPARE(m_appBackend->property("startElements").toInt()*2, resources.size()); + QCOMPARE(m_appBackend->property("startElements").toInt(), resources.size()); QBENCHMARK { for(AbstractResource* res: resources) { QVERIFY(!res->name().isEmpty()); } } } void DummyTest::testProxy() { ResourcesProxyModel pm; QSignalSpy spy(&pm, &ResourcesProxyModel::busyChanged); // QVERIFY(spy.wait()); QVERIFY(!pm.isBusy()); pm.setFiltersFromCategory(CategoryModel::global()->rootCategories().first()); pm.componentComplete(); QVERIFY(pm.isBusy()); QVERIFY(spy.wait()); QVERIFY(!pm.isBusy()); - QCOMPARE(m_appBackend->property("startElements").toInt()*2, pm.rowCount()); + QCOMPARE(m_appBackend->property("startElements").toInt(), pm.rowCount()); pm.setSearch(QStringLiteral("techie")); QVERIFY(pm.isBusy()); QVERIFY(spy.wait()); QVERIFY(!pm.isBusy()); - QCOMPARE(m_appBackend->property("startElements").toInt(), pm.rowCount()); + QCOMPARE(0, pm.rowCount()); QCOMPARE(pm.subcategories().count(), 7); pm.setSearch(QString()); QVERIFY(pm.isBusy()); QVERIFY(spy.wait()); QVERIFY(!pm.isBusy()); - QCOMPARE(m_appBackend->property("startElements").toInt()*2, pm.rowCount()); + QCOMPARE(m_appBackend->property("startElements").toInt(), pm.rowCount()); } void DummyTest::testProxySorting() { ResourcesProxyModel pm; QSignalSpy spy(&pm, &ResourcesProxyModel::busyChanged); // QVERIFY(spy.wait()); QVERIFY(!pm.isBusy()); pm.setFiltersFromCategory(CategoryModel::global()->rootCategories().first()); pm.setSortOrder(Qt::DescendingOrder); pm.setSortRole(ResourcesProxyModel::RatingCountRole); pm.componentComplete(); QVERIFY(pm.isBusy()); QVERIFY(spy.wait()); QVERIFY(!pm.isBusy()); - QCOMPARE(m_appBackend->property("startElements").toInt()*2, pm.rowCount()); + QCOMPARE(m_appBackend->property("startElements").toInt(), pm.rowCount()); QVariant lastRatingCount; for(int i=0, rc=pm.rowCount(); isearch({})); - QCOMPARE(m_appBackend->property("startElements").toInt()*2, resources.count()); + QCOMPARE(m_appBackend->property("startElements").toInt(), resources.count()); //fetches updates, adds new things m_appBackend->checkForUpdates(); QSignalSpy spy(m_model, SIGNAL(allInitialized())); QVERIFY(spy.wait(80000)); auto resources2 = fetchResources(m_appBackend->search({})); - QCOMPARE(m_appBackend->property("startElements").toInt()*4, resources2.count()); + QCOMPARE(m_appBackend->property("startElements").toInt()*2, resources2.count()); } void DummyTest::testSort() { ResourcesProxyModel pm; QCollator c; QBENCHMARK_ONCE { pm.setSortRole(ResourcesProxyModel::NameRole); pm.sort(0); QCOMPARE(pm.sortOrder(), Qt::AscendingOrder); QString last; for(int i = 0, count = pm.rowCount(); isearch(filter)); QCOMPARE(resources.count(), 1); AbstractResource* res = resources.first(); QVERIFY(res); ApplicationAddonsModel m; new ModelTest(&m, &m); m.setApplication(res); QCOMPARE(m.rowCount(), res->addonsInformation().count()); QCOMPARE(res->addonsInformation().at(0).isInstalled(), false); QString firstAddonName = m.data(m.index(0,0)).toString(); m.changeState(firstAddonName, true); QVERIFY(m.hasChanges()); m.applyChanges(); QSignalSpy sR(TransactionModel::global(), &TransactionModel::transactionRemoved); QVERIFY(sR.wait()); QVERIFY(!m.hasChanges()); QCOMPARE(m.data(m.index(0,0)).toString(), firstAddonName); QCOMPARE(res->addonsInformation().at(0).name(), firstAddonName); QCOMPARE(res->addonsInformation().at(0).isInstalled(), true); m.changeState(m.data(m.index(1,0)).toString(), true); QVERIFY(m.hasChanges()); for(int i=0, c=m.rowCount(); isearch(filter)); QCOMPARE(resources.count(), 1); AbstractResource* res = resources.first(); QVERIFY(res); ReviewsModel m; new ModelTest(&m, &m); m.setResource(res); m.fetchMore(); QVERIFY(m.rowCount()>0); QCOMPARE(ReviewsModel::UserChoice(m.data(m.index(0,0), ReviewsModel::UsefulChoice).toInt()), ReviewsModel::None); m.markUseful(0, true); QCOMPARE(ReviewsModel::UserChoice(m.data(m.index(0,0), ReviewsModel::UsefulChoice).toInt()), ReviewsModel::Yes); m.markUseful(0, false); QCOMPARE(ReviewsModel::UserChoice(m.data(m.index(0,0), ReviewsModel::UsefulChoice).toInt()), ReviewsModel::No); const auto resources2 = fetchResources(m_appBackend->search(filter)); QCOMPARE(resources2.count(), 1); res = resources2.first(); m.setResource(res); m.fetchMore(); QSignalSpy spy(&m, &ReviewsModel::rowsChanged); QVERIFY(m.rowCount()>0); } void DummyTest::testUpdateModel() { const auto backend = m_model->backends().first(); ResourcesUpdatesModel ruModel; new ModelTest(&ruModel, &ruModel); UpdateModel model; new ModelTest(&model, &model); model.setBackend(&ruModel); QCOMPARE(model.rowCount(), 4*backend->property("startElements").toInt()/3); QCOMPARE(model.hasUpdates(), true); } void DummyTest::testScreenshotsModel() { AbstractResourcesBackend::Filters filter; filter.resourceUrl = QUrl(QStringLiteral("dummy://Dummy.1")); ScreenshotsModel m; new ModelTest(&m, &m); const auto resources = fetchResources(m_appBackend->search(filter)); QCOMPARE(resources.count(), 1); AbstractResource* res = resources.first(); QVERIFY(res); m.setResource(res); QCOMPARE(res, m.resource()); int c=m.rowCount(); for(int i=0; i