mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
242 lines
9.8 KiB
C++
242 lines
9.8 KiB
C++
|
|
/***************************************************************************
|
||
|
|
* Copyright (C) 2025-2026 by Mike Conley - mike.conley@stickmud.com *
|
||
|
|
* *
|
||
|
|
* 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) any later version. *
|
||
|
|
* *
|
||
|
|
* 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, write to the *
|
||
|
|
* Free Software Foundation, Inc., *
|
||
|
|
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||
|
|
***************************************************************************/
|
||
|
|
|
||
|
|
// Tests for the portable password file → secure storage migration path.
|
||
|
|
//
|
||
|
|
// PR #7956 introduced a regression where the connection-profiles dialog
|
||
|
|
// stopped consulting the portable password file on disk when neither the
|
||
|
|
// system keychain nor QSettings contained a password for the selected
|
||
|
|
// profile. These tests verify that:
|
||
|
|
//
|
||
|
|
// 1. A password stored in the old portable file format (QDataStream) can
|
||
|
|
// be read back correctly.
|
||
|
|
// 2. The CredentialManager correctly stores and retrieves passwords that
|
||
|
|
// have been migrated from the portable format.
|
||
|
|
// 3. If the CredentialManager has no password, the portable file is still
|
||
|
|
// a valid source of truth (simulating the fallback path).
|
||
|
|
|
||
|
|
#include <CredentialManager.h>
|
||
|
|
#include <QDataStream>
|
||
|
|
#include <QDir>
|
||
|
|
#include <QSaveFile>
|
||
|
|
#include <QTemporaryDir>
|
||
|
|
#include <QtTest/QtTest>
|
||
|
|
|
||
|
|
class PasswordMigrationTest : public QObject {
|
||
|
|
Q_OBJECT
|
||
|
|
|
||
|
|
private:
|
||
|
|
QTemporaryDir mTempDir;
|
||
|
|
|
||
|
|
// Mirrors the portable password file write logic from
|
||
|
|
// dlgConnectionProfiles::writeProfileData / mudlet::writeProfileData
|
||
|
|
bool writePortablePasswordFile(const QString &profileDir, const QString &item,
|
||
|
|
const QString &password) {
|
||
|
|
const QString filePath = QStringLiteral("%1/%2").arg(profileDir, item);
|
||
|
|
QSaveFile file(filePath);
|
||
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
QDataStream ofs(&file);
|
||
|
|
// Match the version used by Mudlet for Qt >= 5.13
|
||
|
|
ofs.setVersion(QDataStream::Qt_5_12);
|
||
|
|
ofs << password;
|
||
|
|
return file.commit();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mirrors the portable password file read logic from
|
||
|
|
// dlgConnectionProfiles::readProfileData / mudlet::readProfileData
|
||
|
|
QString readPortablePasswordFile(const QString &profileDir,
|
||
|
|
const QString &item) {
|
||
|
|
const QString filePath = QStringLiteral("%1/%2").arg(profileDir, item);
|
||
|
|
QFile file(filePath);
|
||
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
||
|
|
return QString();
|
||
|
|
}
|
||
|
|
QDataStream ifs(&file);
|
||
|
|
ifs.setVersion(QDataStream::Qt_5_12);
|
||
|
|
QString ret;
|
||
|
|
ifs >> ret;
|
||
|
|
file.close();
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
private slots:
|
||
|
|
void initTestCase() {
|
||
|
|
qputenv("MUDLET_TEST_MODE", "1");
|
||
|
|
QVERIFY(mTempDir.isValid());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify the portable file round-trip: write → read is lossless
|
||
|
|
void testPortableFileRoundTrip() {
|
||
|
|
const QString profile = QStringLiteral("RoundTripProfile");
|
||
|
|
const QString profileDir =
|
||
|
|
QStringLiteral("%1/%2").arg(mTempDir.path(), profile);
|
||
|
|
QVERIFY(QDir().mkpath(profileDir));
|
||
|
|
|
||
|
|
const QString password = QStringLiteral("s3cret!P@ss");
|
||
|
|
QVERIFY(writePortablePasswordFile(profileDir, QStringLiteral("password"),
|
||
|
|
password));
|
||
|
|
|
||
|
|
const QString retrieved =
|
||
|
|
readPortablePasswordFile(profileDir, QStringLiteral("password"));
|
||
|
|
QCOMPARE(retrieved, password);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify empty file → empty string (no crash, no garbage)
|
||
|
|
void testPortableFileEmptyPassword() {
|
||
|
|
const QString profile = QStringLiteral("EmptyPassProfile");
|
||
|
|
const QString profileDir =
|
||
|
|
QStringLiteral("%1/%2").arg(mTempDir.path(), profile);
|
||
|
|
QVERIFY(QDir().mkpath(profileDir));
|
||
|
|
|
||
|
|
// Write an empty password
|
||
|
|
QVERIFY(writePortablePasswordFile(profileDir, QStringLiteral("password"),
|
||
|
|
QString()));
|
||
|
|
|
||
|
|
const QString retrieved =
|
||
|
|
readPortablePasswordFile(profileDir, QStringLiteral("password"));
|
||
|
|
QVERIFY(retrieved.isEmpty());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify no file → empty string (graceful fallback)
|
||
|
|
void testPortableFileMissing() {
|
||
|
|
const QString profile = QStringLiteral("MissingPassProfile");
|
||
|
|
const QString profileDir =
|
||
|
|
QStringLiteral("%1/%2").arg(mTempDir.path(), profile);
|
||
|
|
QVERIFY(QDir().mkpath(profileDir));
|
||
|
|
|
||
|
|
// Don't create a password file
|
||
|
|
const QString retrieved =
|
||
|
|
readPortablePasswordFile(profileDir, QStringLiteral("password"));
|
||
|
|
QVERIFY(retrieved.isEmpty());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Simulate the migration path: portable file exists, CredentialManager
|
||
|
|
// is empty, migration reads portable file and stores into CredentialManager.
|
||
|
|
void testMigrationPattern() {
|
||
|
|
const QString profile = QStringLiteral("MigrationTestProfile");
|
||
|
|
const QString profileDir =
|
||
|
|
QStringLiteral("%1/%2").arg(mTempDir.path(), profile);
|
||
|
|
QVERIFY(QDir().mkpath(profileDir));
|
||
|
|
|
||
|
|
const QString password = QStringLiteral("migr@t1on_P@ss");
|
||
|
|
|
||
|
|
// Step 1: CredentialManager has nothing for this profile
|
||
|
|
const QString beforeMigration = CredentialManager::retrieveCredential(
|
||
|
|
profile, QStringLiteral("character"));
|
||
|
|
QVERIFY(beforeMigration.isEmpty());
|
||
|
|
|
||
|
|
// Step 2: Write password to the portable file (simulating legacy state)
|
||
|
|
QVERIFY(writePortablePasswordFile(profileDir, QStringLiteral("password"),
|
||
|
|
password));
|
||
|
|
|
||
|
|
// Step 3: Read from portable file (simulating what migration does)
|
||
|
|
const QString portablePassword =
|
||
|
|
readPortablePasswordFile(profileDir, QStringLiteral("password"));
|
||
|
|
QCOMPARE(portablePassword, password);
|
||
|
|
|
||
|
|
// Step 4: Store into CredentialManager (simulating
|
||
|
|
// migratePasswordsToSecureStorage)
|
||
|
|
QVERIFY(CredentialManager::storeCredential(
|
||
|
|
profile, QStringLiteral("character"), portablePassword));
|
||
|
|
|
||
|
|
// Step 5: Now CredentialManager has it
|
||
|
|
const QString afterMigration = CredentialManager::retrieveCredential(
|
||
|
|
profile, QStringLiteral("character"));
|
||
|
|
QCOMPARE(afterMigration, password);
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
CredentialManager::removeCredential(profile, QStringLiteral("character"));
|
||
|
|
}
|
||
|
|
|
||
|
|
// The core regression scenario: CredentialManager is empty, QSettings is
|
||
|
|
// empty, but the portable file exists. The fallback should find it.
|
||
|
|
void testFallbackToPortableFile() {
|
||
|
|
const QString profile = QStringLiteral("FallbackTestProfile");
|
||
|
|
const QString profileDir =
|
||
|
|
QStringLiteral("%1/%2").arg(mTempDir.path(), profile);
|
||
|
|
QVERIFY(QDir().mkpath(profileDir));
|
||
|
|
|
||
|
|
const QString password = QStringLiteral("f@llb@ck_P@ss!");
|
||
|
|
|
||
|
|
// Credential manager has nothing
|
||
|
|
QVERIFY(CredentialManager::retrieveCredential(profile,
|
||
|
|
QStringLiteral("character"))
|
||
|
|
.isEmpty());
|
||
|
|
|
||
|
|
// QSettings has nothing (we don't set anything)
|
||
|
|
|
||
|
|
// Portable file has the password
|
||
|
|
QVERIFY(writePortablePasswordFile(profileDir, QStringLiteral("password"),
|
||
|
|
password));
|
||
|
|
|
||
|
|
// The fallback logic should: check CredentialManager → empty,
|
||
|
|
// check QSettings → empty, check portable file → found.
|
||
|
|
// Here we verify each step of the chain independently.
|
||
|
|
|
||
|
|
// CredentialManager: empty
|
||
|
|
const QString fromCredMgr = CredentialManager::retrieveCredential(
|
||
|
|
profile, QStringLiteral("character"));
|
||
|
|
QVERIFY(fromCredMgr.isEmpty());
|
||
|
|
|
||
|
|
// Portable file: has password (this is what the fix restores)
|
||
|
|
const QString fromFile =
|
||
|
|
readPortablePasswordFile(profileDir, QStringLiteral("password"));
|
||
|
|
QCOMPARE(fromFile, password);
|
||
|
|
|
||
|
|
// After finding it, the migration should store it for next time
|
||
|
|
QVERIFY(CredentialManager::storeCredential(
|
||
|
|
profile, QStringLiteral("character"), fromFile));
|
||
|
|
QCOMPARE(CredentialManager::retrieveCredential(profile,
|
||
|
|
QStringLiteral("character")),
|
||
|
|
password);
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
CredentialManager::removeCredential(profile, QStringLiteral("character"));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify that Unicode passwords survive the portable file round-trip
|
||
|
|
void testPortableFileUnicodePassword() {
|
||
|
|
const QString profile = QStringLiteral("UnicodePassProfile");
|
||
|
|
const QString profileDir =
|
||
|
|
QStringLiteral("%1/%2").arg(mTempDir.path(), profile);
|
||
|
|
QVERIFY(QDir().mkpath(profileDir));
|
||
|
|
|
||
|
|
const QString password = QStringLiteral("пароль密码パスワード🔑");
|
||
|
|
QVERIFY(writePortablePasswordFile(profileDir, QStringLiteral("password"),
|
||
|
|
password));
|
||
|
|
|
||
|
|
const QString retrieved =
|
||
|
|
readPortablePasswordFile(profileDir, QStringLiteral("password"));
|
||
|
|
QCOMPARE(retrieved, password);
|
||
|
|
}
|
||
|
|
|
||
|
|
void cleanupTestCase() {
|
||
|
|
// Defensive cleanup
|
||
|
|
CredentialManager::removeCredential(QStringLiteral("MigrationTestProfile"),
|
||
|
|
QStringLiteral("character"));
|
||
|
|
CredentialManager::removeCredential(QStringLiteral("FallbackTestProfile"),
|
||
|
|
QStringLiteral("character"));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
#include "PasswordMigrationTest.moc"
|
||
|
|
QTEST_MAIN(PasswordMigrationTest)
|