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: 89.9% 331 / 0 / 368
Functions: 100.0% 33 / 0 / 33
Branches: 81.7% 509 / 0 / 623

service/CardDavClient.cpp
Line Branch Exec Source
1 #include "CardDavClient.h"
2
3 #include <QDomDocument>
4 #include <QLoggingCategory>
5 #include <QNetworkAccessManager>
6 #include <QNetworkRequest>
7 #include <QSet>
8 #include <QTimer>
9
10 #include "DavNetworkLimits.h"
11 #include "DavPushXml.h"
12 #include "DavXmlHelper.h"
13
14
4/7
✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 8 taken 281 times.
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 9 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
290 Q_LOGGING_CATEGORY(lcCardDav, "mailjd.carddav")
15
16 using namespace DavXmlHelper;
17
18 namespace {
19
20 const QByteArray kBookProperties =
21 "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
22 "<d:propfind xmlns:d=\"DAV:\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\" "
23 "xmlns:cs=\"http://calendarserver.org/ns/\"><d:prop>"
24 "<d:resourcetype/><d:displayname/><cs:getctag/><d:sync-token/><d:current-user-privilege-set/>"
25 "<card:supported-address-data/><card:max-resource-size/><d:supported-report-set/>"
26 "</d:prop></d:propfind>";
27
28 } // namespace
29
30 234 CardDavClient::CardDavClient(const QString &serverUrl, const QString &username, const QString &password,
31 234 QObject *parent)
32 234 : QObject(parent), m_serverUrl(DavNetworkLimits::requestServerUrl(serverUrl)), m_username(username),
33
1/1
✓ Branch 3 → 4 taken 234 times.
468 m_password(password) {
34
3/3
✓ Branch 9 → 10 taken 234 times.
✓ Branch 10 → 11 taken 31 times.
✓ Branch 10 → 12 taken 203 times.
234 if (m_serverUrl.endsWith('/'))
35
1/1
✓ Branch 11 → 12 taken 31 times.
31 m_serverUrl.chop(1);
36
3/6
✓ Branch 12 → 13 taken 234 times.
✓ Branch 13 → 14 taken 234 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 234 times.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
234 m_nam = new QNetworkAccessManager(this);
37
1/1
✓ Branch 16 → 17 taken 234 times.
234 m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy);
38 234 }
39
40 232 void CardDavClient::setNetworkAccessManager(QNetworkAccessManager *nam) {
41
3/6
✓ Branch 2 → 3 taken 232 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 232 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 232 times.
✗ Branch 7 → 10 not taken.
232 if (m_nam && m_nam->parent() == this)
42
1/2
✓ Branch 8 → 9 taken 232 times.
✗ Branch 8 → 10 not taken.
232 delete m_nam;
43 232 m_nam = nam;
44
2/2
✓ Branch 10 → 11 taken 231 times.
✓ Branch 10 → 13 taken 1 time.
232 if (m_nam) {
45 231 m_nam->setParent(this);
46 231 m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy);
47 }
48 232 }
49
50 428 QByteArray CardDavClient::authHeader() const {
51 // No credentials means an intentionally anonymous DAV request.
52
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 427 times.
428 if (m_username.isEmpty())
53 1 return {};
54 // T-504: Refuse to send credentials over unencrypted connections
55
5/5
✓ Branch 6 → 7 taken 427 times.
✓ Branch 7 → 8 taken 427 times.
✓ Branch 8 → 9 taken 427 times.
✓ Branch 13 → 14 taken 47 times.
✓ Branch 13 → 25 taken 380 times.
427 if (QUrl(m_serverUrl).scheme().toLower() == QLatin1String("http")) {
56
5/5
✓ Branch 14 → 15 taken 47 times.
✓ Branch 18 → 19 taken 47 times.
✓ Branch 19 → 20 taken 47 times.
✓ Branch 23 → 16 taken 47 times.
✓ Branch 23 → 24 taken 47 times.
94 qCWarning(lcCardDav) << "Refusing Basic Auth over HTTP — use HTTPS";
57 47 return {};
58 }
59
5/5
✓ Branch 28 → 29 taken 380 times.
✓ Branch 29 → 30 taken 380 times.
✓ Branch 30 → 31 taken 380 times.
✓ Branch 32 → 33 taken 380 times.
✓ Branch 33 → 34 taken 380 times.
1140 return "Basic " + QByteArray((m_username + QStringLiteral(":") + m_password).toUtf8()).toBase64();
60 }
61
62 #ifdef MAILJD_E2E_TESTING
63 351 void CardDavClient::reportE2eTransportPhase(const QString &operation) {
64
2/2
✓ Branch 6 → 7 taken 351 times.
✓ Branch 7 → 8 taken 351 times.
1053 emit e2eTransportPhase(QStringLiteral("request-dispatch:%1").arg(operation), ++m_e2eRequestToken);
65 351 }
66 #endif
67
68 11069 QString CardDavClient::resolveDavUrl(const QString &href) const {
69 11069 const QString resolved = CardDavXml::resolveHref(m_serverUrl, href);
70
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 16 taken 11066 times.
11069 if (resolved.isEmpty())
71
5/5
✓ Branch 5 → 6 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 14 → 7 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
6 qCWarning(lcCardDav) << "Rejected cross-origin DAV URL";
72 11069 return resolved;
73 ✗ }
74
75 1 QString CardDavClient::newResourceUrl(const QString &bookPath, const QString &name) const {
76
1/1
✓ Branch 2 → 3 taken 1 time.
1 QString book = resolveDavUrl(bookPath);
77
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 if (book.isEmpty())
78 ✗ return {};
79
2/3
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 14 not taken.
1 if (!book.endsWith(QLatin1Char('/')))
80
1/1
✓ Branch 12 → 13 taken 1 time.
1 book += QLatin1Char('/');
81
4/4
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
1 return book + QString::fromUtf8(QUrl::toPercentEncoding(name)) + QStringLiteral(".vcf");
82 1 }
83
84 234 QNetworkReply *CardDavClient::send(const QByteArray &verb, const QString &url, const QByteArray &depth,
85 const QByteArray &body, const QString &operation) {
86
2/2
✓ Branch 2 → 3 taken 234 times.
✓ Branch 3 → 4 taken 234 times.
234 QNetworkRequest request{QUrl(url)};
87
3/3
✓ Branch 5 → 6 taken 234 times.
✓ Branch 6 → 7 taken 234 times.
✓ Branch 7 → 8 taken 234 times.
234 request.setRawHeader("Authorization", authHeader());
88
2/2
✓ Branch 10 → 11 taken 234 times.
✓ Branch 11 → 12 taken 234 times.
234 request.setRawHeader("Depth", depth);
89
3/3
✓ Branch 13 → 14 taken 234 times.
✓ Branch 14 → 15 taken 234 times.
✓ Branch 15 → 16 taken 234 times.
234 request.setRawHeader("Content-Type", "application/xml; charset=utf-8");
90 #ifdef MAILJD_E2E_TESTING
91
1/1
✓ Branch 18 → 19 taken 222 times.
222 reportE2eTransportPhase(operation);
92 #else
93 Q_UNUSED(operation);
94 #endif
95
2/2
✓ Branch 18 → 19 taken 12 times.
✓ Branch 19 → 20 taken 222 times.
234 QNetworkReply *reply = m_nam->sendCustomRequest(request, verb, body);
96
2/2
✓ Branch 19 → 20 taken 12 times.
✓ Branch 20 → 21 taken 222 times.
234 DavNetworkLimits::apply(reply);
97 234 return reply;
98 234 }
99
100 137 bool CardDavClient::replyOk(QNetworkReply *reply, const QString &what) {
101
1/1
✓ Branch 2 → 3 taken 137 times.
137 const QString limitError = DavNetworkLimits::failureReason(reply);
102
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 19 taken 137 times.
137 if (!limitError.isEmpty()) {
103 ✗ qCWarning(lcCardDav) << what << "failed:" << limitError;
104 ✗ emit syncFailed(limitError);
105 ✗ return false;
106 }
107
3/3
✓ Branch 19 → 20 taken 137 times.
✓ Branch 20 → 21 taken 8 times.
✓ Branch 20 → 40 taken 129 times.
137 if (reply->error() != QNetworkReply::NoError) {
108
8/8
✓ Branch 21 → 22 taken 8 times.
✓ Branch 25 → 26 taken 8 times.
✓ Branch 26 → 27 taken 8 times.
✓ Branch 27 → 28 taken 8 times.
✓ Branch 28 → 29 taken 8 times.
✓ Branch 29 → 30 taken 8 times.
✓ Branch 34 → 23 taken 8 times.
✓ Branch 34 → 35 taken 8 times.
16 qCWarning(lcCardDav) << what << "failed:" << reply->errorString();
109
1/1
✓ Branch 35 → 36 taken 8 times.
8 m_lastNetworkError = reply->error();
110
2/2
✓ Branch 36 → 37 taken 8 times.
✓ Branch 37 → 38 taken 8 times.
8 emit syncFailed(reply->errorString());
111 8 return false;
112 }
113 129 return true;
114 137 }
115
116 // ─── discovery ─────────────────────────────────────────────
117
118 21 void CardDavClient::discoverAddressBooks() {
119 21 m_discoveryError.clear();
120 21 m_discoveryRedirects = 0;
121 21 m_lastNetworkError = QNetworkReply::NoError;
122 21 requestDiscovery(m_serverUrl, DiscoveryStep::ConfiguredCollection);
123 21 }
124
125 31 void CardDavClient::requestDiscovery(const QString &url, DiscoveryStep step) {
126 31 m_discoveryStep = step;
127
1/1
✓ Branch 2 → 3 taken 31 times.
31 const QString resolved = resolveDavUrl(url);
128
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 31 times.
31 if (resolved.isEmpty()) {
129 ✗ advanceDiscovery(QStringLiteral("Cross-origin discovery URL rejected"));
130 ✗ return;
131 }
132 31 QByteArray body;
133
1/1
✓ Branch 12 → 13 taken 31 times.
31 QByteArray depth = "1";
134
2/2
✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 16 taken 26 times.
31 if (step == DiscoveryStep::WellKnown) {
135
1/1
✓ Branch 14 → 15 taken 5 times.
5 depth = "0";
136
1/1
✓ Branch 15 → 20 taken 5 times.
5 body = "<d:propfind xmlns:d=\"DAV:\"><d:prop><d:current-user-principal/></d:prop></d:propfind>";
137
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 25 times.
26 } else if (step == DiscoveryStep::Principal) {
138
1/1
✓ Branch 17 → 18 taken 1 time.
1 depth = "0";
139 body = "<d:propfind xmlns:d=\"DAV:\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\"><d:prop>"
140
1/1
✓ Branch 18 → 20 taken 1 time.
1 "<card:addressbook-home-set/></d:prop></d:propfind>";
141 } else {
142 25 body = kBookProperties;
143 }
144
2/2
✓ Branch 20 → 21 taken 31 times.
✓ Branch 21 → 22 taken 31 times.
31 QNetworkRequest request{QUrl(resolved)};
145
3/3
✓ Branch 23 → 24 taken 31 times.
✓ Branch 24 → 25 taken 31 times.
✓ Branch 25 → 26 taken 31 times.
31 request.setRawHeader("Authorization", authHeader());
146
2/2
✓ Branch 28 → 29 taken 31 times.
✓ Branch 29 → 30 taken 31 times.
31 request.setRawHeader("Depth", depth);
147
3/3
✓ Branch 31 → 32 taken 31 times.
✓ Branch 32 → 33 taken 31 times.
✓ Branch 33 → 34 taken 31 times.
31 request.setRawHeader("Content-Type", "application/xml; charset=utf-8");
148 // /.well-known/carddav answers with a redirect; Qt would turn a followed
149 // 301/302 into a GET, so discovery follows redirects itself.
150
1/1
✓ Branch 37 → 38 taken 31 times.
31 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
151 #ifdef MAILJD_E2E_TESTING
152
1/1
✓ Branch 42 → 43 taken 1 time.
1 reportE2eTransportPhase(QStringLiteral("PROPFIND_DISCOVERY"));
153 #endif
154
4/4
✓ Branch 39 → 40 taken 30 times.
✓ Branch 40 → 41 taken 30 times.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
31 auto *reply = m_nam->sendCustomRequest(request, "PROPFIND", body);
155
2/2
✓ Branch 42 → 43 taken 30 times.
✓ Branch 48 → 49 taken 1 time.
31 DavNetworkLimits::apply(reply);
156
2/2
✓ Branch 43 → 44 taken 30 times.
✓ Branch 49 → 50 taken 1 time.
62 connect(reply, &QNetworkReply::finished, this, [this, reply] { onDiscoverReply(reply); });
157 31 }
158
159 9 void CardDavClient::advanceDiscovery(const QString &reason) {
160
1/2
✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 5 not taken.
9 if (!reason.isEmpty())
161 9 m_discoveryError = reason;
162
1/1
✓ Branch 5 → 6 taken 9 times.
9 QUrl origin(m_serverUrl);
163
1/1
✓ Branch 9 → 10 taken 9 times.
9 origin.setPath(QStringLiteral("/"));
164
1/1
✓ Branch 13 → 14 taken 9 times.
9 origin.setQuery(QString());
165
1/1
✓ Branch 16 → 17 taken 9 times.
9 origin.setFragment(QString());
166
2/2
✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 33 taken 5 times.
9 if (m_discoveryStep == DiscoveryStep::ConfiguredCollection) {
167
4/4
✓ Branch 22 → 23 taken 4 times.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 25 → 26 taken 4 times.
✓ Branch 26 → 27 taken 4 times.
4 requestDiscovery(origin.resolved(QUrl(QStringLiteral("/.well-known/carddav"))).toString(),
168 DiscoveryStep::WellKnown);
169 4 return;
170 }
171
2/2
✓ Branch 33 → 34 taken 3 times.
✓ Branch 33 → 59 taken 2 times.
5 if (m_discoveryStep != DiscoveryStep::NextcloudFallback) {
172 // T-505: the user name is percent-encoded (no path traversal).
173 const QString fallback =
174
1/1
✓ Branch 44 → 45 taken 3 times.
12 origin.resolved(QUrl(QStringLiteral("/remote.php/dav/addressbooks/users/%1/")
175
4/4
✓ Branch 40 → 41 taken 3 times.
✓ Branch 41 → 42 taken 3 times.
✓ Branch 42 → 43 taken 3 times.
✓ Branch 43 → 44 taken 3 times.
6 .arg(QString::fromUtf8(QUrl::toPercentEncoding(m_username)))))
176
1/1
✓ Branch 46 → 47 taken 3 times.
3 .toString();
177
1/1
✓ Branch 56 → 57 taken 3 times.
3 requestDiscovery(fallback, DiscoveryStep::NextcloudFallback);
178 3 return;
179 3 }
180
3/7
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 64 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 69 taken 2 times.
✗ Branch 138 → 139 not taken.
✗ Branch 138 → 140 not taken.
2 emit syncFailed(m_discoveryError.isEmpty() ? QStringLiteral("CardDAV discovery failed") : m_discoveryError);
181 9 }
182
183 31 void CardDavClient::onDiscoverReply(QNetworkReply *reply) {
184
1/1
✓ Branch 2 → 3 taken 31 times.
31 reply->deleteLater();
185
1/1
✓ Branch 3 → 4 taken 31 times.
31 const QString limitError = DavNetworkLimits::failureReason(reply);
186
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 31 times.
31 if (!limitError.isEmpty()) {
187 ✗ advanceDiscovery(limitError);
188 ✗ return;
189 }
190
2/2
✓ Branch 8 → 9 taken 31 times.
✓ Branch 9 → 10 taken 31 times.
31 const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
191
5/8
✓ Branch 11 → 12 taken 30 times.
✓ Branch 11 → 15 taken 1 time.
✓ Branch 12 → 13 taken 30 times.
✗ Branch 12 → 15 not taken.
✓ Branch 13 → 14 taken 30 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 55 taken 30 times.
31 if (status == 301 || status == 302 || status == 307 || status == 308) {
192
4/4
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
2 const QString target = resolveDavUrl(QString::fromUtf8(reply->rawHeader("Location")).trimmed());
193
3/6
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 30 not taken.
✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 30 not taken.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 46 not taken.
1 if (!target.isEmpty() && m_discoveryRedirects < 3) {
194 1 ++m_discoveryRedirects;
195
7/7
✓ Branch 32 → 33 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 43 → 34 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
2 qCInfo(lcCardDav) << "Discovery redirected (" << status << ")";
196
1/1
✓ Branch 44 → 45 taken 1 time.
1 requestDiscovery(target, m_discoveryStep);
197 1 return;
198 }
199 ✗ advanceDiscovery(QStringLiteral("CardDAV discovery redirect refused"));
200 ✗ return;
201 1 }
202
3/3
✓ Branch 55 → 56 taken 30 times.
✓ Branch 56 → 57 taken 5 times.
✓ Branch 56 → 64 taken 25 times.
30 if (reply->error() != QNetworkReply::NoError) {
203 // A rejected password stays the reported cause.
204
2/2
✓ Branch 57 → 58 taken 4 times.
✓ Branch 57 → 60 taken 1 time.
5 if (m_lastNetworkError != QNetworkReply::AuthenticationRequiredError)
205
1/1
✓ Branch 58 → 59 taken 4 times.
4 m_lastNetworkError = reply->error();
206
2/2
✓ Branch 60 → 61 taken 5 times.
✓ Branch 61 → 62 taken 5 times.
5 advanceDiscovery(reply->errorString());
207 5 return;
208 }
209
1/1
✓ Branch 64 → 65 taken 25 times.
25 const QByteArray data = reply->readAll();
210
1/1
✓ Branch 65 → 66 taken 25 times.
25 QDomDocument doc;
211
3/3
✓ Branch 66 → 67 taken 25 times.
✓ Branch 67 → 68 taken 3 times.
✓ Branch 67 → 75 taken 22 times.
25 if (!DavXmlHelper::parseInto(doc, data)) {
212
1/1
✓ Branch 71 → 72 taken 3 times.
3 advanceDiscovery(QStringLiteral("Invalid XML response"));
213 3 return;
214 }
215
4/4
✓ Branch 75 → 76 taken 21 times.
✓ Branch 75 → 77 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✓ Branch 76 → 113 taken 20 times.
22 if (m_discoveryStep == DiscoveryStep::WellKnown || m_discoveryStep == DiscoveryStep::Principal) {
216 1 const QString property = m_discoveryStep == DiscoveryStep::WellKnown ? QStringLiteral("current-user-principal")
217
6/6
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 81 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
✓ Branch 84 → 86 taken 1 time.
✓ Branch 86 → 87 taken 1 time.
✓ Branch 86 → 88 taken 1 time.
4 : QStringLiteral("addressbook-home-set");
218
2/2
✓ Branch 88 → 89 taken 2 times.
✓ Branch 89 → 90 taken 2 times.
2 const QString href = hrefOfProperty(doc.documentElement(), property);
219
2/3
✗ Branch 92 → 93 not taken.
✓ Branch 92 → 94 taken 2 times.
✓ Branch 94 → 95 taken 2 times.
2 const QString resolved = href.isEmpty() ? QString() : resolveDavUrl(href);
220
1/2
✗ Branch 96 → 97 not taken.
✓ Branch 96 → 104 taken 2 times.
2 if (resolved.isEmpty()) {
221 ✗ advanceDiscovery(QStringLiteral("CardDAV discovery property missing"));
222 ✗ return;
223 }
224
3/3
✓ Branch 104 → 105 taken 1 time.
✓ Branch 104 → 106 taken 1 time.
✓ Branch 107 → 108 taken 2 times.
2 requestDiscovery(resolved, m_discoveryStep == DiscoveryStep::WellKnown ? DiscoveryStep::Principal
225 : DiscoveryStep::Home);
226 2 return;
227 2 }
228
1/1
✓ Branch 113 → 114 taken 20 times.
20 const QList<AddressBookInfo> books = booksIn(data);
229
2/2
✓ Branch 115 → 116 taken 1 time.
✓ Branch 115 → 123 taken 19 times.
20 if (books.isEmpty()) {
230
1/1
✓ Branch 119 → 120 taken 1 time.
1 advanceDiscovery(QStringLiteral("No address books found at discovery URL"));
231 1 return;
232 }
233
7/7
✓ Branch 123 → 124 taken 19 times.
✓ Branch 127 → 128 taken 19 times.
✓ Branch 128 → 129 taken 19 times.
✓ Branch 130 → 131 taken 19 times.
✓ Branch 131 → 132 taken 19 times.
✓ Branch 135 → 125 taken 19 times.
✓ Branch 135 → 136 taken 19 times.
38 qCInfo(lcCardDav) << "Discovered" << books.size() << "address books";
234
1/1
✓ Branch 136 → 137 taken 19 times.
19 emit addressBooksDiscovered(books);
235 44 }
236
237 75 AddressBookInfo CardDavClient::bookFrom(const QDomElement &response) const {
238 75 return CardDavXml::book(response);
239 }
240
241 20 QList<AddressBookInfo> CardDavClient::booksIn(const QByteArray &xml) const {
242 20 return CardDavXml::parseBooks(xml);
243 }
244
245 // ─── sync ──────────────────────────────────────────────────
246
247 84 void CardDavClient::fetchBook(const QString &bookPath) {
248
1/1
✓ Branch 2 → 3 taken 84 times.
84 const QString url = resolveDavUrl(bookPath);
249
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 84 times.
84 if (url.isEmpty()) {
250 ✗ emit syncFailed(QStringLiteral("Cross-origin URL rejected"));
251 ✗ return;
252 }
253 // Sprint 96 S96.6.2: the WebDAV-Push offer travels with the book's
254 // properties (no extra request). A server that rejects the unknown
255 // properties gets the same PROPFIND once more without them.
256 84 const bool push = !m_pushPropertiesRejected;
257 84 QByteArray body = kBookProperties;
258
2/2
✓ Branch 13 → 14 taken 83 times.
✓ Branch 13 → 31 taken 1 time.
84 if (push)
259
3/3
✓ Branch 14 → 15 taken 83 times.
✓ Branch 15 → 16 taken 83 times.
✓ Branch 18 → 19 taken 83 times.
166 body.replace("<d:prop>", "<d:prop>" + DavPushXml::discoveryProperties())
260
4/4
✓ Branch 19 → 20 taken 83 times.
✓ Branch 20 → 21 taken 83 times.
✓ Branch 21 → 22 taken 83 times.
✓ Branch 24 → 25 taken 83 times.
83 .replace("<d:propfind ", "<d:propfind xmlns:P=\"" + DavPushXml::kNamespace.toLatin1() + "\" ");
261
3/3
✓ Branch 34 → 35 taken 84 times.
✓ Branch 35 → 36 taken 84 times.
✓ Branch 36 → 37 taken 84 times.
84 QNetworkReply *reply = send("PROPFIND", url, "0", body, QStringLiteral("PROPFIND_BOOK"));
262
1/3
✓ Branch 42 → 43 taken 84 times.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 94 not taken.
84 connect(reply, &QNetworkReply::finished, this, [this, reply, bookPath, push] {
263
1/1
✓ Branch 2 → 3 taken 84 times.
84 reply->deleteLater();
264
2/2
✓ Branch 3 → 4 taken 84 times.
✓ Branch 4 → 5 taken 84 times.
84 const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
265
5/6
✓ Branch 6 → 7 taken 83 times.
✓ Branch 6 → 23 taken 1 time.
✓ Branch 7 → 8 taken 82 times.
✓ Branch 7 → 9 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 23 taken 82 times.
84 if (push && (status == 400 || status == 422)) {
266
7/7
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 20 → 11 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
2 qCInfo(lcCardDav) << "PROPFIND with push properties rejected (" << status << "), retrying without";
267 1 m_pushPropertiesRejected = true;
268
1/1
✓ Branch 21 → 22 taken 1 time.
1 fetchBook(bookPath);
269 9 return;
270 }
271
3/3
✓ Branch 26 → 27 taken 83 times.
✓ Branch 29 → 30 taken 8 times.
✓ Branch 29 → 31 taken 75 times.
166 if (!replyOk(reply, QStringLiteral("PROPFIND (book)")))
272 8 return;
273
1/1
✓ Branch 31 → 32 taken 75 times.
75 const QByteArray data = reply->readAll();
274
1/1
✓ Branch 32 → 33 taken 75 times.
75 QDomDocument doc;
275
2/3
✓ Branch 33 → 34 taken 75 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 42 taken 75 times.
75 if (!DavXmlHelper::parseInto(doc, data)) {
276 ✗ emit syncFailed(QStringLiteral("Invalid XML response"));
277 ✗ return;
278 }
279
1/1
✓ Branch 45 → 46 taken 75 times.
75 const auto responses = findElementsByLocalNameDoc(doc, QStringLiteral("response"));
280
1/2
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 57 taken 75 times.
75 if (responses.isEmpty()) {
281 ✗ emit syncFailed(QStringLiteral("Empty PROPFIND response"));
282 ✗ return;
283 }
284
3/3
✓ Branch 57 → 58 taken 75 times.
✓ Branch 58 → 59 taken 75 times.
✓ Branch 59 → 60 taken 75 times.
75 AddressBookInfo book = bookFrom(responses.at(0).toElement());
285
2/2
✓ Branch 62 → 63 taken 1 time.
✓ Branch 62 → 70 taken 74 times.
75 if (!push) {
286 1 book.push.capability = DavPushCapability::Unsupported;
287 1 book.push.reason = QStringLiteral("server rejects the push discovery properties");
288 } else {
289 74 QString pushError;
290
1/1
✓ Branch 70 → 71 taken 74 times.
74 const auto offers = DavPushXml::parseDiscovery(data, &pushError);
291
1/2
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 74 taken 74 times.
74 if (!offers) {
292 ✗ book.push.capability = DavPushCapability::ProbeError;
293 ✗ book.push.reason = pushError;
294 } else {
295 // Depth 0: the one response is this book (its href may be spelled
296 // differently from the stored path).
297
1/2
✓ Branch 77 → 78 taken 74 times.
✗ Branch 77 → 82 not taken.
74 book.push = offers->value(book.path, offers->size() == 1 ? offers->cbegin().value() : DavPushOffer{});
298 }
299 74 }
300 75 book.path = bookPath;
301
1/1
✓ Branch 92 → 93 taken 75 times.
75 emit bookFetched(bookPath, book);
302
3/6
✓ Branch 96 → 97 taken 75 times.
✗ Branch 96 → 98 not taken.
✓ Branch 100 → 101 taken 75 times.
✗ Branch 100 → 102 not taken.
✓ Branch 104 → 105 taken 75 times.
✗ Branch 104 → 107 not taken.
75 });
303 84 }
304
305 55 void CardDavClient::listChanges(const QString &bookPath, const QString &syncToken) {
306
1/1
✓ Branch 2 → 3 taken 55 times.
55 const QString url = resolveDavUrl(bookPath);
307
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 55 times.
55 if (url.isEmpty()) {
308 ✗ emit syncFailed(QStringLiteral("Cross-origin URL rejected"));
309 ✗ return;
310 }
311 const QByteArray body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
312 55 "<d:sync-collection xmlns:d=\"DAV:\"><d:sync-token>" +
313
3/3
✓ Branch 12 → 13 taken 55 times.
✓ Branch 13 → 14 taken 55 times.
✓ Branch 14 → 15 taken 55 times.
110 syncToken.toHtmlEscaped().toUtf8() +
314 "</d:sync-token><d:sync-level>1</d:sync-level>"
315
1/1
✓ Branch 15 → 16 taken 55 times.
55 "<d:prop><d:getetag/></d:prop></d:sync-collection>";
316
3/3
✓ Branch 22 → 23 taken 55 times.
✓ Branch 23 → 24 taken 55 times.
✓ Branch 24 → 25 taken 55 times.
55 QNetworkReply *reply = send("REPORT", url, "0", body, QStringLiteral("REPORT_SYNC_COLLECTION"));
317 55 const bool full = syncToken.isEmpty();
318
1/5
✓ Branch 32 → 33 taken 55 times.
✗ Branch 72 → 73 not taken.
✗ Branch 72 → 74 not taken.
✗ Branch 75 → 76 not taken.
✗ Branch 75 → 77 not taken.
55 connect(reply, &QNetworkReply::finished, this, [this, reply, bookPath, url, full] {
319
1/1
✓ Branch 2 → 3 taken 55 times.
55 reply->deleteLater();
320
2/2
✓ Branch 3 → 4 taken 55 times.
✓ Branch 4 → 5 taken 55 times.
55 const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
321
8/12
✓ Branch 6 → 7 taken 55 times.
✓ Branch 8 → 9 taken 55 times.
✗ Branch 8 → 12 not taken.
✓ Branch 9 → 10 taken 55 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 51 times.
✓ Branch 13 → 14 taken 55 times.
✗ Branch 13 → 15 not taken.
✓ Branch 15 → 16 taken 4 times.
✓ Branch 15 → 43 taken 51 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
55 if (DavNetworkLimits::failureReason(reply).isEmpty() && reply->error() != QNetworkReply::NoError) {
322 // RFC 6578 §3.2: an expired token is 403/409 with valid-sync-token;
323 // 400/404/405/415/501 mean the report is not supported.
324
1/1
✓ Branch 16 → 17 taken 4 times.
4 const QByteArray error = reply->readAll();
325
6/10
✓ Branch 17 → 18 taken 3 times.
✓ Branch 17 → 24 taken 1 time.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 3 times.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 24 not taken.
✓ Branch 22 → 23 taken 3 times.
✗ Branch 22 → 24 not taken.
✓ Branch 25 → 26 taken 3 times.
✓ Branch 25 → 28 taken 1 time.
4 if (!full && (status == 403 || status == 409) && error.contains("valid-sync-token")) {
326
1/1
✓ Branch 26 → 27 taken 3 times.
3 emit collectionSyncRejected(bookPath, true);
327 3 return;
328 }
329
7/14
✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 35 not taken.
✓ Branch 29 → 30 taken 1 time.
✗ Branch 29 → 35 not taken.
✓ Branch 30 → 31 taken 1 time.
✗ Branch 30 → 35 not taken.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 35 not taken.
✓ Branch 32 → 33 taken 1 time.
✗ Branch 32 → 35 not taken.
✓ Branch 33 → 34 taken 1 time.
✗ Branch 33 → 35 not taken.
✓ Branch 34 → 35 taken 1 time.
✗ Branch 34 → 37 not taken.
1 if (status == 400 || status == 403 || status == 404 || status == 405 || status == 409 || status == 415 ||
330 status == 501) {
331
1/1
✓ Branch 35 → 36 taken 1 time.
1 emit collectionSyncRejected(bookPath, false);
332 1 return;
333 }
334 4 }
335
2/3
✓ Branch 46 → 47 taken 51 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 51 times.
102 if (!replyOk(reply, QStringLiteral("REPORT (sync-collection)")))
336 ✗ return;
337
1/1
✓ Branch 51 → 52 taken 51 times.
51 const QByteArray data = reply->readAll();
338 51 DavCardChanges changes;
339 51 changes.full = full;
340 5325 const auto resolve = [this](const QString &href) { return resolveDavUrl(href); };
341
2/3
✓ Branch 53 → 54 taken 51 times.
✗ Branch 55 → 56 not taken.
✓ Branch 55 → 63 taken 51 times.
51 if (!CardDavXml::parseChanges(data, resolve, &changes)) {
342 ✗ emit syncFailed(QStringLiteral("Invalid XML response"));
343 ✗ return;
344 }
345
2/2
✓ Branch 64 → 65 taken 1 time.
✓ Branch 64 → 67 taken 50 times.
51 if (changes.syncToken.isEmpty()) {
346
1/1
✓ Branch 65 → 66 taken 1 time.
1 emit collectionSyncRejected(bookPath, false);
347 1 return;
348 }
349
10/10
✓ Branch 67 → 68 taken 50 times.
✓ Branch 71 → 72 taken 50 times.
✓ Branch 72 → 73 taken 50 times.
✓ Branch 73 → 74 taken 31 times.
✓ Branch 73 → 75 taken 19 times.
✓ Branch 76 → 77 taken 50 times.
✓ Branch 77 → 78 taken 50 times.
✓ Branch 79 → 80 taken 50 times.
✓ Branch 86 → 69 taken 50 times.
✓ Branch 86 → 87 taken 50 times.
100 qCInfo(lcCardDav) << "sync-collection" << (full ? "full" : "delta") << "changed" << changes.changed.size()
350
2/2
✓ Branch 80 → 81 taken 50 times.
✓ Branch 82 → 83 taken 50 times.
50 << "removed" << changes.removed.size();
351
1/1
✓ Branch 87 → 88 taken 50 times.
50 emit changesListed(bookPath, changes);
352
4/4
✓ Branch 90 → 91 taken 50 times.
✓ Branch 90 → 92 taken 1 time.
✓ Branch 94 → 95 taken 50 times.
✓ Branch 94 → 97 taken 1 time.
52 });
353 55 }
354
355 3 void CardDavClient::listEtags(const QString &bookPath) {
356
1/1
✓ Branch 2 → 3 taken 3 times.
3 const QString url = resolveDavUrl(bookPath);
357
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 3 times.
3 if (url.isEmpty()) {
358 ✗ emit syncFailed(QStringLiteral("Cross-origin URL rejected"));
359 ✗ return;
360 }
361 const QByteArray body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
362
1/1
✓ Branch 12 → 13 taken 3 times.
3 "<d:propfind xmlns:d=\"DAV:\"><d:prop><d:resourcetype/><d:getetag/></d:prop></d:propfind>";
363
3/3
✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
✓ Branch 18 → 19 taken 3 times.
3 QNetworkReply *reply = send("PROPFIND", url, "1", body, QStringLiteral("PROPFIND_ETAGS"));
364
1/1
✓ Branch 24 → 25 taken 3 times.
3 connect(reply, &QNetworkReply::finished, this, [this, reply, bookPath] {
365
1/1
✓ Branch 2 → 3 taken 3 times.
3 reply->deleteLater();
366
2/3
✓ Branch 6 → 7 taken 3 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 3 times.
6 if (!replyOk(reply, QStringLiteral("PROPFIND (etags)")))
367 ✗ return;
368 3 QHash<QString, QString> etags;
369 4 const auto resolve = [this](const QString &href) { return resolveDavUrl(href); };
370
3/4
✓ Branch 12 → 13 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 24 taken 3 times.
3 if (!CardDavXml::parseEtags(reply->readAll(), resolve, &etags)) {
371 ✗ emit syncFailed(QStringLiteral("Invalid XML response"));
372 ✗ return;
373 }
374
1/1
✓ Branch 24 → 25 taken 3 times.
3 emit etagsListed(bookPath, etags);
375
1/2
✓ Branch 27 → 28 taken 3 times.
✗ Branch 27 → 30 not taken.
3 });
376 3 }
377
378 92 void CardDavClient::multiget(const QString &bookPath, const QStringList &hrefs) {
379
1/1
✓ Branch 2 → 3 taken 92 times.
92 const QString url = resolveDavUrl(bookPath);
380
3/6
✓ Branch 4 → 5 taken 92 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 92 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 17 taken 92 times.
92 if (url.isEmpty() || hrefs.isEmpty()) {
381 ✗ emit multigetFailed(bookPath, hrefs, QStringLiteral("Cross-origin URL rejected"), false);
382 ✗ return;
383 }
384 QByteArray body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
385 "<card:addressbook-multiget xmlns:d=\"DAV:\" xmlns:card=\"urn:ietf:params:xml:ns:carddav\">"
386
1/1
✓ Branch 17 → 18 taken 92 times.
92 "<d:prop><d:getetag/><card:address-data/></d:prop>";
387
2/2
✓ Branch 37 → 20 taken 5314 times.
✓ Branch 37 → 38 taken 92 times.
5406 for (const QString &href : hrefs)
388
7/7
✓ Branch 21 → 22 taken 5314 times.
✓ Branch 23 → 24 taken 5314 times.
✓ Branch 24 → 25 taken 5314 times.
✓ Branch 25 → 26 taken 5314 times.
✓ Branch 26 → 27 taken 5314 times.
✓ Branch 27 → 28 taken 5314 times.
✓ Branch 28 → 29 taken 5314 times.
5314 body += "<d:href>" + QUrl(href).path(QUrl::FullyEncoded).toHtmlEscaped().toUtf8() + "</d:href>";
389
1/1
✓ Branch 38 → 39 taken 92 times.
92 body += "</card:addressbook-multiget>";
390
3/3
✓ Branch 42 → 43 taken 92 times.
✓ Branch 43 → 44 taken 92 times.
✓ Branch 44 → 45 taken 92 times.
92 QNetworkReply *reply = send("REPORT", url, "1", body, QStringLiteral("REPORT_MULTIGET"));
391
1/3
✓ Branch 51 → 52 taken 92 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
92 connect(reply, &QNetworkReply::finished, this, [this, reply, bookPath, hrefs] {
392
1/1
✓ Branch 2 → 3 taken 92 times.
92 reply->deleteLater();
393
1/1
✓ Branch 3 → 4 taken 92 times.
92 const QString limitError = DavNetworkLimits::failureReason(reply);
394
6/7
✓ Branch 5 → 6 taken 90 times.
✓ Branch 5 → 8 taken 2 times.
✓ Branch 6 → 7 taken 90 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 90 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 43 taken 90 times.
92 if (!limitError.isEmpty() || reply->error() != QNetworkReply::NoError) {
395
1/1
✓ Branch 12 → 13 taken 2 times.
2 const bool tooLarge = limitError.contains(QLatin1String("too large"));
396
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 2 times.
2 if (limitError.isEmpty())
397 ✗ m_lastNetworkError = reply->error();
398
7/7
✓ Branch 17 → 18 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 35 → 19 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
4 qCWarning(lcCardDav) << "REPORT (multiget) of" << hrefs.size() << "cards failed:"
399
2/4
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 2 times.
✗ Branch 28 → 30 not taken.
✓ Branch 30 → 31 taken 2 times.
2 << (limitError.isEmpty() ? reply->errorString() : limitError);
400
2/4
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 2 times.
✗ Branch 38 → 40 not taken.
✓ Branch 40 → 41 taken 2 times.
2 emit multigetFailed(bookPath, hrefs, limitError.isEmpty() ? reply->errorString() : limitError, tooLarge);
401 2 return;
402 }
403 90 QList<DavCardResource> cards;
404 90 QSet<QString> answered; // decoded paths: servers re-encode hrefs differently
405 5303 const auto resolve = [this](const QString &href) { return resolveDavUrl(href); };
406
3/4
✓ Branch 45 → 46 taken 90 times.
✓ Branch 46 → 47 taken 90 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 57 taken 90 times.
90 if (!CardDavXml::parseCards(reply->readAll(), resolve, &cards, &answered)) {
407 ✗ emit multigetFailed(bookPath, hrefs, QStringLiteral("Invalid XML response"), false);
408 ✗ return;
409 }
410 90 QStringList missing;
411
2/2
✓ Branch 70 → 59 taken 5304 times.
✓ Branch 70 → 71 taken 90 times.
5394 for (const QString &href : hrefs)
412
4/4
✓ Branch 60 → 61 taken 5304 times.
✓ Branch 62 → 63 taken 5304 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 66 → 68 taken 5302 times.
10608 if (!answered.contains(QUrl(href).path()))
413
1/1
✓ Branch 67 → 68 taken 2 times.
2 missing << href;
414
1/1
✓ Branch 71 → 72 taken 90 times.
90 emit cardsFetched(bookPath, cards, missing);
415
4/6
✓ Branch 75 → 76 taken 90 times.
✗ Branch 75 → 77 not taken.
✓ Branch 79 → 80 taken 90 times.
✗ Branch 79 → 81 not taken.
✓ Branch 83 → 84 taken 90 times.
✓ Branch 83 → 86 taken 2 times.
92 });
416 92 }
417
418 // ─── raw resource requests ─────────────────────────────────
419
420 169 quint64 CardDavClient::sendResourceRequest(const QByteArray &method, const QString &url, const QByteArray &body,
421 const QList<QPair<QByteArray, QByteArray>> &headers) {
422 169 const quint64 id = ++m_nextResourceRequest;
423
3/3
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 164 times.
✓ Branch 5 → 6 taken 164 times.
169 const QString resolved = url.isEmpty() ? QString() : resolveDavUrl(url);
424
2/2
✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 13 taken 163 times.
169 if (resolved.isEmpty()) {
425 // Answer asynchronously like a network reply would.
426
1/5
✓ Branch 10 → 11 taken 6 times.
✗ Branch 81 → 82 not taken.
✗ Branch 81 → 83 not taken.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
6 QTimer::singleShot(0, this, [this, id, method, url] {
427 6 DavResponse response;
428 6 response.request = id;
429 6 response.method = method;
430 6 response.url = url;
431 6 response.error = QStringLiteral("Rejected or empty resource URL");
432 6 response.refusedLocally = true;
433
1/1
✓ Branch 10 → 11 taken 6 times.
6 emit resourceResponse(response);
434 6 });
435 6 return id;
436 }
437
2/2
✓ Branch 13 → 14 taken 163 times.
✓ Branch 14 → 15 taken 163 times.
163 QNetworkRequest request{QUrl(resolved)};
438
3/3
✓ Branch 16 → 17 taken 163 times.
✓ Branch 17 → 18 taken 163 times.
✓ Branch 18 → 19 taken 163 times.
163 request.setRawHeader("Authorization", authHeader());
439
2/2
✓ Branch 22 → 23 taken 88 times.
✓ Branch 22 → 29 taken 75 times.
163 if (!body.isEmpty())
440
3/3
✓ Branch 23 → 24 taken 88 times.
✓ Branch 24 → 25 taken 88 times.
✓ Branch 25 → 26 taken 88 times.
88 request.setRawHeader("Content-Type", "text/vcard; charset=utf-8");
441 // S96.3.7/S96.6.2: own writes name our push registration of the book.
442
8/8
✓ Branch 30 → 31 taken 75 times.
✓ Branch 30 → 33 taken 88 times.
✓ Branch 32 → 33 taken 18 times.
✓ Branch 32 → 36 taken 57 times.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 105 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 48 taken 162 times.
163 if ((method == "PUT" || method == "DELETE") && !m_pushDontNotify.isEmpty())
443
5/5
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
1 request.setRawHeader("Push-Dont-Notify", "\"" + m_pushDontNotify.toUtf8() + "\"");
444
2/2
✓ Branch 54 → 50 taken 106 times.
✓ Branch 54 → 55 taken 163 times.
269 for (const auto &header : headers)
445
1/1
✓ Branch 51 → 52 taken 106 times.
106 request.setRawHeader(header.first, header.second);
446 #ifdef MAILJD_E2E_TESTING
447
3/3
✓ Branch 58 → 59 taken 128 times.
✓ Branch 59 → 60 taken 128 times.
✓ Branch 60 → 61 taken 128 times.
128 reportE2eTransportPhase(QString::fromLatin1(method) + QStringLiteral("_RESOURCE"));
448 #endif
449 // S106-058: a resource request never follows a redirect — QNAM re-sent a
450 // followed 301/302 PUT/DELETE as GET, and the GET's status/ETag was taken
451 // for the write result (the edit was lost, a DELETE looked done). The 3xx
452 // is the answer.
453
2/2
✓ Branch 56 → 57 taken 35 times.
✓ Branch 66 → 67 taken 128 times.
163 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
454
6/6
✓ Branch 59 → 60 taken 13 times.
✓ Branch 59 → 62 taken 22 times.
✓ Branch 60 → 61 taken 13 times.
✓ Branch 69 → 70 taken 75 times.
✓ Branch 69 → 72 taken 53 times.
✓ Branch 70 → 71 taken 75 times.
238 QNetworkReply *reply = method == "PUT" ? m_nam->put(request, body)
455
6/6
✓ Branch 63 → 64 taken 3 times.
✓ Branch 63 → 66 taken 19 times.
✓ Branch 64 → 65 taken 3 times.
✓ Branch 73 → 74 taken 15 times.
✓ Branch 73 → 76 taken 38 times.
✓ Branch 74 → 75 taken 15 times.
75 : method == "DELETE" ? m_nam->deleteResource(request)
456
2/2
✓ Branch 66 → 67 taken 19 times.
✓ Branch 76 → 77 taken 38 times.
57 : m_nam->get(request);
457
2/2
✓ Branch 69 → 70 taken 35 times.
✓ Branch 79 → 80 taken 128 times.
163 DavNetworkLimits::apply(reply);
458
2/6
✓ Branch 72 → 73 taken 35 times.
✓ Branch 82 → 83 taken 128 times.
✗ Branch 120 → 121 not taken.
✗ Branch 120 → 122 not taken.
✗ Branch 142 → 143 not taken.
✗ Branch 142 → 144 not taken.
163 connect(reply, &QNetworkReply::finished, this, [this, reply, id, method, resolved] {
459
1/1
✓ Branch 2 → 3 taken 154 times.
154 reply->deleteLater();
460 154 DavResponse response;
461 154 response.request = id;
462 154 response.method = method;
463 154 response.url = resolved;
464
1/1
✓ Branch 5 → 6 taken 154 times.
154 response.error = DavNetworkLimits::failureReason(reply);
465
1/2
✓ Branch 9 → 10 taken 154 times.
✗ Branch 9 → 45 not taken.
154 if (response.error.isEmpty()) {
466
2/2
✓ Branch 10 → 11 taken 154 times.
✓ Branch 11 → 12 taken 154 times.
154 response.status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
467
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 18 taken 154 times.
154 if (response.status == 0)
468 ✗ response.error = reply->errorString();
469
1/1
✓ Branch 18 → 19 taken 154 times.
154 response.body = reply->readAll();
470
2/2
✓ Branch 25 → 26 taken 154 times.
✓ Branch 26 → 27 taken 154 times.
308 response.etag = QString::fromUtf8(reply->rawHeader("ETag"));
471
1/1
✓ Branch 34 → 35 taken 154 times.
308 const QByteArray location = reply->rawHeader("Location");
472
2/2
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 43 taken 153 times.
154 if (!location.isEmpty())
473
2/2
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
1 response.location = resolveDavUrl(QString::fromUtf8(location));
474 154 }
475
8/8
✓ Branch 45 → 46 taken 154 times.
✓ Branch 49 → 50 taken 154 times.
✓ Branch 50 → 51 taken 154 times.
✓ Branch 51 → 52 taken 154 times.
✓ Branch 52 → 53 taken 154 times.
✓ Branch 53 → 54 taken 154 times.
✓ Branch 57 → 47 taken 154 times.
✓ Branch 57 → 58 taken 154 times.
308 qCDebug(lcCardDav) << method << "resource ->" << response.status << response.error;
476
1/1
✓ Branch 58 → 59 taken 154 times.
154 emit resourceResponse(response);
477 154 });
478 163 return id;
479 169 }
480
481 92 quint64 CardDavClient::putResource(const QString &url, const QByteArray &body, const QString &ifMatch,
482 bool createOnly) {
483 92 QList<QPair<QByteArray, QByteArray>> headers;
484
2/2
✓ Branch 2 → 3 taken 22 times.
✓ Branch 2 → 11 taken 70 times.
92 if (createOnly) {
485
4/4
✓ Branch 3 → 4 taken 22 times.
✓ Branch 4 → 5 taken 22 times.
✓ Branch 5 → 6 taken 22 times.
✓ Branch 6 → 7 taken 22 times.
22 headers.append(qMakePair(QByteArray("If-None-Match"), QByteArray("*")));
486
3/3
✓ Branch 11 → 12 taken 70 times.
✓ Branch 12 → 13 taken 66 times.
✓ Branch 12 → 21 taken 4 times.
70 } else if (DavEntityTag::isValid(ifMatch)) {
487
4/4
✓ Branch 13 → 14 taken 66 times.
✓ Branch 14 → 15 taken 66 times.
✓ Branch 15 → 16 taken 66 times.
✓ Branch 16 → 17 taken 66 times.
66 headers.append(qMakePair(QByteArray("If-Match"), ifMatch.toUtf8()));
488 } else {
489
5/5
✓ Branch 21 → 22 taken 4 times.
✓ Branch 25 → 26 taken 4 times.
✓ Branch 26 → 27 taken 4 times.
✓ Branch 30 → 23 taken 4 times.
✓ Branch 30 → 31 taken 4 times.
8 qCWarning(lcCardDav) << "Refusing PUT without a valid entity-tag";
490
2/2
✓ Branch 34 → 35 taken 4 times.
✓ Branch 35 → 36 taken 4 times.
4 return sendResourceRequest("PUT", QString(), {}, {});
491 }
492
2/2
✓ Branch 42 → 43 taken 88 times.
✓ Branch 43 → 44 taken 88 times.
88 return sendResourceRequest("PUT", url, body, headers);
493 92 }
494
495 19 quint64 CardDavClient::deleteResource(const QString &url, const QString &ifMatch) {
496
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 25 taken 18 times.
19 if (!DavEntityTag::isValid(ifMatch)) {
497
5/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 6 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
2 qCWarning(lcCardDav) << "Refusing DELETE without a valid entity-tag";
498
2/2
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
1 return sendResourceRequest("DELETE", QString(), {}, {});
499 }
500
5/7
✓ Branch 26 → 27 taken 18 times.
✓ Branch 30 → 31 taken 18 times.
✓ Branch 31 → 32 taken 18 times.
✓ Branch 37 → 38 taken 18 times.
✓ Branch 37 → 39 taken 18 times.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
54 return sendResourceRequest("DELETE", url, {}, {qMakePair(QByteArray("If-Match"), ifMatch.toUtf8())});
501 36 }
502
503
2/2
✓ Branch 4 → 5 taken 58 times.
✓ Branch 5 → 6 taken 58 times.
58 quint64 CardDavClient::getResource(const QString &url) { return sendResourceRequest("GET", url, {}, {}); }
504