v0.2.0: data-safety, unsaved-changes guard, FormCard UI, filesystem UX
- Preserve override keys we do not model so Save never drops data - Prompt before discarding unsaved changes when switching applications - Native FormCard layout for the permission list (grouped cards) - Filesystem entries gain an access-mode selector and a folder browser - Add resolution autotests (toggle render/save, filesystem split, key preservation)
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
add_executable(resolutiontest
|
||||
resolutiontest.cpp
|
||||
../src/keyfile.cpp
|
||||
../src/flatpakinstallations.cpp
|
||||
../src/permissioncatalog.cpp
|
||||
../src/portalsbackend.cpp
|
||||
../src/permissionscontroller.cpp
|
||||
)
|
||||
|
||||
target_include_directories(resolutiontest PRIVATE ../src)
|
||||
|
||||
target_link_libraries(resolutiontest PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Qml
|
||||
Qt6::DBus
|
||||
Qt6::Test
|
||||
KF6::I18n
|
||||
)
|
||||
|
||||
add_test(NAME resolutiontest COMMAND resolutiontest)
|
||||
@@ -0,0 +1,145 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "keyfile.h"
|
||||
#include "permissionscontroller.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
|
||||
using PC = PermissionsController;
|
||||
|
||||
class ResolutionTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
QString userBase() const
|
||||
{
|
||||
return m_dir.path() + QStringLiteral("/user");
|
||||
}
|
||||
|
||||
void makeApp(const QString &appId, const QString &metadata)
|
||||
{
|
||||
const QString d = userBase() + QStringLiteral("/app/") + appId + QStringLiteral("/current/active");
|
||||
QVERIFY(QDir().mkpath(d));
|
||||
QFile f(d + QStringLiteral("/metadata"));
|
||||
QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||
f.write(metadata.toUtf8());
|
||||
}
|
||||
|
||||
void writeOverride(const QString &appId, const QString &content)
|
||||
{
|
||||
const QString d = userBase() + QStringLiteral("/overrides");
|
||||
QVERIFY(QDir().mkpath(d));
|
||||
QFile f(d + QLatin1Char('/') + appId);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||
f.write(content.toUtf8());
|
||||
}
|
||||
|
||||
KeyFile readOverride(const QString &appId)
|
||||
{
|
||||
KeyFile kf;
|
||||
kf.load(userBase() + QStringLiteral("/overrides/") + appId);
|
||||
return kf;
|
||||
}
|
||||
|
||||
static int findToggle(PC &c, const QString &catId, const QString &option)
|
||||
{
|
||||
for (int i = 0; i < c.rowCount(); ++i) {
|
||||
const QModelIndex idx = c.index(i);
|
||||
if (c.data(idx, PC::RowTypeRole).toInt() == PC::Toggle && c.data(idx, PC::CategoryIdRole).toString() == catId
|
||||
&& c.data(idx, PC::OptionKeyRole).toString() == option) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
qputenv("FLATPAK_USER_DIR", userBase().toUtf8());
|
||||
qputenv("FLATPAK_SYSTEM_DIR", (m_dir.path() + QStringLiteral("/system")).toUtf8());
|
||||
// Point config at an empty dir so host custom installations don't interfere.
|
||||
qputenv("FLATPAK_CONFIG_DIR", (m_dir.path() + QStringLiteral("/config")).toUtf8());
|
||||
}
|
||||
|
||||
void toggleRenderAndSave()
|
||||
{
|
||||
makeApp(QStringLiteral("org.test.Toggle"), QStringLiteral("[Context]\nshared=network;ipc\n"));
|
||||
PC c;
|
||||
c.setAppId(QStringLiteral("org.test.Toggle"));
|
||||
|
||||
const int net = findToggle(c, QStringLiteral("share"), QStringLiteral("network"));
|
||||
QVERIFY(net >= 0);
|
||||
QCOMPARE(c.data(c.index(net), PC::ValueRole).toBool(), true);
|
||||
QCOMPARE(c.data(c.index(net), PC::StatusRole).toInt(), int(PC::Original));
|
||||
QVERIFY(!c.modified());
|
||||
|
||||
c.setToggleValue(net, false);
|
||||
QVERIFY(c.modified());
|
||||
QCOMPARE(c.data(c.index(net), PC::StatusRole).toInt(), int(PC::User));
|
||||
|
||||
c.save();
|
||||
QVERIFY(!c.modified());
|
||||
|
||||
const QStringList shared = readOverride(QStringLiteral("org.test.Toggle")).value(QStringLiteral("Context"), QStringLiteral("shared")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
|
||||
QVERIFY(shared.contains(QStringLiteral("!network")));
|
||||
QVERIFY(!shared.contains(QStringLiteral("!ipc"))); // unchanged options are not written
|
||||
}
|
||||
|
||||
void filesystemsSplitByMode()
|
||||
{
|
||||
makeApp(QStringLiteral("org.test.Fs"), QStringLiteral("[Context]\nfilesystems=home;~/Docs:ro;xdg-download\n"));
|
||||
PC c;
|
||||
c.setAppId(QStringLiteral("org.test.Fs"));
|
||||
|
||||
// "home" is a preset toggle, on.
|
||||
const int home = findToggle(c, QStringLiteral("filesystems-presets"), QStringLiteral("home"));
|
||||
QVERIFY(home >= 0);
|
||||
QCOMPARE(c.data(c.index(home), PC::ValueRole).toBool(), true);
|
||||
|
||||
// The non-preset paths become entries split into path + access mode.
|
||||
QMap<QString, QString> paths;
|
||||
for (int i = 0; i < c.rowCount(); ++i) {
|
||||
const QModelIndex idx = c.index(i);
|
||||
if (c.data(idx, PC::RowTypeRole).toInt() == PC::PathEntry && c.data(idx, PC::CategoryIdRole).toString() == QStringLiteral("filesystems-other")) {
|
||||
paths.insert(c.data(idx, PC::ValueRole).toString(), c.data(idx, PC::SecondaryRole).toString());
|
||||
}
|
||||
}
|
||||
QCOMPARE(paths.value(QStringLiteral("~/Docs")), QStringLiteral("ro"));
|
||||
QCOMPARE(paths.value(QStringLiteral("xdg-download")), QStringLiteral("rw"));
|
||||
}
|
||||
|
||||
void unknownKeysPreserved()
|
||||
{
|
||||
makeApp(QStringLiteral("org.test.Unknown"), QStringLiteral("[Context]\nshared=network\n"));
|
||||
writeOverride(QStringLiteral("org.test.Unknown"),
|
||||
QStringLiteral("[Context]\nshared=!network\nunknownkey=foo\n\n[Weird Group]\na=b\n"));
|
||||
|
||||
PC c;
|
||||
c.setAppId(QStringLiteral("org.test.Unknown"));
|
||||
// Loading an existing file must be idempotent (no spurious "modified").
|
||||
QVERIFY(!c.modified());
|
||||
|
||||
const int net = findToggle(c, QStringLiteral("share"), QStringLiteral("network"));
|
||||
QVERIFY(net >= 0);
|
||||
QCOMPARE(c.data(c.index(net), PC::ValueRole).toBool(), false); // overridden off
|
||||
|
||||
c.setToggleValue(net, true); // back to the metadata default -> override removed
|
||||
QVERIFY(c.modified());
|
||||
c.save();
|
||||
|
||||
const KeyFile kf = readOverride(QStringLiteral("org.test.Unknown"));
|
||||
// Keys we do not model must survive the round-trip.
|
||||
QCOMPARE(kf.value(QStringLiteral("Context"), QStringLiteral("unknownkey")), QStringLiteral("foo"));
|
||||
QCOMPARE(kf.value(QStringLiteral("Weird Group"), QStringLiteral("a")), QStringLiteral("b"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(ResolutionTest)
|
||||
#include "resolutiontest.moc"
|
||||
Reference in New Issue
Block a user