MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 90.8% 247 / 0 / 272
Functions: 88.5% 23 / 0 / 26
Branches: 56.9% 319 / 0 / 561

data/DavCredentials.cpp
Line Branch Exec Source
1 #include "DavCredentials.h"
2
3 #include "CredentialStore.h"
4
5 #include <QCryptographicHash>
6 #include <QEventLoop>
7 #include <QSettings>
8 #include <QTimer>
9 #include <QPointer>
10
11 namespace {
12
13 constexpr int kKeyringTimeoutMs = 30000;
14 constexpr auto kDavService = "carddav";
15
16 76 QString digestMaterial(const QString &accountId, const QString &origin,
17 const QString &username) {
18
2/2
✓ Branch 7 → 8 taken 76 times.
✓ Branch 8 → 9 taken 76 times.
152 const QString material = accountId.trimmed() + QLatin1Char('\n') +
19
2/2
✓ Branch 9 → 10 taken 76 times.
✓ Branch 10 → 11 taken 76 times.
228 origin + QLatin1Char('\n') +
20
2/2
✓ Branch 2 → 3 taken 76 times.
✓ Branch 11 → 12 taken 76 times.
228 username.trimmed();
21 return QString::fromLatin1(
22
2/2
✓ Branch 17 → 18 taken 76 times.
✓ Branch 19 → 20 taken 76 times.
152 QCryptographicHash::hash(material.toUtf8(),
23 QCryptographicHash::Sha256)
24
2/2
✓ Branch 20 → 21 taken 76 times.
✓ Branch 21 → 22 taken 76 times.
152 .toHex());
25 76 }
26
27 29 QString legacyAccountName(const QString &accountId, const QString &serverUrl,
28 const QString &username) {
29
1/1
✓ Branch 2 → 3 taken 29 times.
58 const QString id = accountId.trimmed().isEmpty()
30
1/4
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 29 times.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
29 ? QStringLiteral("unknown")
31
2/3
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 29 times.
✓ Branch 8 → 9 taken 29 times.
29 : accountId.trimmed();
32 // Pre-2026-07 endpoint-bound format. Keep this exact raw-URL digest solely
33 // for a safe one-way migration; unlike the later account-ID-only format it
34 // cannot move a secret to another endpoint.
35 58 return QStringLiteral("%1/%2").arg(
36
3/3
✓ Branch 15 → 16 taken 29 times.
✓ Branch 16 → 17 taken 29 times.
✓ Branch 17 → 18 taken 29 times.
58 id, digestMaterial(accountId, serverUrl.trimmed(), username));
37 29 }
38
39 40 QByteArray readPasswordKeyBlocking(const QString &accountName, bool &success,
40 QString *error) {
41 40 success = false;
42
43
1/1
✓ Branch 2 → 3 taken 40 times.
40 CredentialStore store;
44
1/1
✓ Branch 3 → 4 taken 40 times.
40 QEventLoop loop;
45
1/1
✓ Branch 4 → 5 taken 40 times.
40 QTimer timeout;
46
1/1
✓ Branch 5 → 6 taken 40 times.
40 timeout.setSingleShot(true);
47
48 40 bool completed = false;
49 40 QByteArray password;
50 40 QString readError;
51
52
1/1
✓ Branch 6 → 7 taken 40 times.
40 QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
53 ✗ if (completed)
54 ✗ return;
55 ✗ completed = true;
56 ✗ readError = QStringLiteral("Timed out reading DAV password from keyring");
57 ✗ loop.quit();
58 });
59
60
2/2
✓ Branch 10 → 11 taken 40 times.
✓ Branch 11 → 12 taken 40 times.
40 store.readPassword(QString::fromLatin1(kDavService), accountName,
61
1/1
✓ Branch 8 → 9 taken 40 times.
40 [&](bool ok, const QByteArray &pw) {
62
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 40 times.
40 if (completed)
63 ✗ return;
64 40 completed = true;
65 40 success = ok;
66 40 password = pw;
67 40 loop.quit();
68 });
69
70 // S106-201: a synchronous answer (a refusal off the GUI thread) quits
71 // the loop before exec() — the quit is lost and the loop never ends.
72
2/2
✓ Branch 14 → 15 taken 38 times.
✓ Branch 14 → 19 taken 2 times.
40 if (!completed) {
73
1/1
✓ Branch 15 → 16 taken 38 times.
38 timeout.start(kKeyringTimeoutMs);
74
1/1
✓ Branch 17 → 18 taken 38 times.
38 loop.exec();
75 }
76
77
4/4
✓ Branch 19 → 20 taken 32 times.
✓ Branch 19 → 32 taken 8 times.
✓ Branch 20 → 21 taken 6 times.
✓ Branch 20 → 32 taken 26 times.
40 if (!success && error) {
78 6 *error = readError.isEmpty()
79
2/4
✓ Branch 22 → 23 taken 6 times.
✗ Branch 22 → 26 not taken.
✓ Branch 29 → 30 taken 6 times.
✗ Branch 29 → 31 not taken.
18 ? QStringLiteral("Could not read DAV password from keyring")
80 6 : readError;
81 }
82
2/2
✓ Branch 32 → 33 taken 8 times.
✓ Branch 32 → 34 taken 32 times.
80 return success ? password : QByteArray();
83 40 }
84
85 7 bool writePasswordKeyBlocking(const QString &accountName,
86 const QByteArray &password, QString *error) {
87
1/1
✓ Branch 2 → 3 taken 7 times.
7 CredentialStore store;
88
1/1
✓ Branch 3 → 4 taken 7 times.
7 QEventLoop loop;
89
1/1
✓ Branch 4 → 5 taken 7 times.
7 QTimer timeout;
90
1/1
✓ Branch 5 → 6 taken 7 times.
7 timeout.setSingleShot(true);
91
92 7 bool completed = false;
93 7 bool success = false;
94 7 QString writeError;
95
96
1/1
✓ Branch 6 → 7 taken 7 times.
7 QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
97
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (completed)
98 ✗ return;
99 1 completed = true;
100 1 writeError = QStringLiteral("Timed out writing DAV password to keyring");
101 1 loop.quit();
102 });
103
104
2/2
✓ Branch 10 → 11 taken 7 times.
✓ Branch 11 → 12 taken 7 times.
7 store.writePassword(QString::fromLatin1(kDavService), accountName, password,
105
1/1
✓ Branch 8 → 9 taken 7 times.
7 [&](bool ok, const QString &err) {
106
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 6 times.
6 if (completed)
107 ✗ return;
108 6 completed = true;
109 6 success = ok;
110 6 writeError = err;
111 6 loop.quit();
112 });
113
114 // S106-201: a synchronous answer (a refusal off the GUI thread) quits
115 // the loop before exec() — the quit is lost and the loop never ends.
116
1/2
✓ Branch 14 → 15 taken 7 times.
✗ Branch 14 → 19 not taken.
7 if (!completed) {
117
1/1
✓ Branch 15 → 16 taken 7 times.
7 timeout.start(kKeyringTimeoutMs);
118
1/1
✓ Branch 17 → 18 taken 7 times.
7 loop.exec();
119 }
120
121
3/4
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 32 taken 6 times.
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 32 not taken.
7 if (!success && error) {
122 1 *error = writeError.isEmpty()
123
2/4
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 1 time.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 1 time.
2 ? QStringLiteral("Could not write DAV password to keyring")
124 1 : writeError;
125 }
126 7 return success;
127 7 }
128
129 4 bool deletePasswordKeyBlocking(const QString &accountName) {
130
1/1
✓ Branch 2 → 3 taken 4 times.
4 CredentialStore store;
131
1/1
✓ Branch 3 → 4 taken 4 times.
4 QEventLoop loop;
132
1/1
✓ Branch 4 → 5 taken 4 times.
4 QTimer timeout;
133
1/1
✓ Branch 5 → 6 taken 4 times.
4 timeout.setSingleShot(true);
134
135 4 bool completed = false;
136 4 bool success = false;
137
138
1/1
✓ Branch 6 → 7 taken 4 times.
4 QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
139 ✗ if (completed)
140 ✗ return;
141 ✗ completed = true;
142 ✗ loop.quit();
143 });
144
145
2/2
✓ Branch 10 → 11 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
4 store.deletePassword(QString::fromLatin1(kDavService), accountName,
146
1/1
✓ Branch 8 → 9 taken 4 times.
4 [&](bool ok) {
147
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 4 times.
4 if (completed)
148 ✗ return;
149 4 completed = true;
150 4 success = ok;
151 4 loop.quit();
152 });
153
154 // S106-201: a synchronous answer (a refusal off the GUI thread) quits
155 // the loop before exec() — the quit is lost and the loop never ends.
156
1/2
✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 19 not taken.
4 if (!completed) {
157
1/1
✓ Branch 15 → 16 taken 4 times.
4 timeout.start(kKeyringTimeoutMs);
158
1/1
✓ Branch 17 → 18 taken 4 times.
4 loop.exec();
159 }
160 4 return success;
161 4 }
162
163 32 bool requiresLocalAuthorization(const QString &accountId,
164 const QString &serverUrl,
165 const QString &username) {
166
1/1
✓ Branch 2 → 3 taken 32 times.
32 QSettings settings;
167
1/1
✓ Branch 7 → 8 taken 32 times.
32 const int count = settings.beginReadArray(QStringLiteral("carddav/accounts"));
168 32 bool required = false;
169
2/2
✓ Branch 96 → 11 taken 7 times.
✓ Branch 96 → 97 taken 25 times.
32 for (int i = 0; i < count; ++i) {
170
1/1
✓ Branch 11 → 12 taken 7 times.
7 settings.setArrayIndex(i);
171
3/3
✓ Branch 17 → 18 taken 7 times.
✓ Branch 18 → 19 taken 7 times.
✓ Branch 19 → 20 taken 7 times.
14 if (settings.value(QStringLiteral("id")).toString().trimmed() !=
172
2/3
✓ Branch 12 → 13 taken 7 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 7 times.
21 accountId.trimmed()) {
173 ✗ continue;
174 }
175 const bool sameIdentity =
176
1/3
✓ Branch 36 → 37 taken 7 times.
✗ Branch 150 → 151 not taken.
✗ Branch 150 → 152 not taken.
14 DavCredentials::canonicalOrigin(
177
6/18
✓ Branch 34 → 35 taken 7 times.
✓ Branch 35 → 36 taken 7 times.
✓ Branch 65 → 66 taken 7 times.
✗ Branch 65 → 67 not taken.
✓ Branch 67 → 68 taken 7 times.
✗ Branch 67 → 69 not taken.
✓ Branch 69 → 70 taken 7 times.
✗ Branch 69 → 71 not taken.
✓ Branch 71 → 72 taken 7 times.
✗ Branch 71 → 73 not taken.
✗ Branch 154 → 155 not taken.
✗ Branch 154 → 156 not taken.
✗ Branch 158 → 159 not taken.
✗ Branch 158 → 160 not taken.
✗ Branch 163 → 164 not taken.
✗ Branch 163 → 165 not taken.
✗ Branch 167 → 168 not taken.
✗ Branch 167 → 169 not taken.
14 settings.value(QStringLiteral("serverUrl")).toString()) ==
178
4/9
✓ Branch 29 → 30 taken 7 times.
✓ Branch 38 → 39 taken 7 times.
✗ Branch 38 → 50 not taken.
✓ Branch 48 → 49 taken 7 times.
✗ Branch 48 → 50 not taken.
✓ Branch 73 → 74 taken 7 times.
✗ Branch 73 → 75 not taken.
✗ Branch 171 → 172 not taken.
✗ Branch 171 → 173 not taken.
35 DavCredentials::canonicalOrigin(serverUrl) &&
179
7/21
✓ Branch 44 → 45 taken 7 times.
✓ Branch 45 → 46 taken 7 times.
✓ Branch 46 → 47 taken 7 times.
✓ Branch 53 → 54 taken 7 times.
✗ Branch 53 → 55 not taken.
✓ Branch 55 → 56 taken 7 times.
✗ Branch 55 → 57 not taken.
✓ Branch 57 → 58 taken 7 times.
✗ Branch 57 → 59 not taken.
✓ Branch 59 → 60 taken 7 times.
✗ Branch 59 → 61 not taken.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
✗ Branch 129 → 130 not taken.
✗ Branch 129 → 131 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 138 → 139 not taken.
✗ Branch 138 → 140 not taken.
✗ Branch 142 → 143 not taken.
✗ Branch 142 → 144 not taken.
14 settings.value(QStringLiteral("username")).toString().trimmed() ==
180
4/9
✓ Branch 39 → 40 taken 7 times.
✓ Branch 51 → 52 taken 7 times.
✗ Branch 51 → 53 not taken.
✓ Branch 61 → 62 taken 7 times.
✗ Branch 61 → 63 not taken.
✓ Branch 63 → 64 taken 7 times.
✗ Branch 63 → 65 not taken.
✗ Branch 146 → 147 not taken.
✗ Branch 146 → 148 not taken.
21 username.trimmed();
181
1/2
✓ Branch 75 → 76 taken 7 times.
✗ Branch 75 → 85 not taken.
14 required = sameIdentity &&
182
5/17
✓ Branch 81 → 82 taken 7 times.
✓ Branch 86 → 87 taken 7 times.
✗ Branch 86 → 88 not taken.
✓ Branch 88 → 89 taken 7 times.
✗ Branch 88 → 90 not taken.
✓ Branch 90 → 91 taken 7 times.
✗ Branch 90 → 92 not taken.
✓ Branch 92 → 93 taken 7 times.
✗ Branch 92 → 94 not taken.
✗ Branch 175 → 176 not taken.
✗ Branch 175 → 177 not taken.
✗ Branch 180 → 181 not taken.
✗ Branch 180 → 182 not taken.
✗ Branch 184 → 185 not taken.
✗ Branch 184 → 186 not taken.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
21 settings.value(QStringLiteral("requiresLocalAuthorization"),
183 false)
184
3/3
✓ Branch 82 → 83 taken 7 times.
✓ Branch 83 → 84 taken 2 times.
✓ Branch 83 → 85 taken 5 times.
7 .toBool();
185 7 break;
186 }
187
1/1
✓ Branch 97 → 98 taken 32 times.
32 settings.endArray();
188 32 return required;
189 32 }
190
191 28 QString persistedCredentialId(const QString &accountId,
192 const QString &serverUrl,
193 const QString &username,
194 const QString &explicitId) {
195
4/4
✓ Branch 2 → 3 taken 28 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 8 taken 26 times.
✓ Branch 6 → 7 taken 2 times.
28 if (!explicitId.trimmed().isEmpty()) return explicitId.trimmed();
196
1/1
✓ Branch 8 → 9 taken 26 times.
26 QSettings settings;
197
1/1
✓ Branch 13 → 14 taken 26 times.
26 const int count = settings.beginReadArray(QStringLiteral("carddav/accounts"));
198 26 QString result;
199 26 int matches = 0;
200
2/2
✓ Branch 98 → 17 taken 7 times.
✓ Branch 98 → 99 taken 26 times.
33 for (int i = 0; i < count; ++i) {
201
1/1
✓ Branch 17 → 18 taken 7 times.
7 settings.setArrayIndex(i);
202
5/6
✓ Branch 18 → 19 taken 7 times.
✓ Branch 23 → 24 taken 7 times.
✓ Branch 24 → 25 taken 7 times.
✓ Branch 25 → 26 taken 7 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 7 times.
14 if (settings.value(QStringLiteral("id")).toString().trimmed() != accountId.trimmed()) continue;
203
7/21
✓ Branch 40 → 41 taken 7 times.
✓ Branch 41 → 42 taken 7 times.
✓ Branch 42 → 43 taken 7 times.
✓ Branch 71 → 72 taken 7 times.
✗ Branch 71 → 73 not taken.
✓ Branch 73 → 74 taken 7 times.
✗ Branch 73 → 75 not taken.
✓ Branch 75 → 76 taken 7 times.
✗ Branch 75 → 77 not taken.
✓ Branch 77 → 78 taken 7 times.
✗ Branch 77 → 79 not taken.
✗ Branch 158 → 159 not taken.
✗ Branch 158 → 160 not taken.
✗ Branch 162 → 163 not taken.
✗ Branch 162 → 164 not taken.
✗ Branch 166 → 167 not taken.
✗ Branch 166 → 168 not taken.
✗ Branch 171 → 172 not taken.
✗ Branch 171 → 173 not taken.
✗ Branch 175 → 176 not taken.
✗ Branch 175 → 177 not taken.
14 if (DavCredentials::canonicalOrigin(settings.value(QStringLiteral("serverUrl")).toString()) !=
204
4/9
✓ Branch 35 → 36 taken 7 times.
✓ Branch 44 → 45 taken 7 times.
✗ Branch 44 → 55 not taken.
✓ Branch 79 → 80 taken 7 times.
✗ Branch 79 → 81 not taken.
✗ Branch 81 → 82 not taken.
✓ Branch 81 → 83 taken 7 times.
✗ Branch 179 → 180 not taken.
✗ Branch 179 → 181 not taken.
35 DavCredentials::canonicalOrigin(serverUrl) ||
205
12/32
✓ Branch 45 → 46 taken 7 times.
✓ Branch 50 → 51 taken 7 times.
✓ Branch 51 → 52 taken 7 times.
✓ Branch 52 → 53 taken 7 times.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 7 times.
✓ Branch 57 → 58 taken 7 times.
✗ Branch 57 → 59 not taken.
✓ Branch 59 → 60 taken 7 times.
✗ Branch 59 → 61 not taken.
✓ Branch 61 → 62 taken 7 times.
✗ Branch 61 → 63 not taken.
✓ Branch 63 → 64 taken 7 times.
✗ Branch 63 → 65 not taken.
✓ Branch 65 → 66 taken 7 times.
✗ Branch 65 → 67 not taken.
✓ Branch 67 → 68 taken 7 times.
✗ Branch 67 → 69 not taken.
✓ Branch 69 → 70 taken 7 times.
✗ Branch 69 → 71 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 137 → 138 not taken.
✗ Branch 137 → 139 not taken.
✗ Branch 141 → 142 not taken.
✗ Branch 141 → 143 not taken.
✗ Branch 146 → 147 not taken.
✗ Branch 146 → 148 not taken.
✗ Branch 150 → 151 not taken.
✗ Branch 150 → 152 not taken.
✗ Branch 154 → 155 not taken.
✗ Branch 154 → 156 not taken.
21 settings.value(QStringLiteral("username")).toString().trimmed() != username.trimmed()) continue;
206
3/3
✓ Branch 87 → 88 taken 7 times.
✓ Branch 88 → 89 taken 7 times.
✓ Branch 89 → 90 taken 7 times.
7 result = settings.value(QStringLiteral("credentialId")).toString().trimmed();
207 7 ++matches;
208 }
209
1/1
✓ Branch 99 → 100 taken 26 times.
26 settings.endArray();
210
2/2
✓ Branch 100 → 101 taken 7 times.
✓ Branch 100 → 102 taken 19 times.
26 return matches == 1 ? result : QString{};
211 26 }
212
213 } // namespace
214
215 namespace DavCredentials {
216
217 53 QString accountName(const QString &accountId, const QString &serverUrl,
218 const QString &username, const QString &credentialId) {
219
3/3
✓ Branch 2 → 3 taken 53 times.
✓ Branch 5 → 6 taken 6 times.
✓ Branch 5 → 17 taken 47 times.
53 if (!credentialId.trimmed().isEmpty())
220
2/2
✓ Branch 10 → 11 taken 6 times.
✓ Branch 11 → 12 taken 6 times.
12 return QStringLiteral("credential/%1").arg(credentialId.trimmed());
221
1/1
✓ Branch 17 → 18 taken 47 times.
47 const QString id = accountId.trimmed().isEmpty()
222
2/4
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 46 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
47 ? QStringLiteral("unknown")
223
3/3
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 23 taken 46 times.
✓ Branch 23 → 24 taken 46 times.
48 : accountId.trimmed();
224
1/1
✓ Branch 27 → 28 taken 47 times.
47 const QString canonical = canonicalOrigin(serverUrl);
225 47 const QString origin = canonical.isEmpty()
226
3/13
✗ Branch 30 → 31 not taken.
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 47 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 47 times.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 47 times.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
✗ Branch 80 → 81 not taken.
✗ Branch 80 → 82 not taken.
✗ Branch 84 → 85 not taken.
✗ Branch 84 → 86 not taken.
47 ? QStringLiteral("invalid:") + serverUrl.trimmed()
227
1/3
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 35 taken 47 times.
✗ Branch 34 → 36 not taken.
47 : canonical;
228 const QString digest =
229
2/2
✓ Branch 42 → 43 taken 47 times.
✓ Branch 43 → 44 taken 47 times.
47 digestMaterial(accountId, origin, username.trimmed());
230
1/1
✓ Branch 48 → 49 taken 47 times.
47 return QStringLiteral("%1/v2/%2").arg(id, digest);
231 47 }
232
233 12 QObject *readPasswordAsync(QObject *owner, const QString &accountId,
234 const QString &serverUrl, const QString &username,
235 PasswordCallback callback,
236 const QString &credentialId) {
237
3/6
✓ Branch 2 → 3 taken 12 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 12 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 12 times.
12 if (!owner || !callback) return nullptr;
238
3/6
✓ Branch 9 → 10 taken 12 times.
✓ Branch 10 → 11 taken 12 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 12 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
12 auto *store = new CredentialStore(owner);
239
1/1
✓ Branch 13 → 14 taken 12 times.
12 const QPointer<CredentialStore> guard(store);
240 18 const auto finish = [guard, accountId, serverUrl, username, callback = std::move(callback)]
241 (QByteArray password, QString error) {
242
6/12
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 6 times.
✓ Branch 6 → 7 taken 6 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 6 times.
✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 6 times.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
6 if (!guard || guard->property("completed").toBool()) return;
243
1/1
✓ Branch 16 → 17 taken 6 times.
6 guard->setProperty("completed", true);
244
2/2
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 28 taken 5 times.
6 if (requiresLocalAuthorization(accountId, serverUrl, username)) {
245 1 password.clear();
246 1 error = QStringLiteral("DAV account requires local authorization");
247 }
248
1/1
✓ Branch 32 → 33 taken 6 times.
6 callback(std::move(password), std::move(error));
249
2/2
✓ Branch 36 → 37 taken 5 times.
✓ Branch 36 → 39 taken 1 time.
6 if (guard) guard->deleteLater();
250 12 };
251
2/2
✓ Branch 20 → 21 taken 12 times.
✓ Branch 21 → 22 taken 12 times.
12 QTimer::singleShot(kKeyringTimeoutMs, store, [finish] {
252 ✗ finish({}, QStringLiteral("DAV password lookup timed out"));
253 ✗ });
254 const QString effectiveCredential = persistedCredentialId(
255
1/1
✓ Branch 23 → 24 taken 12 times.
12 accountId, serverUrl, username, credentialId);
256
2/12
✓ Branch 29 → 30 taken 12 times.
✓ Branch 30 → 31 taken 12 times.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
12 QTimer::singleShot(0, store, [guard, accountId, serverUrl, username,
257 effectiveCredential, finish] {
258
10/23
✓ Branch 2 → 3 taken 9 times.
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 11 not taken.
✓ Branch 5 → 6 taken 9 times.
✓ Branch 7 → 8 taken 9 times.
✗ Branch 7 → 11 not taken.
✓ Branch 8 → 9 taken 9 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 9 times.
✓ Branch 13 → 14 taken 9 times.
✗ Branch 13 → 15 not taken.
✓ Branch 15 → 16 taken 9 times.
✗ Branch 15 → 17 not taken.
✓ Branch 17 → 18 taken 9 times.
✗ Branch 17 → 19 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 29 taken 9 times.
✗ Branch 60 → 61 not taken.
✗ Branch 60 → 62 not taken.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
9 if (accountId.trimmed().isEmpty() || username.trimmed().isEmpty() || canonicalOrigin(serverUrl).isEmpty()) {
259 ✗ finish({}, QStringLiteral("Invalid DAV credential identity"));
260 ✗ return;
261 }
262
2/3
✓ Branch 29 → 30 taken 9 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 40 taken 9 times.
9 if (requiresLocalAuthorization(accountId, serverUrl, username)) {
263 ✗ finish({}, QStringLiteral("DAV account requires local authorization"));
264 ✗ return;
265 }
266
1/1
✓ Branch 40 → 41 taken 9 times.
9 const QString stable = accountName(accountId, serverUrl, username, effectiveCredential);
267
1/1
✓ Branch 41 → 42 taken 9 times.
9 const QString legacy = legacyAccountName(accountId, serverUrl, username);
268
2/2
✓ Branch 50 → 51 taken 9 times.
✓ Branch 51 → 52 taken 9 times.
18 guard->readPassword(QString::fromLatin1(kDavService), stable,
269
2/8
✓ Branch 47 → 48 taken 9 times.
✓ Branch 48 → 49 taken 9 times.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
✗ Branch 101 → 102 not taken.
✗ Branch 101 → 103 not taken.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
18 [guard, stable, legacy, versioned = !effectiveCredential.isEmpty(), finish](bool found, const QByteArray &password) {
270
6/12
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 6 times.
✓ Branch 6 → 7 taken 6 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 6 times.
✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 6 times.
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 58 not taken.
6 if (!guard || guard->property("completed").toBool()) return;
271
3/3
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 21 taken 4 times.
✓ Branch 17 → 18 taken 2 times.
6 if (found) { finish(password, {}); return; }
272
1/3
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 31 taken 4 times.
✗ Branch 26 → 27 not taken.
4 if (versioned) { finish({}, QStringLiteral("DAV password is unavailable")); return; }
273
1/3
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 42 taken 4 times.
✗ Branch 37 → 38 not taken.
4 if (legacy == stable) { finish({}, QStringLiteral("DAV password is unavailable")); return; }
274 // Only the endpoint-bound legacy identity is eligible. Never consult
275 // an unbound account-ID key that could belong to another server.
276
2/2
✓ Branch 49 → 50 taken 4 times.
✓ Branch 50 → 51 taken 4 times.
8 guard->readPassword(QString::fromLatin1(kDavService), legacy,
277
2/8
✓ Branch 46 → 47 taken 4 times.
✓ Branch 47 → 48 taken 4 times.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 94 not taken.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
8 [guard, stable, legacy, finish](bool legacyFound, const QByteArray &legacyPassword) {
278
6/12
✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 6 → 7 taken 4 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 4 times.
✓ Branch 10 → 11 taken 4 times.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 4 times.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
4 if (!guard || guard->property("completed").toBool()) return;
279
3/3
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 24 taken 1 time.
✓ Branch 19 → 20 taken 3 times.
7 if (!legacyFound) { finish({}, QStringLiteral("DAV password is unavailable")); return; }
280
2/2
✓ Branch 31 → 32 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
2 guard->writePassword(QString::fromLatin1(kDavService), stable, legacyPassword,
281
2/8
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
2 [guard, legacy, password = legacyPassword, finish](bool migrated, const QString &) {
282
6/12
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
1 if (!guard || guard->property("completed").toBool()) return;
283
1/3
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 21 taken 1 time.
✗ Branch 17 → 18 not taken.
1 if (!migrated) { finish(password, {}); return; }
284
2/2
✓ Branch 26 → 27 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
2 guard->deletePassword(QString::fromLatin1(kDavService), legacy,
285
3/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
3 [finish, password](bool) { finish(password, {}); });
286 });
287 });
288 });
289 9 });
290 12 return store;
291 12 }
292
293 17 QByteArray readPasswordBlocking(const QString &accountId,
294 const QString &serverUrl,
295 const QString &username,
296 QString *error,
297 const QString &credentialId) {
298
3/3
✓ Branch 2 → 3 taken 17 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 13 taken 16 times.
17 if (requiresLocalAuthorization(accountId, serverUrl, username)) {
299
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 12 not taken.
1 if (error)
300 1 *error = QStringLiteral("DAV account requires local authorization");
301 1 return {};
302 }
303
304 16 bool success = false;
305 const QString effectiveCredential = persistedCredentialId(
306
1/1
✓ Branch 13 → 14 taken 16 times.
16 accountId, serverUrl, username, credentialId);
307
1/1
✓ Branch 14 → 15 taken 16 times.
16 const QString stableName = accountName(accountId, serverUrl, username, effectiveCredential);
308
1/1
✓ Branch 15 → 16 taken 16 times.
16 QByteArray password = readPasswordKeyBlocking(stableName, success, error);
309
2/2
✓ Branch 16 → 17 taken 7 times.
✓ Branch 16 → 18 taken 9 times.
16 if (success)
310 7 return password;
311
2/2
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 8 times.
9 if (!effectiveCredential.isEmpty()) return {};
312
313
1/1
✓ Branch 21 → 22 taken 8 times.
8 const QString legacyName = legacyAccountName(accountId, serverUrl, username);
314
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 8 times.
8 if (legacyName == stableName)
315 ✗ return {};
316
317
1/1
✓ Branch 25 → 26 taken 8 times.
8 password = readPasswordKeyBlocking(legacyName, success, nullptr);
318
2/2
✓ Branch 28 → 29 taken 7 times.
✓ Branch 28 → 30 taken 1 time.
8 if (!success)
319 7 return {};
320
321 1 QString migrationError;
322
2/3
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 33 not taken.
1 if (writePasswordKeyBlocking(stableName, password, &migrationError))
323
1/1
✓ Branch 32 → 33 taken 1 time.
1 deletePasswordKeyBlocking(legacyName);
324
1/2
✓ Branch 33 → 34 taken 1 time.
✗ Branch 33 → 35 not taken.
1 if (error)
325 1 error->clear();
326 1 return password;
327 16 }
328
329 7 bool writePasswordBlocking(const QString &accountId, const QString &serverUrl,
330 const QString &username,
331 const QByteArray &password,
332 QString *error,
333 const QString &credentialId) {
334
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 6 times.
7 if (password.isEmpty())
335 1 return true;
336
1/1
✓ Branch 5 → 6 taken 6 times.
6 const QString boundName = accountName(accountId, serverUrl, username, credentialId);
337
3/3
✓ Branch 6 → 7 taken 6 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 5 times.
6 if (!writePasswordKeyBlocking(boundName, password, error))
338 1 return false;
339
340 // SEC-2026-07-21-24: Delete ALL legacy key forms after an explicit write,
341 // not just the ID-only key. The endpoint-bound legacy key may still hold
342 // an OLD password (e.g. after re-authorization against a changed endpoint);
343 // leaving it orphaned creates a valid stale credential in the keyring.
344 const QString legacyName =
345
1/1
✓ Branch 9 → 10 taken 5 times.
5 legacyAccountName(accountId, serverUrl, username);
346
1/2
✓ Branch 11 → 12 taken 5 times.
✗ Branch 11 → 17 not taken.
5 if (legacyName != boundName) {
347 5 bool legacyExists = false;
348
1/1
✓ Branch 12 → 13 taken 5 times.
5 readPasswordKeyBlocking(legacyName, legacyExists, nullptr);
349
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 5 times.
5 if (legacyExists)
350 ✗ deletePasswordKeyBlocking(legacyName);
351 }
352
353
1/1
✓ Branch 17 → 18 taken 5 times.
5 const QString legacyStableName = accountId.trimmed();
354
3/6
✓ Branch 19 → 20 taken 5 times.
✗ Branch 19 → 25 not taken.
✓ Branch 21 → 22 taken 5 times.
✗ Branch 21 → 25 not taken.
✓ Branch 26 → 27 taken 5 times.
✗ Branch 26 → 32 not taken.
10 if (!legacyStableName.isEmpty() && legacyStableName != boundName &&
355
1/2
✓ Branch 23 → 24 taken 5 times.
✗ Branch 23 → 25 not taken.
5 legacyStableName != legacyName) {
356 5 bool legacyStableExists = false;
357
1/1
✓ Branch 27 → 28 taken 5 times.
5 readPasswordKeyBlocking(legacyStableName, legacyStableExists, nullptr);
358
1/2
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 5 times.
5 if (legacyStableExists)
359 ✗ deletePasswordKeyBlocking(legacyStableName);
360 }
361 5 return true;
362 6 }
363
364 3 bool deletePasswordBlocking(const QString &accountId, const QString &serverUrl,
365 const QString &username,
366 QString *error,
367 const QString &credentialId) {
368
1/1
✓ Branch 2 → 3 taken 3 times.
3 const QString stableName = accountName(accountId, serverUrl, username, credentialId);
369
1/1
✓ Branch 3 → 4 taken 3 times.
3 bool success = deletePasswordKeyBlocking(stableName);
370
371 const QString legacyName =
372
1/1
✓ Branch 4 → 5 taken 3 times.
3 legacyAccountName(accountId, serverUrl, username);
373
1/2
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 17 not taken.
3 if (legacyName != stableName) {
374 3 bool legacyExists = false;
375
1/1
✓ Branch 7 → 8 taken 3 times.
3 readPasswordKeyBlocking(legacyName, legacyExists, nullptr);
376
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 16 taken 3 times.
3 if (legacyExists)
377 ✗ success = deletePasswordKeyBlocking(legacyName) || success;
378 }
379
380
1/1
✓ Branch 17 → 18 taken 3 times.
3 const QString legacyStableName = accountId.trimmed();
381
3/6
✓ Branch 19 → 20 taken 3 times.
✗ Branch 19 → 25 not taken.
✓ Branch 21 → 22 taken 3 times.
✗ Branch 21 → 25 not taken.
✓ Branch 26 → 27 taken 3 times.
✗ Branch 26 → 37 not taken.
6 if (!legacyStableName.isEmpty() && legacyStableName != stableName &&
382
1/2
✓ Branch 23 → 24 taken 3 times.
✗ Branch 23 → 25 not taken.
3 legacyStableName != legacyName) {
383 3 bool legacyStableExists = false;
384
1/1
✓ Branch 27 → 28 taken 3 times.
3 readPasswordKeyBlocking(legacyStableName, legacyStableExists, nullptr);
385
1/2
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 36 taken 3 times.
3 if (legacyStableExists)
386 ✗ success = deletePasswordKeyBlocking(legacyStableName) || success;
387 }
388
389
3/4
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 46 taken 2 times.
✓ Branch 38 → 39 taken 1 time.
✗ Branch 38 → 46 not taken.
3 if (!success && error)
390 1 *error = QStringLiteral("Could not delete DAV password from keyring");
391 3 return success;
392 3 }
393
394 4 QObject *deletePasswordAsync(QObject *owner, const QString &accountId,
395 const QString &serverUrl, const QString &username,
396 std::function<void(bool)> callback,
397 const QString &credentialId) {
398 QStringList names{accountName(accountId, serverUrl, username, credentialId),
399
2/4
✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 8 taken 4 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
12 legacyAccountName(accountId, serverUrl, username)};
400
4/5
✓ Branch 8 → 9 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
✗ Branch 11 → 16 not taken.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 13 → 14 taken 4 times.
4 if (!accountId.trimmed().isEmpty()) names.append(accountId.trimmed());
401
1/1
✓ Branch 16 → 17 taken 4 times.
4 names.removeDuplicates();
402 4 QList<CredentialStore::SecretKey> keys;
403
4/4
✓ Branch 17 → 18 taken 4 times.
✓ Branch 18 → 19 taken 4 times.
✓ Branch 28 → 20 taken 12 times.
✓ Branch 28 → 29 taken 4 times.
16 for (const auto &name : names)
404 24 keys.append({QString::fromLatin1(kDavService), name});
405
1/1
✓ Branch 31 → 32 taken 4 times.
8 return CredentialStore::deletePasswordsAsync(owner, keys, std::move(callback));
406 32 }
407
408 } // namespace DavCredentials
409