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: 84.5% 257 / 0 / 304
Functions: 83.3% 20 / 0 / 24
Branches: 67.0% 299 / 0 / 446

presentation/DavAccountSettings.cpp
Line Branch Exec Source
1 #include "DavAccountSettings.h"
2 #include <QCoreApplication>
3 #include "data/DavSelectionList.h"
4 #include "data/DavCredentials.h"
5 #include "data/CredentialStore.h"
6 #include <QPointer>
7 #include <QTimer>
8 #include <QSettings>
9 #include <QUuid>
10 #include <utility>
11
12 27413 QList<DavAccountSettings::Account> DavAccountSettings::accounts() {
13 27413 QList<Account> rows;
14
1/1
✓ Branch 2 → 3 taken 27413 times.
27413 QSettings settings;
15
1/1
✓ Branch 7 → 8 taken 27413 times.
27413 const int count = settings.beginReadArray(QStringLiteral("carddav/accounts"));
16
2/2
✓ Branch 94 → 11 taken 7611 times.
✓ Branch 94 → 95 taken 27413 times.
35024 for (int i = 0; i < count; ++i) {
17
1/1
✓ Branch 11 → 12 taken 7611 times.
7611 settings.setArrayIndex(i);
18 7611 Account row;
19
2/2
✓ Branch 16 → 17 taken 7611 times.
✓ Branch 17 → 18 taken 7611 times.
7611 row.id = settings.value(QStringLiteral("id")).toString();
20
2/2
✓ Branch 27 → 28 taken 7611 times.
✓ Branch 28 → 29 taken 7611 times.
7611 row.serverUrl = settings.value(QStringLiteral("serverUrl")).toString();
21
2/2
✓ Branch 38 → 39 taken 7611 times.
✓ Branch 39 → 40 taken 7611 times.
7611 row.username = settings.value(QStringLiteral("username")).toString();
22
2/2
✓ Branch 49 → 50 taken 7611 times.
✓ Branch 50 → 51 taken 7611 times.
7611 row.credentialId = settings.value(QStringLiteral("credentialId")).toString();
23
2/2
✓ Branch 60 → 61 taken 7611 times.
✓ Branch 61 → 62 taken 7611 times.
7611 row.selectedBooks = davPathList(settings.value(QStringLiteral("selectedBooks")));
24
2/2
✓ Branch 72 → 73 taken 7611 times.
✓ Branch 73 → 74 taken 7611 times.
15222 row.requiresLocalAuthorization = settings.value(QStringLiteral("requiresLocalAuthorization"), false).toBool();
25
2/2
✓ Branch 82 → 83 taken 7611 times.
✓ Branch 83 → 84 taken 7611 times.
7611 row.mailAccountId = settings.value(QStringLiteral("mailAccountId")).toString();
26
2/3
✓ Branch 90 → 91 taken 7611 times.
✗ Branch 90 → 92 not taken.
✓ Branch 91 → 92 taken 7611 times.
7611 if (!row.id.isEmpty()) rows.append(row);
27 7611 }
28
1/1
✓ Branch 95 → 96 taken 27413 times.
27413 settings.endArray();
29 27413 return rows;
30 27413 }
31
32 12416 QList<DavAccountSettings::CalendarConfig> DavAccountSettings::calendarConfigs() {
33 12416 QList<CalendarConfig> rows;
34
1/1
✓ Branch 2 → 3 taken 12416 times.
12416 QSettings settings;
35
1/1
✓ Branch 7 → 8 taken 12416 times.
12416 const int count = settings.beginReadArray(QStringLiteral("caldav/configs"));
36
2/2
✓ Branch 50 → 11 taken 307 times.
✓ Branch 50 → 51 taken 12416 times.
12723 for (int i = 0; i < count; ++i) {
37
1/1
✓ Branch 11 → 12 taken 307 times.
307 settings.setArrayIndex(i);
38 307 CalendarConfig row;
39
2/2
✓ Branch 16 → 17 taken 307 times.
✓ Branch 17 → 18 taken 307 times.
307 row.accountId = settings.value(QStringLiteral("carddavAccountId")).toString();
40
2/2
✓ Branch 27 → 28 taken 307 times.
✓ Branch 28 → 29 taken 307 times.
307 row.selectedCalendars = davPathList(settings.value(QStringLiteral("selectedCalendars")));
41
2/2
✓ Branch 38 → 39 taken 307 times.
✓ Branch 39 → 40 taken 307 times.
307 row.readOnlyCalendars = davPathList(settings.value(QStringLiteral("readOnlyCalendars")));
42
2/3
✓ Branch 46 → 47 taken 307 times.
✗ Branch 46 → 48 not taken.
✓ Branch 47 → 48 taken 307 times.
307 if (!row.accountId.isEmpty()) rows.append(row);
43 307 }
44
1/1
✓ Branch 51 → 52 taken 12416 times.
12416 settings.endArray();
45 12416 return rows;
46 12416 }
47
48 35 bool DavAccountSettings::storeAccounts(const QList<Account> &rows) {
49
2/2
✓ Branch 2 → 3 taken 35 times.
✓ Branch 3 → 4 taken 35 times.
35 return persist(rows, calendarConfigs());
50 }
51
52 55 bool DavAccountSettings::persist(const QList<Account> &rows, const QList<CalendarConfig> &calendars) {
53
1/1
✓ Branch 2 → 3 taken 55 times.
55 const auto previous = accounts();
54
1/1
✓ Branch 3 → 4 taken 55 times.
55 const auto previousCalendars = calendarConfigs();
55
1/1
✓ Branch 4 → 5 taken 55 times.
55 QSettings settings;
56 56 const auto write = [&settings](const QList<Account> &values, const QList<CalendarConfig> &configs) {
57
1/1
✓ Branch 6 → 7 taken 56 times.
112 settings.remove(QStringLiteral("carddav/accounts"));
58
1/1
✓ Branch 14 → 15 taken 56 times.
112 settings.beginWriteArray(QStringLiteral("carddav/accounts"), values.size());
59
2/2
✓ Branch 115 → 18 taken 57 times.
✓ Branch 115 → 116 taken 56 times.
113 for (int i = 0; i < values.size(); ++i) {
60 57 settings.setArrayIndex(i);
61
1/1
✓ Branch 25 → 26 taken 57 times.
114 settings.setValue(QStringLiteral("id"), values.at(i).id);
62
1/1
✓ Branch 35 → 36 taken 57 times.
114 settings.setValue(QStringLiteral("serverUrl"), values.at(i).serverUrl);
63
1/1
✓ Branch 45 → 46 taken 57 times.
114 settings.setValue(QStringLiteral("username"), values.at(i).username);
64
3/3
✓ Branch 51 → 52 taken 56 times.
✓ Branch 51 → 60 taken 1 time.
✓ Branch 56 → 57 taken 56 times.
113 if (values.at(i).credentialId.isEmpty()) settings.remove(QStringLiteral("credentialId"));
65
1/1
✓ Branch 66 → 67 taken 1 time.
2 else settings.setValue(QStringLiteral("credentialId"), values.at(i).credentialId);
66
1/1
✓ Branch 77 → 78 taken 57 times.
114 settings.setValue(QStringLiteral("selectedBooks"), values.at(i).selectedBooks);
67
1/1
✓ Branch 87 → 88 taken 57 times.
114 settings.setValue(QStringLiteral("requiresLocalAuthorization"), values.at(i).requiresLocalAuthorization);
68
3/3
✓ Branch 93 → 94 taken 53 times.
✓ Branch 93 → 102 taken 4 times.
✓ Branch 98 → 99 taken 53 times.
110 if (values.at(i).mailAccountId.isEmpty()) settings.remove(QStringLiteral("mailAccountId"));
69
1/1
✓ Branch 108 → 109 taken 4 times.
8 else settings.setValue(QStringLiteral("mailAccountId"), values.at(i).mailAccountId);
70 }
71 56 settings.endArray();
72
1/1
✓ Branch 121 → 122 taken 56 times.
112 settings.remove(QStringLiteral("caldav/configs"));
73
1/1
✓ Branch 129 → 130 taken 56 times.
112 settings.beginWriteArray(QStringLiteral("caldav/configs"), configs.size());
74
2/2
✓ Branch 166 → 133 taken 14 times.
✓ Branch 166 → 167 taken 56 times.
70 for (int i = 0; i < configs.size(); ++i) {
75 14 settings.setArrayIndex(i);
76
1/1
✓ Branch 140 → 141 taken 14 times.
28 settings.setValue(QStringLiteral("carddavAccountId"), configs.at(i).accountId);
77
1/1
✓ Branch 150 → 151 taken 14 times.
28 settings.setValue(QStringLiteral("selectedCalendars"), configs.at(i).selectedCalendars);
78
1/1
✓ Branch 160 → 161 taken 14 times.
28 settings.setValue(QStringLiteral("readOnlyCalendars"), configs.at(i).readOnlyCalendars);
79 }
80 56 settings.endArray();
81 56 settings.sync();
82 56 };
83
1/1
✓ Branch 5 → 6 taken 55 times.
55 write(rows, calendars);
84
3/3
✓ Branch 6 → 7 taken 55 times.
✓ Branch 7 → 8 taken 54 times.
✓ Branch 7 → 9 taken 1 time.
55 if (settings.status() == QSettings::NoError) return true;
85 // QSettings also shares an in-process cache. Restore that snapshot after
86 // failed persistence, so a later reader cannot mistake it for saved data.
87
1/1
✓ Branch 9 → 10 taken 1 time.
1 write(previous, previousCalendars);
88 1 return false;
89 55 }
90
91 14 bool DavAccountSettings::storeSelections(const Account &expected, const CalendarConfig &expectedCalendar,
92 const std::optional<QStringList> &books,
93 const std::optional<QStringList> &calendars) {
94
6/10
✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 7 taken 3 times.
✓ Branch 6 → 7 taken 11 times.
✗ Branch 6 → 12 not taken.
✓ Branch 8 → 9 taken 14 times.
✗ Branch 8 → 13 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 14 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 14 times.
14 if ((books && books->size() > 256) || (calendars && calendars->size() > 256)) return false;
95
1/1
✓ Branch 16 → 17 taken 14 times.
14 auto rows = accounts();
96
1/1
✓ Branch 17 → 18 taken 14 times.
14 auto configs = calendarConfigs();
97 14 int index = -1;
98
2/2
✓ Branch 28 → 19 taken 15 times.
✓ Branch 28 → 29 taken 14 times.
29 for (int i = 0; i < rows.size(); ++i) {
99
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 14 times.
15 if (rows.at(i).id != expected.id) continue;
100
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 14 times.
14 if (index >= 0) return false;
101 14 index = i;
102 }
103
5/9
✓ Branch 29 → 30 taken 14 times.
✗ Branch 29 → 34 not taken.
✓ Branch 31 → 32 taken 14 times.
✓ Branch 32 → 33 taken 14 times.
✗ Branch 32 → 34 not taken.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 14 times.
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 14 times.
14 if (index < 0 || rows.at(index) != expected || expected.requiresLocalAuthorization) return false;
104 14 CalendarConfig current{expected.id, {}, {}};
105 14 int configIndex = -1;
106
2/2
✓ Branch 50 → 40 taken 4 times.
✓ Branch 50 → 51 taken 14 times.
18 for (int i = 0; i < configs.size(); ++i) {
107
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 4 times.
4 if (configs.at(i).accountId != expected.id) continue;
108
1/2
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 4 times.
4 if (configIndex >= 0) return false;
109 4 configIndex = i;
110 4 current = configs.at(i);
111 }
112
2/3
✓ Branch 51 → 52 taken 14 times.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 14 times.
14 if (current != expectedCalendar) return false;
113
3/3
✓ Branch 55 → 56 taken 11 times.
✓ Branch 55 → 59 taken 3 times.
✓ Branch 57 → 58 taken 11 times.
14 if (books) rows[index].selectedBooks = *books;
114
1/2
✓ Branch 60 → 61 taken 14 times.
✗ Branch 60 → 67 not taken.
14 if (calendars) {
115 14 current.selectedCalendars = *calendars;
116
3/3
✓ Branch 63 → 64 taken 10 times.
✓ Branch 63 → 65 taken 4 times.
✓ Branch 64 → 67 taken 10 times.
14 if (configIndex < 0) configs.append(current);
117
1/1
✓ Branch 65 → 66 taken 4 times.
4 else configs[configIndex] = current;
118 }
119
1/1
✓ Branch 67 → 68 taken 14 times.
14 return persist(rows, configs);
120 14 }
121
122 4 QString DavAccountSettings::linkedTo(const QString &mailAccountId) {
123
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 3 times.
4 if (mailAccountId.isEmpty()) return {};
124
5/5
✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 16 → 9 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
4 for (const Account &account : accounts())
125
4/4
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 14 taken 1 time.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 22 taken 1 time.
4 if (account.mailAccountId == mailAccountId) return account.id;
126 2 return {};
127 }
128
129 3 bool DavAccountSettings::setMailAccountLink(const QString &davAccountId, const QString &mailAccountId) {
130
1/1
✓ Branch 2 → 3 taken 3 times.
3 auto rows = accounts();
131 3 bool found = false;
132
4/4
✓ Branch 3 → 4 taken 3 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 14 → 6 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
6 for (auto &row : rows) {
133
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 2 times.
3 if (row.id != davAccountId) continue;
134 2 row.mailAccountId = mailAccountId;
135 2 found = true;
136 }
137
4/5
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 19 taken 1 time.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
✗ Branch 17 → 19 not taken.
6 return found && storeAccounts(rows);
138 3 }
139
140 6 std::optional<DavAccountSettings::Account> DavAccountSettings::removeMetadata(const QString &id) {
141
1/1
✓ Branch 2 → 3 taken 6 times.
6 auto rows = accounts();
142 6 std::optional<Account> removed;
143
2/2
✓ Branch 15 → 4 taken 7 times.
✓ Branch 15 → 16 taken 6 times.
13 for (int i = 0; i < rows.size(); ++i) {
144
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 6 times.
7 if (rows.at(i).id != id) continue;
145
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
6 if (removed) return std::nullopt;
146 6 removed = rows.at(i);
147 }
148
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 6 times.
6 if (!removed) return std::nullopt;
149
1/1
✓ Branch 19 → 20 taken 6 times.
13 rows.removeIf([&id](const Account &row) { return row.id == id; });
150
1/1
✓ Branch 20 → 21 taken 6 times.
6 auto configs = calendarConfigs();
151
1/1
✓ Branch 21 → 22 taken 6 times.
8 configs.removeIf([&id](const CalendarConfig &row) { return row.accountId == id; });
152
3/4
✓ Branch 22 → 23 taken 6 times.
✓ Branch 23 → 24 taken 6 times.
✗ Branch 23 → 25 not taken.
✓ Branch 24 → 26 taken 6 times.
6 return persist(rows, configs) ? removed : std::nullopt;
153 6 }
154
155 23 QVariantMap DavAccountSettings::save(const QVariantMap &fields, const PasswordWriter &passwordWriter) {
156
2/2
✓ Branch 6 → 7 taken 23 times.
✓ Branch 7 → 8 taken 23 times.
46 const QString id = fields.value(QStringLiteral("id")).toString();
157
2/2
✓ Branch 13 → 14 taken 19 times.
✓ Branch 13 → 15 taken 4 times.
23 const PasswordWriter writer = passwordWriter ? passwordWriter : PasswordWriter(
158 ✗ [](const QString &accountId, const QString &serverUrl,
159 const QString &username, const QByteArray &password, QString *error) {
160
1/1
✓ Branch 3 → 4 taken 1 time.
2 return DavCredentials::writePasswordBlocking(
161 2 accountId, serverUrl, username, password, error);
162
1/1
✓ Branch 14 → 16 taken 19 times.
23 });
163
3/3
✓ Branch 20 → 21 taken 23 times.
✓ Branch 21 → 22 taken 23 times.
✓ Branch 22 → 23 taken 23 times.
46 QString serverUrl = fields.value(QStringLiteral("serverUrl")).toString().trimmed();
164
3/3
✓ Branch 32 → 33 taken 23 times.
✓ Branch 33 → 34 taken 23 times.
✓ Branch 34 → 35 taken 23 times.
46 const QString username = fields.value(QStringLiteral("username")).toString().trimmed();
165
2/2
✓ Branch 44 → 45 taken 23 times.
✓ Branch 45 → 46 taken 23 times.
46 const QString password = fields.value(QStringLiteral("password")).toString();
166
2/4
✓ Branch 56 → 57 taken 23 times.
✓ Branch 56 → 58 taken 23 times.
✗ Branch 335 → 336 not taken.
✗ Branch 335 → 337 not taken.
92 QVariantMap rejected{{QStringLiteral("ok"), false}};
167
2/2
✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 74 taken 22 times.
23 if (username.isEmpty()) {
168
2/2
✓ Branch 62 → 63 taken 1 time.
✓ Branch 67 → 68 taken 1 time.
2 rejected.insert(QStringLiteral("error"), tr("Enter a user name."));
169 1 return rejected;
170 }
171 // The canonical https-origin check is the same boundary the keyring and
172 // the DAV clients use; it rejects plain http, embedded credentials and
173 // malformed URLs before anything is persisted.
174
1/1
✓ Branch 74 → 75 taken 22 times.
22 const QString canonical = DavCredentials::canonicalOrigin(serverUrl);
175
2/2
✓ Branch 76 → 77 taken 3 times.
✓ Branch 76 → 89 taken 19 times.
22 if (canonical.isEmpty()) {
176
1/1
✓ Branch 82 → 83 taken 3 times.
6 rejected.insert(QStringLiteral("error"),
177
1/1
✓ Branch 77 → 78 taken 3 times.
6 tr("Enter a server address starting with https://"));
178 3 return rejected;
179 }
180 19 serverUrl = canonical;
181
182 19 bool updating = false;
183 19 bool credentialBoundaryChanged = false;
184
1/1
✓ Branch 90 → 91 taken 19 times.
19 const auto original = accounts();
185 19 QList<Account> rows = original;
186 19 Account row;
187 19 row.id = id; row.serverUrl = serverUrl; row.username = username;
188
4/4
✓ Branch 95 → 96 taken 19 times.
✓ Branch 96 → 97 taken 19 times.
✓ Branch 122 → 98 taken 7 times.
✓ Branch 122 → 123 taken 12 times.
19 for (auto &existing : rows) {
189
3/6
✓ Branch 100 → 101 taken 7 times.
✗ Branch 100 → 104 not taken.
✓ Branch 102 → 103 taken 7 times.
✗ Branch 102 → 104 not taken.
✓ Branch 105 → 106 taken 7 times.
✗ Branch 105 → 120 not taken.
7 if (existing.id == id && !id.isEmpty()) {
190
6/9
✓ Branch 106 → 107 taken 7 times.
✓ Branch 108 → 109 taken 6 times.
✓ Branch 108 → 111 taken 1 time.
✓ Branch 110 → 111 taken 1 time.
✓ Branch 110 → 112 taken 5 times.
✓ Branch 113 → 114 taken 7 times.
✗ Branch 113 → 115 not taken.
✗ Branch 375 → 376 not taken.
✗ Branch 375 → 377 not taken.
7 credentialBoundaryChanged = DavCredentials::canonicalOrigin(existing.serverUrl) != serverUrl || existing.username != username;
191 7 row.selectedBooks = existing.selectedBooks;
192 7 row.credentialId = existing.credentialId;
193 7 row.requiresLocalAuthorization = existing.requiresLocalAuthorization;
194 7 row.mailAccountId = existing.mailAccountId;
195 7 existing = row;
196 7 updating = true;
197 7 break;
198 }
199 }
200 // S106-237: an id that names no stored account is refused like the
201 // asynchronous save does — minting a new identity dropped the caller's.
202
6/6
✓ Branch 123 → 124 taken 12 times.
✓ Branch 123 → 127 taken 7 times.
✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 127 taken 11 times.
✓ Branch 128 → 129 taken 1 time.
✓ Branch 128 → 141 taken 18 times.
19 if (!updating && !id.isEmpty()) {
203
1/1
✓ Branch 134 → 135 taken 1 time.
2 rejected.insert(QStringLiteral("error"),
204
1/1
✓ Branch 129 → 130 taken 1 time.
2 QCoreApplication::translate("DavAsyncSave", "The DAV account no longer exists."));
205 1 return rejected;
206 }
207
2/2
✓ Branch 141 → 142 taken 11 times.
✓ Branch 141 → 159 taken 7 times.
18 if (!updating) {
208
2/2
✓ Branch 142 → 143 taken 11 times.
✓ Branch 143 → 144 taken 11 times.
11 row.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
209
2/2
✓ Branch 150 → 151 taken 11 times.
✓ Branch 151 → 152 taken 11 times.
22 row.mailAccountId = fields.value(QStringLiteral("mailAccountId")).toString();
210
1/1
✓ Branch 158 → 159 taken 11 times.
11 rows.append(row);
211 }
212
213 // A secret is required for new accounts; an empty password on update
214 // keeps the stored keyring entry (the user edited other fields only).
215
2/2
✓ Branch 160 → 161 taken 11 times.
✓ Branch 160 → 194 taken 7 times.
18 if (!password.isEmpty()) {
216 // The synchronous compatibility API writes the endpoint-bound key. A
217 // versioned id belongs only to the staged asynchronous path.
218 11 row.credentialId.clear();
219 11 QString error;
220
4/4
✓ Branch 162 → 163 taken 11 times.
✓ Branch 163 → 164 taken 11 times.
✓ Branch 165 → 166 taken 2 times.
✓ Branch 165 → 178 taken 9 times.
11 if (!writer(row.id, serverUrl, username, password.toUtf8(), &error)) {
221
1/1
✓ Branch 171 → 172 taken 2 times.
4 rejected.insert(QStringLiteral("error"),
222
1/1
✓ Branch 166 → 167 taken 2 times.
4 tr("The password could not be stored in the keyring."));
223 2 return rejected;
224 }
225
4/4
✓ Branch 178 → 179 taken 9 times.
✓ Branch 179 → 180 taken 9 times.
✓ Branch 187 → 181 taken 10 times.
✓ Branch 187 → 188 taken 9 times.
19 for (auto &entry : rows)
226
2/2
✓ Branch 183 → 184 taken 9 times.
✓ Branch 183 → 185 taken 1 time.
10 if (entry.id == row.id) entry.requiresLocalAuthorization = false;
227
4/4
✓ Branch 190 → 191 taken 9 times.
✓ Branch 190 → 193 taken 2 times.
✓ Branch 194 → 195 taken 2 times.
✓ Branch 194 → 207 taken 5 times.
18 } else if (!updating) {
228
2/2
✓ Branch 195 → 196 taken 2 times.
✓ Branch 200 → 201 taken 2 times.
4 rejected.insert(QStringLiteral("error"), tr("Enter a password."));
229 2 return rejected;
230
2/2
✓ Branch 207 → 208 taken 2 times.
✓ Branch 207 → 220 taken 3 times.
5 } else if (credentialBoundaryChanged) {
231
2/2
✓ Branch 208 → 209 taken 2 times.
✓ Branch 213 → 214 taken 2 times.
4 rejected.insert(QStringLiteral("error"), tr("Enter a password when changing the server or user name."));
232 2 return rejected;
233 }
234
235
4/4
✓ Branch 220 → 221 taken 12 times.
✓ Branch 221 → 222 taken 12 times.
✓ Branch 223 → 224 taken 1 time.
✓ Branch 223 → 236 taken 11 times.
12 if (accounts() != original) {
236
2/2
✓ Branch 224 → 225 taken 1 time.
✓ Branch 229 → 230 taken 1 time.
2 rejected.insert(QStringLiteral("error"), tr("DAV settings changed while credentials were being stored. Try again."));
237 1 return rejected;
238 }
239
3/3
✓ Branch 236 → 237 taken 11 times.
✓ Branch 237 → 238 taken 1 time.
✓ Branch 237 → 250 taken 10 times.
11 if (!storeAccounts(rows)) {
240
2/2
✓ Branch 238 → 239 taken 1 time.
✓ Branch 243 → 244 taken 1 time.
2 rejected.insert(QStringLiteral("error"), tr("The DAV settings could not be saved."));
241 1 return rejected;
242 }
243
2/4
✓ Branch 260 → 261 taken 20 times.
✓ Branch 260 → 262 taken 10 times.
✗ Branch 478 → 479 not taken.
✗ Branch 478 → 480 not taken.
60 return {{QStringLiteral("ok"), true}, {QStringLiteral("id"), row.id}};
244 132 }
245
246 class DavAsyncSave final : public QObject {
247 ✗ Q_DECLARE_TR_FUNCTIONS(DavAsyncSave)
248 public:
249 1 DavAsyncSave(QObject *owner, QVariantMap fields,
250 DavAccountSettings::AsyncSaveCallback callback,
251 DavAccountSettings::AsyncPasswordWriter writer)
252 1 : QObject(owner), m_fields(std::move(fields)), m_callback(std::move(callback)),
253
4/7
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
2 m_writer(std::move(writer)), m_store(new CredentialStore(this)) {
254
1/1
✓ Branch 18 → 19 taken 1 time.
1 m_deadline.setSingleShot(true);
255
1/1
✓ Branch 19 → 20 taken 1 time.
1 m_deadline.setInterval(30000);
256 1 connect(&m_deadline, &QTimer::timeout, this,
257
1/3
✗ Branch 2 → 3 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 20 → 21 taken 1 time.
1 [this] { finish(false, tr("Timed out while saving the DAV password.")); });
258
1/1
✓ Branch 22 → 23 taken 1 time.
2 QTimer::singleShot(0, this, [this] { begin(); });
259 1 }
260 2 ~DavAsyncSave() override {
261
2/6
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 6 taken 1 time.
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 23 taken 1 time.
1 if (!m_finished && !m_stagedKey.isEmpty())
262 ✗ CredentialStore::deletePasswordsAsync(QCoreApplication::instance(),
263 ✗ {{QStringLiteral("carddav"), m_stagedKey}}, {});
264 2 }
265
266 private:
267 1 void begin() {
268
1/1
✓ Branch 2 → 3 taken 1 time.
1 m_original = DavAccountSettings::accounts();
269
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
2 const QString requestedId = m_fields.value(QStringLiteral("id")).toString();
270
3/3
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
2 QString serverUrl = m_fields.value(QStringLiteral("serverUrl")).toString().trimmed();
271
3/3
✓ Branch 31 → 32 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
2 const QString username = m_fields.value(QStringLiteral("username")).toString().trimmed();
272
3/3
✓ Branch 43 → 44 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
2 const QByteArray password = m_fields.value(QStringLiteral("password")).toString().toUtf8();
273
1/4
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 57 taken 1 time.
✗ Branch 53 → 54 not taken.
✗ Branch 54 → 55 not taken.
1 if (username.isEmpty()) { finish(false, tr("Enter a user name.")); return; }
274
1/1
✓ Branch 57 → 58 taken 1 time.
1 serverUrl = DavCredentials::canonicalOrigin(serverUrl);
275
1/2
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 66 taken 1 time.
1 if (serverUrl.isEmpty()) {
276 ✗ finish(false, tr("Enter a server address starting with https://"));
277 ✗ return;
278 }
279
280 1 int index = -1;
281
1/2
✗ Branch 84 → 67 not taken.
✓ Branch 84 → 85 taken 1 time.
1 for (int i = 0; i < m_original.size(); ++i) {
282 ✗ if (m_original.at(i).id != requestedId || requestedId.isEmpty()) continue;
283 ✗ if (index >= 0) { finish(false, tr("The DAV account identity is stored more than once.")); return; }
284 ✗ index = i;
285 }
286
2/6
✗ Branch 86 → 87 not taken.
✓ Branch 86 → 89 taken 1 time.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 89 not taken.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 95 taken 1 time.
1 if (!requestedId.isEmpty() && index < 0) {
287 ✗ finish(false, tr("The DAV account no longer exists."));
288 ✗ return;
289 }
290
1/2
✗ Branch 95 → 96 not taken.
✓ Branch 95 → 98 taken 1 time.
1 if (index >= 0) m_previous = m_original.at(index);
291
1/2
✗ Branch 98 → 99 not taken.
✓ Branch 98 → 100 taken 1 time.
1 m_row = index >= 0 ? m_previous : DavAccountSettings::Account{};
292
1/2
✓ Branch 104 → 105 taken 1 time.
✗ Branch 104 → 122 not taken.
1 if (m_row.id.isEmpty()) {
293
2/2
✓ Branch 105 → 106 taken 1 time.
✓ Branch 106 → 107 taken 1 time.
1 m_row.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
294
2/2
✓ Branch 113 → 114 taken 1 time.
✓ Branch 114 → 115 taken 1 time.
2 m_row.mailAccountId = m_fields.value(QStringLiteral("mailAccountId")).toString();
295 }
296
1/2
✗ Branch 122 → 123 not taken.
✓ Branch 122 → 129 taken 1 time.
1 const bool boundaryChanged = index >= 0 &&
297 ✗ (DavCredentials::canonicalOrigin(m_previous.serverUrl) != serverUrl ||
298
1/4
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
✗ Branch 130 → 131 not taken.
✓ Branch 130 → 132 taken 1 time.
1 m_previous.username != username);
299
2/8
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 136 taken 1 time.
✗ Branch 134 → 135 not taken.
✗ Branch 134 → 136 not taken.
✗ Branch 137 → 138 not taken.
✓ Branch 137 → 142 taken 1 time.
✗ Branch 138 → 139 not taken.
✗ Branch 139 → 140 not taken.
1 if (password.isEmpty() && index < 0) { finish(false, tr("Enter a password.")); return; }
300
2/6
✗ Branch 143 → 144 not taken.
✓ Branch 143 → 146 taken 1 time.
✗ Branch 144 → 145 not taken.
✗ Branch 144 → 146 not taken.
✗ Branch 147 → 148 not taken.
✓ Branch 147 → 152 taken 1 time.
1 if (password.isEmpty() && boundaryChanged) {
301 ✗ finish(false, tr("Enter a password when changing the server or user name."));
302 ✗ return;
303 }
304 1 m_row.serverUrl = serverUrl;
305 1 m_row.username = username;
306
1/3
✗ Branch 155 → 156 not taken.
✓ Branch 155 → 158 taken 1 time.
✗ Branch 156 → 157 not taken.
1 if (password.isEmpty()) { publish(); return; }
307
308
2/2
✓ Branch 158 → 159 taken 1 time.
✓ Branch 159 → 160 taken 1 time.
1 m_row.credentialId = QUuid::createUuid().toString(QUuid::WithoutBraces);
309 1 m_row.requiresLocalAuthorization = false;
310 1 m_stagedKey = DavCredentials::accountName(m_row.id, m_row.serverUrl,
311
1/1
✓ Branch 162 → 163 taken 1 time.
1 m_row.username, m_row.credentialId);
312
1/1
✓ Branch 165 → 166 taken 1 time.
1 m_deadline.start();
313 1 const auto completion = [this](bool success, const QString &error) {
314
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (m_finished) return;
315
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 1 time.
1 if (!success) {
316 ✗ finish(false, error.isEmpty()
317 ✗ ? tr("The password could not be stored in the keyring.") : error);
318 ✗ return;
319 }
320 1 publish();
321 1 };
322
2/3
✓ Branch 167 → 168 taken 1 time.
✗ Branch 167 → 172 not taken.
✓ Branch 169 → 170 taken 1 time.
1 if (m_writer) m_writer(this, m_stagedKey, password, completion);
323 ✗ else m_store->writePassword(QStringLiteral("carddav"), m_stagedKey,
324 password, completion);
325 1 }
326
327 1 void publish() {
328
1/1
✓ Branch 2 → 3 taken 1 time.
1 m_deadline.stop();
329
3/4
✓ Branch 3 → 4 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 11 taken 1 time.
1 if (DavAccountSettings::accounts() != m_original) {
330 ✗ finish(false, tr("DAV settings changed while credentials were being stored. Try again."));
331 ✗ return;
332 }
333 1 auto rows = m_original;
334 1 int index = -1;
335
1/2
✗ Branch 19 → 13 not taken.
✓ Branch 19 → 20 taken 1 time.
1 for (int i = 0; i < rows.size(); ++i)
336 ✗ if (rows.at(i).id == m_row.id) { index = i; break; }
337
2/4
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 22 not taken.
✓ Branch 21 → 24 taken 1 time.
✗ Branch 22 → 23 not taken.
1 if (index < 0) rows.append(m_row); else rows[index] = m_row;
338
2/3
✓ Branch 24 → 25 taken 1 time.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 30 taken 1 time.
1 if (!DavAccountSettings::storeAccounts(rows)) {
339 ✗ finish(false, tr("The DAV settings could not be saved."));
340 ✗ return;
341 }
342 1 const QString staged = std::exchange(m_stagedKey, {});
343 1 m_finished = true;
344 1 const auto callback = std::move(m_callback);
345 // S106-236 (INV-09): the superseded secret goes also when it lived in
346 // the older id/v2-digest or legacy form (no credential id) — it used to
347 // stay in the keyring after the account moved to a credential id.
348
2/6
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 1 time.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 47 taken 1 time.
1 if (!m_previous.id.isEmpty() && m_previous.credentialId != m_row.credentialId) {
349 ✗ DavCredentials::deletePasswordAsync(QCoreApplication::instance(),
350 ✗ m_previous.id, m_previous.serverUrl, m_previous.username, {},
351 ✗ m_previous.credentialId);
352 }
353 Q_UNUSED(staged)
354
4/7
✓ Branch 48 → 49 taken 1 time.
✗ Branch 48 → 68 not taken.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 63 taken 1 time.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
6 if (callback) callback({{QStringLiteral("ok"), true},
355 2 {QStringLiteral("id"), m_row.id}});
356
1/1
✓ Branch 68 → 69 taken 1 time.
1 deleteLater();
357 5 }
358
359 ✗ void finish(bool success, QString error) {
360 ✗ if (m_finished) return;
361 ✗ m_finished = true;
362 ✗ m_deadline.stop();
363 // A failed keyring write reports from inside the finished signal of a
364 // job owned by m_store; deleting the store synchronously freed that job
365 // while it was still emitting (use-after-free, crash on a later timer;
366 // Sprint 100 SB-14). Remaining jobs are dropped with the store after
367 // the current event; their callbacks see m_finished and return.
368 ✗ if (m_store) m_store->deleteLater();
369 ✗ m_store = nullptr;
370 ✗ const QString staged = std::exchange(m_stagedKey, {});
371 ✗ const auto callback = std::move(m_callback);
372 ✗ const QVariantMap result{{QStringLiteral("ok"), success},
373 ✗ {QStringLiteral("error"), error}};
374 ✗ if (staged.isEmpty()) {
375 ✗ if (callback) callback(result);
376 ✗ deleteLater();
377 ✗ return;
378 }
379 ✗ CredentialStore::deletePasswordsAsync(this,
380 ✗ {{QStringLiteral("carddav"), staged}},
381 ✗ [this, callback, result](bool) {
382 ✗ if (callback) callback(result);
383 ✗ deleteLater();
384 ✗ });
385 ✗ }
386
387 QVariantMap m_fields;
388 DavAccountSettings::AsyncSaveCallback m_callback;
389 DavAccountSettings::AsyncPasswordWriter m_writer;
390 CredentialStore *m_store;
391 QTimer m_deadline;
392 QList<DavAccountSettings::Account> m_original;
393 DavAccountSettings::Account m_previous;
394 DavAccountSettings::Account m_row;
395 QString m_stagedKey;
396 bool m_finished = false;
397 };
398
399 1 QObject *DavAccountSettings::saveAsync(QObject *owner, const QVariantMap &fields,
400 AsyncSaveCallback callback,
401 const AsyncPasswordWriter &passwordWriter) {
402
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (!owner) return nullptr;
403
3/6
✓ Branch 5 → 6 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 29 not taken.
1 return new DavAsyncSave(owner, fields, std::move(callback), passwordWriter);
404 }
405