MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - data - CredentialStore.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 72.4 % 127 92
Test Date: 2026-06-21 21:10:19 Functions: 94.1 % 17 16
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 31.8 % 296 94

             Branch data     Line data    Source code
       1                 :             : #include "CredentialStore.h"
       2                 :             : 
       3                 :             : #include <QEventLoop>
       4                 :             : #include <QHash>
       5                 :             : #include <QLoggingCategory>
       6                 :             : #include <QTimer>
       7                 :             : #include <QUuid>
       8                 :             : #include <qt6keychain/keychain.h>
       9                 :             : 
      10   [ +  +  +  -  :          25 : Q_LOGGING_CATEGORY(lcCredentialStore, "mailjd.credentials")
             +  -  -  - ]
      11                 :             : 
      12                 :             : // Test seam: MAILJD_FAKE_KEYRING=1 replaces QtKeychain with an in-process
      13                 :             : // store. Required for headless CI — QtKeychain blocks (and times out) when
      14                 :             : // no keyring daemon is available, which makes every credential code path
      15                 :             : // untestable otherwise. Never enabled in production.
      16                 :             : namespace {
      17                 :         603 : QHash<QString, QByteArray> &fakeKeyring() {
      18   [ +  +  +  - ]:         603 :   static QHash<QString, QByteArray> store;
      19                 :         603 :   return store;
      20                 :             : }
      21                 :         389 : bool useFakeKeyring() {
      22                 :         389 :   return qEnvironmentVariableIsSet("MAILJD_FAKE_KEYRING");
      23                 :             : }
      24                 :             : } // namespace
      25                 :             : 
      26                 :         339 : CredentialStore::CredentialStore(QObject *parent) : QObject(parent) {}
      27                 :             : 
      28                 :         393 : QString CredentialStore::keyringKey(const QString &service,
      29                 :             :                                     const QString &accountName) {
      30         [ +  - ]:         393 :   return QStringLiteral("mailjd/%1/%2").arg(service, accountName);
      31                 :             : }
      32                 :             : 
      33                 :           3 : bool CredentialStore::isAvailable() {
      34         [ +  + ]:           3 :   if (useFakeKeyring())
      35                 :           2 :     return true;
      36         [ +  - ]:           1 :   QKeychain::ReadPasswordJob job(QStringLiteral("mailjd"));
      37         [ +  - ]:           1 :   job.setAutoDelete(false);
      38   [ +  -  +  - ]:           1 :   job.setKey(keyringKey(
      39                 :           2 :       QStringLiteral("availability-probe"),
      40   [ +  -  +  - ]:           2 :       QUuid::createUuid().toString(QUuid::WithoutBraces)));
      41                 :             : 
      42         [ +  - ]:           1 :   QEventLoop loop;
      43         [ +  - ]:           1 :   QTimer timeout;
      44         [ +  - ]:           1 :   timeout.setSingleShot(true);
      45                 :             : 
      46                 :           1 :   bool finished = false;
      47                 :           1 :   bool timedOut = false;
      48                 :           1 :   QObject::connect(&job, &QKeychain::ReadPasswordJob::finished, &loop,
      49         [ +  - ]:           1 :                    [&](QKeychain::Job *) {
      50         [ -  + ]:           1 :                      if (timedOut)
      51                 :           0 :                        return;
      52                 :           1 :                      finished = true;
      53                 :           1 :                      loop.quit();
      54                 :             :                    });
      55         [ +  - ]:           1 :   QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
      56         [ #  # ]:           0 :     if (finished)
      57                 :           0 :       return;
      58                 :           0 :     timedOut = true;
      59                 :           0 :     loop.quit();
      60                 :             :   });
      61                 :             : 
      62         [ +  - ]:           1 :   timeout.start(5000);
      63         [ +  - ]:           1 :   job.start();
      64         [ +  - ]:           1 :   loop.exec();
      65                 :             : 
      66         [ -  + ]:           1 :   if (timedOut) {
      67   [ #  #  #  #  :           0 :     qCWarning(lcCredentialStore)
                   #  # ]
      68         [ #  # ]:           0 :         << "Keyring availability probe did not complete";
      69                 :           0 :     return false;
      70                 :             :   }
      71                 :             : 
      72         [ +  - ]:           1 :   const auto error = job.error();
      73   [ +  -  -  + ]:           1 :   const bool available = error == QKeychain::NoError ||
      74                 :             :                          error == QKeychain::EntryNotFound;
      75         [ +  - ]:           1 :   if (!available) {
      76   [ +  -  +  -  :           2 :     qCWarning(lcCredentialStore)
                   +  + ]
      77   [ +  -  +  -  :           1 :         << "Keyring availability probe failed:" << job.errorString();
                   +  - ]
      78                 :             :   }
      79                 :           1 :   return available;
      80                 :           1 : }
      81                 :             : 
      82                 :         250 : void CredentialStore::readPassword(const QString &service,
      83                 :             :                                    const QString &accountName,
      84                 :             :                                    ReadCallback callback) {
      85         [ +  - ]:         250 :   if (useFakeKeyring()) {
      86         [ +  - ]:         250 :     const QString key = keyringKey(service, accountName);
      87                 :             :     // Deliver asynchronously to preserve the QtKeychain calling contract.
      88   [ +  -  +  -  :         250 :     QTimer::singleShot(0, this, [callback, key]() {
                   -  - ]
      89                 :         250 :       const bool found = fakeKeyring().contains(key);
      90         [ +  - ]:         250 :       callback(found, fakeKeyring().value(key));
      91                 :         250 :     });
      92                 :         250 :     return;
      93                 :         250 :   }
      94                 :             :   auto *job = new QKeychain::ReadPasswordJob(
      95   [ #  #  #  #  :           0 :       QStringLiteral("mailjd"), this);
                   #  # ]
      96                 :           0 :   job->setAutoDelete(true);
      97   [ #  #  #  # ]:           0 :   job->setKey(keyringKey(service, accountName));
      98                 :             : 
      99                 :           0 :   connect(job, &QKeychain::ReadPasswordJob::finished, this,
     100   [ #  #  #  #  :           0 :           [callback, service, accountName](QKeychain::Job *j) {
             #  #  #  # ]
     101                 :           0 :             auto *readJob = qobject_cast<QKeychain::ReadPasswordJob *>(j);
     102   [ #  #  #  #  :           0 :             if (readJob && readJob->error() == QKeychain::NoError) {
                   #  # ]
     103                 :             :               // QKeychain stores as QString — convert to QByteArray
     104   [ #  #  #  # ]:           0 :               QByteArray pw = readJob->textData().toUtf8();
     105   [ #  #  #  #  :           0 :               qCDebug(lcCredentialStore)
                   #  # ]
     106   [ #  #  #  #  :           0 :                   << "Read password for" << service << accountName;
                   #  # ]
     107         [ #  # ]:           0 :               callback(true, pw);
     108                 :           0 :             } else {
     109                 :           0 :               QString errMsg = readJob ? readJob->errorString()
     110   [ #  #  #  #  :           0 :                                        : QStringLiteral("Unknown error");
             #  #  #  # ]
     111   [ #  #  #  #  :           0 :               qCWarning(lcCredentialStore)
                   #  # ]
     112   [ #  #  #  #  :           0 :                   << "Failed to read password for" << service << accountName
                   #  # ]
     113   [ #  #  #  # ]:           0 :                   << ":" << errMsg;
     114         [ #  # ]:           0 :               callback(false, QByteArray());
     115                 :           0 :             }
     116                 :           0 :           });
     117                 :             : 
     118                 :           0 :   job->start();
     119                 :             : }
     120                 :             : 
     121                 :         111 : void CredentialStore::writePassword(const QString &service,
     122                 :             :                                     const QString &accountName,
     123                 :             :                                     const QByteArray &password,
     124                 :             :                                     WriteCallback callback) {
     125         [ +  + ]:         111 :   if (useFakeKeyring()) {
     126         [ +  - ]:          89 :     const QString key = keyringKey(service, accountName);
     127   [ +  -  +  -  :          89 :     QTimer::singleShot(0, this, [callback, key, password]() {
             -  -  -  - ]
     128         [ +  + ]:          89 :       if (qEnvironmentVariableIsSet("MAILJD_FAKE_KEYRING_FAIL_WRITES")) {
     129         [ +  - ]:          18 :         callback(false, QStringLiteral("Injected fake keyring write failure"));
     130                 :           9 :         return;
     131                 :             :       }
     132                 :          80 :       fakeKeyring().insert(key, password);
     133         [ +  - ]:          80 :       callback(true, QString());
     134                 :             :     });
     135                 :          89 :     return;
     136                 :          89 :   }
     137                 :             :   auto *job = new QKeychain::WritePasswordJob(
     138   [ +  -  -  +  :          44 :       QStringLiteral("mailjd"), this);
                   -  - ]
     139                 :          22 :   job->setAutoDelete(true);
     140   [ +  -  +  - ]:          22 :   job->setKey(keyringKey(service, accountName));
     141   [ +  -  +  - ]:          22 :   job->setTextData(QString::fromUtf8(password));
     142                 :             : 
     143                 :          22 :   connect(job, &QKeychain::WritePasswordJob::finished, this,
     144   [ +  -  +  -  :          44 :           [callback, service, accountName](QKeychain::Job *j) {
             -  -  -  - ]
     145         [ -  + ]:          22 :             if (j->error() == QKeychain::NoError) {
     146   [ #  #  #  #  :           0 :               qCInfo(lcCredentialStore)
                   #  # ]
     147   [ #  #  #  #  :           0 :                   << "Stored password for" << service << accountName;
                   #  # ]
     148         [ #  # ]:           0 :               callback(true, QString());
     149                 :             :             } else {
     150   [ +  -  +  -  :          44 :               qCWarning(lcCredentialStore)
                   +  + ]
     151   [ +  -  +  -  :          22 :                   << "Failed to write password for" << service << accountName
                   +  - ]
     152   [ +  -  +  -  :          22 :                   << ":" << j->errorString();
                   +  - ]
     153   [ +  -  +  - ]:          22 :               callback(false, j->errorString());
     154                 :             :             }
     155                 :          22 :           });
     156                 :             : 
     157                 :          22 :   job->start();
     158                 :             : }
     159                 :             : 
     160                 :          25 : void CredentialStore::deletePassword(const QString &service,
     161                 :             :                                      const QString &accountName,
     162                 :             :                                      DeleteCallback callback) {
     163         [ +  + ]:          25 :   if (useFakeKeyring()) {
     164         [ +  - ]:          23 :     const QString key = keyringKey(service, accountName);
     165   [ +  -  +  -  :          23 :     QTimer::singleShot(0, this, [callback, key]() {
                   -  - ]
     166                 :          23 :       callback(fakeKeyring().remove(key) > 0);
     167                 :          23 :     });
     168                 :          23 :     return;
     169                 :          23 :   }
     170                 :             :   auto *job = new QKeychain::DeletePasswordJob(
     171   [ +  -  -  +  :           4 :       QStringLiteral("mailjd"), this);
                   -  - ]
     172                 :           2 :   job->setAutoDelete(true);
     173   [ +  -  +  - ]:           2 :   job->setKey(keyringKey(service, accountName));
     174                 :             : 
     175                 :           2 :   connect(job, &QKeychain::DeletePasswordJob::finished, this,
     176   [ +  -  +  -  :           4 :           [callback, service, accountName](QKeychain::Job *j) {
             -  -  -  - ]
     177         [ -  + ]:           2 :             if (j->error() == QKeychain::NoError) {
     178   [ #  #  #  #  :           0 :               qCInfo(lcCredentialStore)
                   #  # ]
     179   [ #  #  #  #  :           0 :                   << "Deleted password for" << service << accountName;
                   #  # ]
     180                 :           0 :               callback(true);
     181                 :             :             } else {
     182   [ +  -  +  -  :           4 :               qCWarning(lcCredentialStore)
                   +  + ]
     183   [ +  -  +  -  :           2 :                   << "Failed to delete password for" << service << accountName
                   +  - ]
     184   [ +  -  +  -  :           2 :                   << ":" << j->errorString();
                   +  - ]
     185                 :           2 :               callback(false);
     186                 :             :             }
     187                 :           2 :           });
     188                 :             : 
     189                 :           2 :   job->start();
     190                 :             : }
        

Generated by: LCOV version 2.0-1