service/CalDavClient.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "data/ICalTodoCodec.h" | ||
| 2 | #include "CalDavClient.h" | ||
| 3 | #include "CalDavXml.h" | ||
| 4 | |||
| 5 | #include "performance/PimPerformanceTracker.h" | ||
| 6 | |||
| 7 | #include <QDomDocument> | ||
| 8 | #include <utility> | ||
| 9 | #include <QLoggingCategory> | ||
| 10 | #include <QNetworkAccessManager> | ||
| 11 | #include <QNetworkReply> | ||
| 12 | #include <QNetworkRequest> | ||
| 13 | #include <QRegularExpression> | ||
| 14 | #include <QTimeZone> | ||
| 15 | #include <QTimer> | ||
| 16 | |||
| 17 | #include "DavNetworkLimits.h" | ||
| 18 | #include "data/ICalEventCodec.h" | ||
| 19 | #include "DavXmlHelper.h" | ||
| 20 | #include "DavPushXml.h" | ||
| 21 | #include "util/MessageId.h" | ||
| 22 | |||
| 23 | Q_DECLARE_LOGGING_CATEGORY(lcCalDav) // defined in CalDavXml.cpp | ||
| 24 | |||
| 25 | using namespace DavXmlHelper; | ||
| 26 | |||
| 27 | using CalDavXml::MultistatusSummary; | ||
| 28 | using CalDavXml::summarizeMultistatus; | ||
| 29 | |||
| 30 | 453 | CalDavClient::CalDavClient(const QString &serverUrl, const QString &username, | |
| 31 | 453 | const QString &password, QObject *parent) | |
| 32 | 453 | : QObject(parent), m_serverUrl(DavNetworkLimits::requestServerUrl(serverUrl)), m_username(username), | |
| 33 |
1/1✓ Branch 3 → 4 taken 453 times.
|
906 | m_password(password) { |
| 34 |
3/3✓ Branch 15 → 16 taken 453 times.
✓ Branch 16 → 17 taken 38 times.
✓ Branch 16 → 18 taken 415 times.
|
453 | if (m_serverUrl.endsWith('/')) |
| 35 |
1/1✓ Branch 17 → 18 taken 38 times.
|
38 | m_serverUrl.chop(1); |
| 36 |
3/6✓ Branch 18 → 19 taken 453 times.
✓ Branch 19 → 20 taken 453 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 453 times.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
|
453 | m_nam = new QNetworkAccessManager(this); |
| 37 |
1/1✓ Branch 22 → 23 taken 453 times.
|
453 | m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy); |
| 38 | |||
| 39 | // T-79.F1/H8: inactivity watchdog. Started at construction so an idle | ||
| 40 | // client (caller never issued a request) still releases itself; rearmed | ||
| 41 | // on every request and every finished reply. | ||
| 42 |
3/6✓ Branch 23 → 24 taken 453 times.
✓ Branch 24 → 25 taken 453 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 453 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
|
453 | m_inactivityTimer = new QTimer(this); |
| 43 |
1/1✓ Branch 27 → 28 taken 453 times.
|
453 | m_inactivityTimer->setSingleShot(true); |
| 44 |
1/1✓ Branch 28 → 29 taken 453 times.
|
453 | m_inactivityTimer->setInterval(kInactivityTimeoutMs); |
| 45 | 453 | connect(m_inactivityTimer, &QTimer::timeout, this, | |
| 46 |
1/1✓ Branch 29 → 30 taken 453 times.
|
453 | &CalDavClient::onInactivityTimeout); |
| 47 |
1/1✓ Branch 31 → 32 taken 453 times.
|
453 | m_inactivityTimer->start(); |
| 48 | 453 | } | |
| 49 | |||
| 50 | // T-79.F1/H8: register a dispatched reply for lifetime tracking. The | ||
| 51 | // decrement runs as a QUEUED handler so it always executes after the | ||
| 52 | // reply's direct business handler — result signals (e.g. | ||
| 53 | // calendarsDiscovered) synchronously start follow-up requests, which must | ||
| 54 | // be counted before the zero check runs. | ||
| 55 | 676 | void CalDavClient::trackReply(QNetworkReply *reply) { | |
| 56 | 676 | ++m_pendingReplies; | |
| 57 |
1/1✓ Branch 2 → 3 taken 676 times.
|
676 | m_trackedReplies.insert(reply); |
| 58 | 676 | m_inactivityTimer->start(); | |
| 59 | 676 | connect( | |
| 60 | reply, &QNetworkReply::finished, this, | ||
| 61 |
1/1✓ Branch 4 → 5 taken 676 times.
|
676 | [this, reply]() { |
| 62 | 619 | m_trackedReplies.remove(reply); | |
| 63 | 619 | m_inactivityTimer->start(); | |
| 64 | 619 | emit requestAnswered(); // F-54: account runs restart their budget | |
| 65 |
2/2✓ Branch 5 → 6 taken 383 times.
✓ Branch 5 → 8 taken 236 times.
|
619 | if (--m_pendingReplies == 0) { |
| 66 | 383 | m_inactivityTimer->stop(); | |
| 67 | 383 | emit allRequestsFinished(); | |
| 68 | } | ||
| 69 | 619 | }, | |
| 70 | Qt::QueuedConnection); | ||
| 71 | 676 | } | |
| 72 | |||
| 73 | ✗ | void CalDavClient::onInactivityTimeout() { | |
| 74 | ✗ | if (m_pendingReplies <= 0) { | |
| 75 | // Idle client that never got (or has finished) its work — release it. | ||
| 76 | ✗ | emit allRequestsFinished(); | |
| 77 | ✗ | return; | |
| 78 | } | ||
| 79 | ✗ | qCWarning(lcCalDav) << "CalDAV inactivity timeout with" << m_pendingReplies | |
| 80 | ✗ | << "outstanding replies — aborting"; | |
| 81 | ✗ | m_pendingReplies = 0; | |
| 82 | ✗ | const auto replies = m_trackedReplies; | |
| 83 | ✗ | m_trackedReplies.clear(); | |
| 84 | ✗ | for (QNetworkReply *reply : replies) { | |
| 85 | ✗ | reply->disconnect(this); // no late business/tracking callbacks | |
| 86 | ✗ | reply->abort(); | |
| 87 | ✗ | reply->deleteLater(); | |
| 88 | } | ||
| 89 | // Both signals so sync callers surface the error and write callers can | ||
| 90 | // roll back their optimistic local change. | ||
| 91 | ✗ | m_lastNetworkError = QNetworkReply::TimeoutError; | |
| 92 | ✗ | emit syncFailed(QStringLiteral("CalDAV request timed out")); | |
| 93 | ✗ | emit writeFailed(QStringLiteral("CalDAV request timed out")); | |
| 94 | ✗ | emit allRequestsFinished(); | |
| 95 | ✗ | } | |
| 96 | |||
| 97 | 431 | void CalDavClient::setNetworkAccessManager(QNetworkAccessManager *nam) { | |
| 98 |
5/6✓ Branch 2 → 3 taken 430 times.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 4 → 5 taken 430 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 430 times.
✓ Branch 7 → 10 taken 1 time.
|
431 | if (m_nam && m_nam->parent() == this) |
| 99 |
1/2✓ Branch 8 → 9 taken 430 times.
✗ Branch 8 → 10 not taken.
|
430 | delete m_nam; |
| 100 | 431 | m_nam = nam; | |
| 101 |
2/2✓ Branch 10 → 11 taken 430 times.
✓ Branch 10 → 13 taken 1 time.
|
431 | if (m_nam) { |
| 102 | 430 | m_nam->setParent(this); | |
| 103 | 430 | m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy); | |
| 104 | } | ||
| 105 | 431 | } | |
| 106 | |||
| 107 | 678 | QByteArray CalDavClient::authHeader() const { | |
| 108 | // No credentials → no auth header (e.g. testing with auth-free server) | ||
| 109 |
2/2✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 5 taken 670 times.
|
678 | if (m_username.isEmpty()) |
| 110 | 8 | return {}; | |
| 111 | // T-504: Refuse to send credentials over unencrypted connections | ||
| 112 |
1/1✓ Branch 5 → 6 taken 670 times.
|
670 | QUrl serverUrl(m_serverUrl); |
| 113 |
4/4✓ Branch 7 → 8 taken 670 times.
✓ Branch 8 → 9 taken 670 times.
✓ Branch 12 → 13 taken 52 times.
✓ Branch 12 → 24 taken 618 times.
|
670 | if (serverUrl.scheme().toLower() == QLatin1String("http")) { |
| 114 |
5/5✓ Branch 13 → 14 taken 52 times.
✓ Branch 17 → 18 taken 52 times.
✓ Branch 18 → 19 taken 52 times.
✓ Branch 22 → 15 taken 52 times.
✓ Branch 22 → 23 taken 52 times.
|
104 | qCWarning(lcCalDav) << "Refusing Basic Auth over HTTP — use HTTPS"; |
| 115 | 52 | return {}; | |
| 116 | } | ||
| 117 | return "Basic " + | ||
| 118 | ✗ | QByteArray( | |
| 119 |
3/3✓ Branch 27 → 28 taken 618 times.
✓ Branch 28 → 29 taken 618 times.
✓ Branch 29 → 30 taken 618 times.
|
1854 | (m_username + QStringLiteral(":") + m_password).toUtf8()) |
| 120 |
2/2✓ Branch 31 → 32 taken 618 times.
✓ Branch 32 → 33 taken 618 times.
|
1236 | .toBase64(); |
| 121 | 670 | } | |
| 122 | |||
| 123 | #ifdef MAILJD_E2E_TESTING | ||
| 124 | 480 | void CalDavClient::reportE2eTransportPhase(const QString &operation) { | |
| 125 |
2/2✓ Branch 6 → 7 taken 480 times.
✓ Branch 7 → 8 taken 480 times.
|
960 | emit e2eTransportPhase(QStringLiteral("request-dispatch:%1").arg(operation), |
| 126 | 480 | ++m_e2eRequestToken); | |
| 127 | 480 | } | |
| 128 | #endif | ||
| 129 | |||
| 130 | 152 | bool CalDavClient::isValidEtag(const QString &etag) { | |
| 131 | 152 | return DavEntityTag::isValid(etag); | |
| 132 | } | ||
| 133 | |||
| 134 | 2474 | QString CalDavClient::resolveDavUrl(const QString &href) const { | |
| 135 | 2474 | const QString resolved = DavNetworkLimits::resolveSameOrigin(m_serverUrl, href); | |
| 136 |
8/8✓ Branch 4 → 5 taken 24 times.
✓ Branch 4 → 17 taken 2450 times.
✓ Branch 5 → 6 taken 24 times.
✓ Branch 9 → 10 taken 24 times.
✓ Branch 10 → 11 taken 24 times.
✓ Branch 11 → 12 taken 24 times.
✓ Branch 15 → 7 taken 24 times.
✓ Branch 15 → 16 taken 24 times.
|
2498 | if (resolved.isEmpty()) qCWarning(lcCalDav) << "Rejected cross-origin DAV URL:" << href; |
| 137 | 2474 | return resolved; | |
| 138 | ✗ | } | |
| 139 | |||
| 140 | // ═══════════════════════════════════════════════════════ | ||
| 141 | // Calendar Discovery (PROPFIND) | ||
| 142 | // ═══════════════════════════════════════════════════════ | ||
| 143 | |||
| 144 | 122 | void CalDavClient::discoverCalendars() { | |
| 145 | 122 | m_pushPropertiesRejected = false; | |
| 146 | 122 | m_discoveryError.clear(); | |
| 147 | 122 | m_lastNetworkError = QNetworkReply::NoError; | |
| 148 | 122 | m_discoveryRedirects = 0; | |
| 149 | 122 | requestDiscovery(m_serverUrl, DiscoveryStep::ConfiguredCollection); | |
| 150 | 122 | } | |
| 151 | |||
| 152 | 155 | void CalDavClient::requestDiscovery(const QString &url, DiscoveryStep step) { | |
| 153 |
1/1✓ Branch 2 → 3 taken 155 times.
|
155 | const QString resolved = resolveDavUrl(url); |
| 154 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 155 times.
|
155 | if (resolved.isEmpty()) { |
| 155 | ✗ | m_discoveryStep = step; | |
| 156 | ✗ | advanceDiscovery(QStringLiteral("Cross-origin discovery URL rejected")); | |
| 157 | ✗ | return; | |
| 158 | } | ||
| 159 | 155 | m_discoveryStep = step; | |
| 160 | |||
| 161 |
2/2✓ Branch 12 → 13 taken 155 times.
✓ Branch 13 → 14 taken 155 times.
|
155 | QNetworkRequest request{QUrl(resolved)}; |
| 162 |
3/3✓ Branch 15 → 16 taken 155 times.
✓ Branch 16 → 17 taken 155 times.
✓ Branch 17 → 18 taken 155 times.
|
155 | request.setRawHeader("Authorization", authHeader()); |
| 163 |
5/5✓ Branch 20 → 21 taken 32 times.
✓ Branch 20 → 23 taken 123 times.
✓ Branch 25 → 26 taken 155 times.
✓ Branch 26 → 27 taken 155 times.
✓ Branch 27 → 28 taken 155 times.
|
187 | request.setRawHeader("Depth", |
| 164 |
2/2✓ Branch 21 → 22 taken 29 times.
✓ Branch 21 → 23 taken 3 times.
|
32 | step == DiscoveryStep::ConfiguredCollection || |
| 165 |
2/2✓ Branch 22 → 23 taken 11 times.
✓ Branch 22 → 24 taken 18 times.
|
29 | step == DiscoveryStep::CalendarHome || |
| 166 | step == DiscoveryStep::NextcloudFallback | ||
| 167 | ? "1" | ||
| 168 | : "0"); | ||
| 169 |
3/3✓ Branch 30 → 31 taken 155 times.
✓ Branch 31 → 32 taken 155 times.
✓ Branch 32 → 33 taken 155 times.
|
155 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 170 | // Discovery follows redirects itself: Qt turns a followed 301/302 into a | ||
| 171 | // GET, and /.well-known/caldav (RFC 6764) answers with a redirect. | ||
| 172 |
1/1✓ Branch 36 → 37 taken 155 times.
|
155 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); |
| 173 | |||
| 174 | 155 | QByteArray body; | |
| 175 |
2/2✓ Branch 38 → 39 taken 15 times.
✓ Branch 38 → 40 taken 140 times.
|
155 | if (step == DiscoveryStep::WellKnown) { |
| 176 | body = "<d:propfind xmlns:d=\"DAV:\"><d:prop>" | ||
| 177 |
1/1✓ Branch 39 → 69 taken 15 times.
|
15 | "<d:current-user-principal/></d:prop></d:propfind>"; |
| 178 |
2/2✓ Branch 40 → 41 taken 3 times.
✓ Branch 40 → 42 taken 137 times.
|
140 | } else if (step == DiscoveryStep::Principal) { |
| 179 | body = "<d:propfind xmlns:d=\"DAV:\" " | ||
| 180 | "xmlns:c=\"urn:ietf:params:xml:ns:caldav\"><d:prop>" | ||
| 181 |
1/1✓ Branch 41 → 69 taken 3 times.
|
3 | "<c:calendar-home-set/><c:schedule-outbox-URL/><c:calendar-user-address-set/></d:prop></d:propfind>"; |
| 182 | } else { | ||
| 183 | // Sprint 96 S96.1.2: the WebDAV-Push properties travel with the normal | ||
| 184 | // collection discovery (no extra request), unless the server rejected | ||
| 185 | // them once in this discovery. | ||
| 186 | 137 | const bool push = !m_pushPropertiesRejected; | |
| 187 | body = "<d:propfind xmlns:d=\"DAV:\" " | ||
| 188 | "xmlns:c=\"urn:ietf:params:xml:ns:caldav\" xmlns:cs=\"http://calendarserver.org/ns/\" " | ||
| 189 | 137 | "xmlns:apple=\"http://apple.com/ns/ical/\"" + | |
| 190 |
13/25✓ Branch 45 → 46 taken 136 times.
✓ Branch 45 → 50 taken 1 time.
✓ Branch 46 → 47 taken 136 times.
✓ Branch 47 → 48 taken 136 times.
✓ Branch 48 → 49 taken 136 times.
✓ Branch 49 → 51 taken 136 times.
✓ Branch 51 → 52 taken 137 times.
✓ Branch 61 → 62 taken 136 times.
✓ Branch 61 → 63 taken 1 time.
✓ Branch 63 → 64 taken 136 times.
✓ Branch 63 → 65 taken 1 time.
✓ Branch 65 → 66 taken 136 times.
✓ Branch 65 → 67 taken 1 time.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
✗ Branch 129 → 130 not taken.
✗ Branch 129 → 131 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 133 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 135 → 136 not taken.
✗ Branch 135 → 137 not taken.
✗ Branch 139 → 140 not taken.
✗ Branch 139 → 141 not taken.
|
411 | (push ? QByteArray(" xmlns:P=\"") + DavPushXml::kNamespace.toLatin1() + "\"" : QByteArray()) + |
| 191 | "><d:prop><d:displayname/><d:resourcetype/><cs:getctag/>" | ||
| 192 |
1/1✓ Branch 52 → 53 taken 137 times.
|
274 | "<apple:calendar-color/><c:supported-calendar-component-set/><d:current-user-privilege-set/>" + |
| 193 |
5/5✓ Branch 42 → 43 taken 136 times.
✓ Branch 42 → 44 taken 1 time.
✓ Branch 43 → 45 taken 136 times.
✓ Branch 53 → 54 taken 137 times.
✓ Branch 54 → 55 taken 137 times.
|
411 | (push ? DavPushXml::discoveryProperties() : QByteArray()) + "</d:prop></d:propfind>"; |
| 194 | } | ||
| 195 | |||
| 196 | #ifdef MAILJD_E2E_TESTING | ||
| 197 |
1/1✓ Branch 72 → 73 taken 99 times.
|
99 | reportE2eTransportPhase(QStringLiteral("PROPFIND_DISCOVERY")); |
| 198 | #endif | ||
| 199 |
4/4✓ Branch 69 → 70 taken 56 times.
✓ Branch 70 → 71 taken 56 times.
✓ Branch 75 → 76 taken 99 times.
✓ Branch 76 → 77 taken 99 times.
|
155 | auto *reply = m_nam->sendCustomRequest(request, "PROPFIND", body); |
| 200 |
4/4✓ Branch 72 → 73 taken 56 times.
✓ Branch 73 → 74 taken 56 times.
✓ Branch 78 → 79 taken 99 times.
✓ Branch 79 → 80 taken 99 times.
|
155 | MailJD::Performance::PimPerformanceTracker::instance().davWaitStarted(); |
| 201 |
2/2✓ Branch 74 → 75 taken 56 times.
✓ Branch 80 → 81 taken 99 times.
|
155 | DavNetworkLimits::apply(reply); |
| 202 |
2/2✓ Branch 75 → 76 taken 56 times.
✓ Branch 81 → 82 taken 99 times.
|
155 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 203 |
2/2✓ Branch 76 → 77 taken 56 times.
✓ Branch 82 → 83 taken 99 times.
|
155 | connect(reply, &QNetworkReply::finished, this, [this, reply]() { |
| 204 | 152 | onDiscoverReply(reply); | |
| 205 | 152 | }); | |
| 206 | 155 | } | |
| 207 | |||
| 208 | 35 | void CalDavClient::advanceDiscovery(const QString &reason) { | |
| 209 |
1/2✓ Branch 3 → 4 taken 35 times.
✗ Branch 3 → 5 not taken.
|
35 | if (!reason.isEmpty()) |
| 210 | 35 | m_discoveryError = reason; | |
| 211 | |||
| 212 |
1/1✓ Branch 5 → 6 taken 35 times.
|
35 | QUrl origin(m_serverUrl); |
| 213 |
1/1✓ Branch 9 → 10 taken 35 times.
|
35 | origin.setPath(QStringLiteral("/")); |
| 214 |
1/1✓ Branch 13 → 14 taken 35 times.
|
35 | origin.setQuery(QString()); |
| 215 |
1/1✓ Branch 16 → 17 taken 35 times.
|
35 | origin.setFragment(QString()); |
| 216 | |||
| 217 |
2/2✓ Branch 18 → 19 taken 14 times.
✓ Branch 18 → 33 taken 21 times.
|
35 | if (m_discoveryStep == DiscoveryStep::ConfiguredCollection) { |
| 218 |
3/3✓ Branch 22 → 23 taken 14 times.
✓ Branch 23 → 24 taken 14 times.
✓ Branch 26 → 27 taken 14 times.
|
42 | requestDiscovery(origin.resolved(QUrl(QStringLiteral("/.well-known/caldav"))) |
| 219 |
1/1✓ Branch 25 → 26 taken 14 times.
|
28 | .toString(), |
| 220 | DiscoveryStep::WellKnown); | ||
| 221 | 14 | return; | |
| 222 | } | ||
| 223 | |||
| 224 |
2/2✓ Branch 33 → 34 taken 11 times.
✓ Branch 33 → 59 taken 10 times.
|
21 | if (m_discoveryStep != DiscoveryStep::NextcloudFallback) { |
| 225 | const QString fallback = | ||
| 226 |
1/1✓ Branch 44 → 45 taken 11 times.
|
44 | origin.resolved(QUrl(QStringLiteral("/remote.php/dav/calendars/%1/") |
| 227 |
2/2✓ Branch 41 → 42 taken 11 times.
✓ Branch 42 → 43 taken 11 times.
|
22 | .arg(QString::fromUtf8( |
| 228 |
2/2✓ Branch 40 → 41 taken 11 times.
✓ Branch 43 → 44 taken 11 times.
|
33 | QUrl::toPercentEncoding(m_username))))) |
| 229 |
1/1✓ Branch 46 → 47 taken 11 times.
|
11 | .toString(); |
| 230 |
1/1✓ Branch 56 → 57 taken 11 times.
|
11 | requestDiscovery(fallback, DiscoveryStep::NextcloudFallback); |
| 231 | 11 | return; | |
| 232 | 11 | } | |
| 233 | |||
| 234 |
1/1✓ Branch 65 → 66 taken 10 times.
|
20 | emit syncFailed(m_discoveryError.isEmpty() |
| 235 |
2/6✗ Branch 60 → 61 not taken.
✓ Branch 60 → 64 taken 10 times.
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 69 taken 10 times.
✗ Branch 138 → 139 not taken.
✗ Branch 138 → 140 not taken.
|
20 | ? QStringLiteral("CalDAV discovery failed") |
| 236 | 10 | : m_discoveryError); | |
| 237 | 35 | } | |
| 238 | |||
| 239 | 152 | void CalDavClient::onDiscoverReply(QNetworkReply *reply) { | |
| 240 |
1/1✓ Branch 2 → 3 taken 152 times.
|
152 | reply->deleteLater(); |
| 241 | auto &performanceTracker = | ||
| 242 |
1/1✓ Branch 3 → 4 taken 152 times.
|
152 | MailJD::Performance::PimPerformanceTracker::instance(); |
| 243 |
2/2✓ Branch 4 → 5 taken 152 times.
✓ Branch 5 → 6 taken 152 times.
|
152 | performanceTracker.davWaitFinished(reply->bytesAvailable()); |
| 244 | |||
| 245 |
1/1✓ Branch 6 → 7 taken 152 times.
|
152 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 246 |
2/2✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 22 taken 149 times.
|
152 | if (!limitError.isEmpty()) { |
| 247 |
6/6✓ Branch 9 → 10 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 19 → 11 taken 3 times.
✓ Branch 19 → 20 taken 3 times.
|
6 | qCWarning(lcCalDav) << "PROPFIND failed:" << limitError; |
| 248 |
1/1✓ Branch 20 → 21 taken 3 times.
|
3 | advanceDiscovery(limitError); |
| 249 | 3 | return; | |
| 250 | } | ||
| 251 | |||
| 252 | // RFC 6764: the well-known URI (or a moved principal) redirects. The | ||
| 253 | // PROPFIND is repeated at the target, same origin only, at most 3 times. | ||
| 254 |
2/2✓ Branch 22 → 23 taken 149 times.
✓ Branch 23 → 24 taken 149 times.
|
149 | const int redirectStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 255 |
5/8✓ Branch 25 → 26 taken 147 times.
✓ Branch 25 → 29 taken 2 times.
✓ Branch 26 → 27 taken 147 times.
✗ Branch 26 → 29 not taken.
✓ Branch 27 → 28 taken 147 times.
✗ Branch 27 → 29 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 69 taken 147 times.
|
149 | if (redirectStatus == 301 || redirectStatus == 302 || redirectStatus == 307 || redirectStatus == 308) { |
| 256 |
4/4✓ Branch 33 → 34 taken 2 times.
✓ Branch 34 → 35 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
|
4 | const QString target = resolveDavUrl(QString::fromUtf8(reply->rawHeader("Location")).trimmed()); |
| 257 |
5/6✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 44 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
✗ Branch 42 → 44 not taken.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 45 → 60 taken 1 time.
|
2 | if (!target.isEmpty() && m_discoveryRedirects < 3) { |
| 258 | 1 | ++m_discoveryRedirects; | |
| 259 |
7/7✓ Branch 46 → 47 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 53 → 54 taken 1 time.
✓ Branch 57 → 48 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
|
2 | qCInfo(lcCalDav) << "Discovery redirected (" << redirectStatus << ")"; |
| 260 |
1/1✓ Branch 58 → 59 taken 1 time.
|
1 | requestDiscovery(target, m_discoveryStep); |
| 261 | 1 | return; | |
| 262 | } | ||
| 263 |
1/1✓ Branch 63 → 64 taken 1 time.
|
1 | advanceDiscovery(QStringLiteral("CalDAV discovery redirect refused")); |
| 264 | 1 | return; | |
| 265 | 2 | } | |
| 266 | |||
| 267 | // S96.1.2: a server that refuses the extended property list as a whole | ||
| 268 | // gets the same PROPFIND once more without the push properties. | ||
| 269 | 322 | const bool collectionStep = m_discoveryStep == DiscoveryStep::ConfiguredCollection || | |
| 270 |
4/4✓ Branch 69 → 70 taken 28 times.
✓ Branch 69 → 72 taken 119 times.
✓ Branch 70 → 71 taken 25 times.
✓ Branch 70 → 72 taken 3 times.
|
172 | m_discoveryStep == DiscoveryStep::CalendarHome || |
| 271 |
2/2✓ Branch 71 → 72 taken 10 times.
✓ Branch 71 → 73 taken 15 times.
|
25 | m_discoveryStep == DiscoveryStep::NextcloudFallback; |
| 272 |
5/8✓ Branch 74 → 75 taken 146 times.
✓ Branch 74 → 76 taken 1 time.
✗ Branch 75 → 76 not taken.
✓ Branch 75 → 99 taken 146 times.
✓ Branch 76 → 77 taken 1 time.
✗ Branch 76 → 99 not taken.
✓ Branch 77 → 78 taken 1 time.
✗ Branch 77 → 99 not taken.
|
147 | if ((redirectStatus == 400 || redirectStatus == 422) && collectionStep && !m_pushPropertiesRejected) { |
| 273 |
7/7✓ Branch 78 → 79 taken 1 time.
✓ Branch 82 → 83 taken 1 time.
✓ Branch 83 → 84 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
✓ Branch 85 → 86 taken 1 time.
✓ Branch 89 → 80 taken 1 time.
✓ Branch 89 → 90 taken 1 time.
|
2 | qCInfo(lcCalDav) << "PROPFIND with push properties rejected (" << redirectStatus << "), retrying without"; |
| 274 | 1 | m_pushPropertiesRejected = true; | |
| 275 |
4/4✓ Branch 90 → 91 taken 1 time.
✓ Branch 91 → 92 taken 1 time.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 94 → 95 taken 1 time.
|
1 | requestDiscovery(reply->request().url().toString(), m_discoveryStep); |
| 276 | 1 | return; | |
| 277 | } | ||
| 278 | |||
| 279 |
3/3✓ Branch 99 → 100 taken 146 times.
✓ Branch 100 → 101 taken 16 times.
✓ Branch 100 → 138 taken 130 times.
|
146 | if (reply->error() != QNetworkReply::NoError) { |
| 280 | // A step with a fallback after it failing is the usual way through | ||
| 281 | // discovery (a server root answers 405 before /.well-known/caldav, on | ||
| 282 | // every sync of a Nextcloud account given by its address); only the | ||
| 283 | // last step's failure is a warning, and syncFailed reports it. | ||
| 284 |
2/2✓ Branch 101 → 102 taken 4 times.
✓ Branch 101 → 116 taken 12 times.
|
16 | if (m_discoveryStep == DiscoveryStep::NextcloudFallback) |
| 285 |
7/7✓ Branch 102 → 103 taken 4 times.
✓ Branch 106 → 107 taken 4 times.
✓ Branch 107 → 108 taken 4 times.
✓ Branch 108 → 109 taken 4 times.
✓ Branch 109 → 110 taken 4 times.
✓ Branch 114 → 104 taken 4 times.
✓ Branch 114 → 115 taken 4 times.
|
8 | qCWarning(lcCalDav) << "PROPFIND failed:" << reply->errorString(); |
| 286 | else | ||
| 287 |
8/8✓ Branch 116 → 117 taken 12 times.
✓ Branch 120 → 121 taken 12 times.
✓ Branch 121 → 122 taken 12 times.
✓ Branch 122 → 123 taken 12 times.
✓ Branch 123 → 124 taken 12 times.
✓ Branch 124 → 125 taken 12 times.
✓ Branch 129 → 118 taken 12 times.
✓ Branch 129 → 130 taken 12 times.
|
24 | qCInfo(lcCalDav) << "PROPFIND answered" << redirectStatus << "in discovery step" << int(m_discoveryStep) |
| 288 |
1/1✓ Branch 125 → 126 taken 12 times.
|
12 | << "- trying the next one"; |
| 289 | // A rejected password stays the reported cause even when the | ||
| 290 | // discovery fallbacks fail differently afterwards. | ||
| 291 |
2/2✓ Branch 131 → 132 taken 10 times.
✓ Branch 131 → 134 taken 6 times.
|
16 | if (m_lastNetworkError != QNetworkReply::AuthenticationRequiredError) |
| 292 |
1/1✓ Branch 132 → 133 taken 10 times.
|
10 | m_lastNetworkError = reply->error(); |
| 293 |
2/2✓ Branch 134 → 135 taken 16 times.
✓ Branch 135 → 136 taken 16 times.
|
16 | advanceDiscovery(reply->errorString()); |
| 294 | 16 | return; | |
| 295 | } | ||
| 296 | |||
| 297 |
1/1✓ Branch 138 → 139 taken 130 times.
|
130 | QByteArray data = reply->readAll(); |
| 298 |
7/7✓ Branch 139 → 140 taken 130 times.
✓ Branch 143 → 144 taken 130 times.
✓ Branch 144 → 145 taken 130 times.
✓ Branch 145 → 146 taken 130 times.
✓ Branch 146 → 147 taken 130 times.
✓ Branch 151 → 141 taken 130 times.
✓ Branch 151 → 152 taken 130 times.
|
260 | qCDebug(lcCalDav) << "PROPFIND response:" << data.left(2000); |
| 299 |
1/1✓ Branch 152 → 153 taken 130 times.
|
130 | performanceTracker.parseStarted(); |
| 300 |
1/1✓ Branch 153 → 154 taken 130 times.
|
130 | QDomDocument doc; |
| 301 | // T-511: Check XML parse result | ||
| 302 |
3/3✓ Branch 154 → 155 taken 130 times.
✓ Branch 155 → 156 taken 11 times.
✓ Branch 155 → 175 taken 119 times.
|
130 | if (!DavXmlHelper::parseInto(doc, data)) { |
| 303 |
1/1✓ Branch 157 → 158 taken 11 times.
|
11 | performanceTracker.parseFinished(0, data.size()); |
| 304 |
5/5✓ Branch 158 → 159 taken 11 times.
✓ Branch 162 → 163 taken 11 times.
✓ Branch 163 → 164 taken 11 times.
✓ Branch 167 → 160 taken 11 times.
✓ Branch 167 → 168 taken 11 times.
|
22 | qCWarning(lcCalDav) << "Failed to parse XML response"; |
| 305 |
1/1✓ Branch 171 → 172 taken 11 times.
|
11 | advanceDiscovery(QStringLiteral("Invalid XML response")); |
| 306 | 11 | return; | |
| 307 | } | ||
| 308 | |||
| 309 |
2/2✓ Branch 175 → 176 taken 115 times.
✓ Branch 175 → 177 taken 4 times.
|
119 | if (m_discoveryStep == DiscoveryStep::WellKnown || |
| 310 |
2/2✓ Branch 176 → 177 taken 3 times.
✓ Branch 176 → 245 taken 112 times.
|
115 | m_discoveryStep == DiscoveryStep::Principal) { |
| 311 | const QString property = | ||
| 312 | 7 | m_discoveryStep == DiscoveryStep::WellKnown | |
| 313 | 4 | ? QStringLiteral("current-user-principal") | |
| 314 |
6/6✓ Branch 177 → 178 taken 4 times.
✓ Branch 177 → 181 taken 3 times.
✓ Branch 184 → 185 taken 3 times.
✓ Branch 184 → 186 taken 4 times.
✓ Branch 186 → 187 taken 4 times.
✓ Branch 186 → 188 taken 3 times.
|
14 | : QStringLiteral("calendar-home-set"); |
| 315 |
2/2✓ Branch 188 → 189 taken 7 times.
✓ Branch 189 → 190 taken 7 times.
|
7 | const QString href = hrefOfProperty(doc.documentElement(), property); |
| 316 |
3/3✓ Branch 192 → 193 taken 1 time.
✓ Branch 192 → 194 taken 6 times.
✓ Branch 194 → 195 taken 6 times.
|
7 | const QString resolvedHref = href.isEmpty() ? QString() : resolveDavUrl(href); |
| 317 |
2/2✓ Branch 195 → 196 taken 3 times.
✓ Branch 195 → 223 taken 4 times.
|
7 | if (m_discoveryStep == DiscoveryStep::Principal) { |
| 318 | // Sprint 108 S108.8.4: the outbox URL (free/busy) and the addresses | ||
| 319 | // the server schedules for. | ||
| 320 |
1/1✓ Branch 196 → 197 taken 3 times.
|
3 | const CalDavXml::Scheduling scheduling = CalDavXml::parseScheduling(doc); |
| 321 | 3 | m_serverScheduling = !scheduling.outboxHref.isEmpty(); | |
| 322 |
3/3✓ Branch 200 → 201 taken 2 times.
✓ Branch 200 → 202 taken 1 time.
✓ Branch 202 → 203 taken 1 time.
|
3 | m_scheduleOutboxUrl = scheduling.outboxHref.isEmpty() ? QString() : resolveDavUrl(scheduling.outboxHref); |
| 323 | 3 | m_calendarUserAddresses = scheduling.addresses; | |
| 324 |
7/7✓ Branch 206 → 207 taken 3 times.
✓ Branch 210 → 211 taken 3 times.
✓ Branch 211 → 212 taken 3 times.
✓ Branch 213 → 214 taken 3 times.
✓ Branch 214 → 215 taken 3 times.
✓ Branch 220 → 208 taken 3 times.
✓ Branch 220 → 221 taken 3 times.
|
6 | qCInfo(lcCalDav) << "Server scheduling (RFC 6638):" << *m_serverScheduling << "addresses" |
| 325 |
1/1✓ Branch 216 → 217 taken 3 times.
|
3 | << m_calendarUserAddresses.size(); |
| 326 | 3 | } | |
| 327 |
2/2✓ Branch 224 → 225 taken 1 time.
✓ Branch 224 → 234 taken 6 times.
|
7 | if (resolvedHref.isEmpty()) { |
| 328 |
1/1✓ Branch 226 → 227 taken 1 time.
|
1 | performanceTracker.parseFinished(0, data.size()); |
| 329 |
1/1✓ Branch 230 → 231 taken 1 time.
|
1 | advanceDiscovery(QStringLiteral("CalDAV discovery property missing")); |
| 330 | 1 | return; | |
| 331 | } | ||
| 332 |
1/1✓ Branch 237 → 238 taken 6 times.
|
6 | requestDiscovery(resolvedHref, |
| 333 |
2/2✓ Branch 234 → 235 taken 3 times.
✓ Branch 234 → 236 taken 3 times.
|
6 | m_discoveryStep == DiscoveryStep::WellKnown |
| 334 | ? DiscoveryStep::Principal | ||
| 335 | : DiscoveryStep::CalendarHome); | ||
| 336 |
1/1✓ Branch 239 → 240 taken 6 times.
|
6 | performanceTracker.parseFinished(1, data.size()); |
| 337 | 6 | return; | |
| 338 | 7 | } | |
| 339 | |||
| 340 |
1/1✓ Branch 245 → 246 taken 112 times.
|
112 | QList<CalendarInfo> calendars = CalDavXml::parseCalendars(doc); |
| 341 | // S96.1.2: push offers per href, parsed namespace-exactly. | ||
| 342 | 112 | QString pushError; | |
| 343 |
1/1✓ Branch 246 → 247 taken 112 times.
|
112 | const auto offers = DavPushXml::parseDiscovery(data, &pushError); |
| 344 |
4/4✓ Branch 247 → 248 taken 112 times.
✓ Branch 248 → 249 taken 112 times.
✓ Branch 286 → 250 taken 214 times.
✓ Branch 286 → 287 taken 112 times.
|
326 | for (CalendarInfo &cal : calendars) { |
| 345 |
2/2✓ Branch 251 → 252 taken 3 times.
✓ Branch 251 → 259 taken 211 times.
|
214 | if (m_pushPropertiesRejected) { |
| 346 | 3 | cal.push.capability = DavPushCapability::Unsupported; | |
| 347 | 3 | cal.push.reason = QStringLiteral("server rejects the push discovery properties"); | |
| 348 |
1/2✗ Branch 260 → 261 not taken.
✓ Branch 260 → 262 taken 211 times.
|
211 | } else if (!offers) { |
| 349 | ✗ | cal.push.capability = DavPushCapability::ProbeError; | |
| 350 | ✗ | cal.push.reason = pushError; | |
| 351 | } else { | ||
| 352 |
1/1✓ Branch 263 → 264 taken 211 times.
|
211 | cal.push = offers->value(cal.path.trimmed()); |
| 353 | } | ||
| 354 |
8/8✓ Branch 269 → 270 taken 214 times.
✓ Branch 273 → 274 taken 214 times.
✓ Branch 274 → 275 taken 214 times.
✓ Branch 275 → 276 taken 214 times.
✓ Branch 276 → 277 taken 214 times.
✓ Branch 277 → 278 taken 214 times.
✓ Branch 283 → 271 taken 214 times.
✓ Branch 283 → 284 taken 214 times.
|
428 | qCDebug(lcCalDav) << "Calendar:" << cal.displayName << "at" << cal.path |
| 355 |
2/2✓ Branch 278 → 279 taken 214 times.
✓ Branch 279 → 280 taken 214 times.
|
214 | << "color:" << cal.color; |
| 356 | } | ||
| 357 | |||
| 358 |
2/2✓ Branch 288 → 289 taken 3 times.
✓ Branch 288 → 298 taken 109 times.
|
112 | if (calendars.isEmpty()) { |
| 359 |
1/1✓ Branch 290 → 291 taken 3 times.
|
3 | performanceTracker.parseFinished(0, data.size()); |
| 360 |
1/1✓ Branch 294 → 295 taken 3 times.
|
3 | advanceDiscovery(QStringLiteral("No calendars found at discovery URL")); |
| 361 | 3 | return; | |
| 362 | } | ||
| 363 |
1/1✓ Branch 300 → 301 taken 109 times.
|
109 | performanceTracker.parseFinished(calendars.size(), data.size()); |
| 364 |
7/7✓ Branch 301 → 302 taken 109 times.
✓ Branch 305 → 306 taken 109 times.
✓ Branch 306 → 307 taken 109 times.
✓ Branch 308 → 309 taken 109 times.
✓ Branch 309 → 310 taken 109 times.
✓ Branch 313 → 303 taken 109 times.
✓ Branch 313 → 314 taken 109 times.
|
218 | qCInfo(lcCalDav) << "Discovered" << calendars.size() << "calendars"; |
| 365 |
1/1✓ Branch 314 → 315 taken 109 times.
|
109 | emit calendarsDiscovered(calendars); |
| 366 | 203 | } | |
| 367 | |||
| 368 | // ═══════════════════════════════════════════════════════ | ||
| 369 | // Event Sync (REPORT calendar-query VEVENT) | ||
| 370 | // ═══════════════════════════════════════════════════════ | ||
| 371 | |||
| 372 | 30 | void CalDavClient::syncCalendar(const QString &calendarPath) { | |
| 373 |
1/1✓ Branch 2 → 3 taken 30 times.
|
30 | QString url = resolveDavUrl(calendarPath); |
| 374 |
2/2✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 12 taken 24 times.
|
30 | if (url.isEmpty()) { |
| 375 |
1/1✓ Branch 8 → 9 taken 6 times.
|
6 | emit syncFailed(QStringLiteral("Cross-origin URL rejected")); |
| 376 | 6 | return; | |
| 377 | } | ||
| 378 | |||
| 379 |
2/2✓ Branch 12 → 13 taken 24 times.
✓ Branch 13 → 14 taken 24 times.
|
24 | QNetworkRequest request{QUrl(url)}; |
| 380 |
3/3✓ Branch 15 → 16 taken 24 times.
✓ Branch 16 → 17 taken 24 times.
✓ Branch 17 → 18 taken 24 times.
|
24 | request.setRawHeader("Authorization", authHeader()); |
| 381 |
3/3✓ Branch 20 → 21 taken 24 times.
✓ Branch 21 → 22 taken 24 times.
✓ Branch 22 → 23 taken 24 times.
|
24 | request.setRawHeader("Depth", "1"); |
| 382 |
3/3✓ Branch 25 → 26 taken 24 times.
✓ Branch 26 → 27 taken 24 times.
✓ Branch 27 → 28 taken 24 times.
|
24 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 383 | |||
| 384 | QByteArray body = | ||
| 385 | "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" | ||
| 386 | "<c:calendar-query xmlns:d=\"DAV:\" " | ||
| 387 | " xmlns:c=\"urn:ietf:params:xml:ns:caldav\">" | ||
| 388 | " <d:prop>" | ||
| 389 | " <d:getetag/>" | ||
| 390 | " <c:calendar-data/>" | ||
| 391 | " </d:prop>" | ||
| 392 | " <c:filter>" | ||
| 393 | " <c:comp-filter name=\"VCALENDAR\">" | ||
| 394 | " <c:comp-filter name=\"VEVENT\"/>" | ||
| 395 | " </c:comp-filter>" | ||
| 396 | " </c:filter>" | ||
| 397 |
1/1✓ Branch 30 → 31 taken 24 times.
|
24 | "</c:calendar-query>"; |
| 398 | |||
| 399 | #ifdef MAILJD_E2E_TESTING | ||
| 400 |
1/1✓ Branch 34 → 35 taken 17 times.
|
17 | reportE2eTransportPhase(QStringLiteral("REPORT_EVENTS")); |
| 401 | #endif | ||
| 402 |
4/4✓ Branch 31 → 32 taken 7 times.
✓ Branch 32 → 33 taken 7 times.
✓ Branch 37 → 38 taken 17 times.
✓ Branch 38 → 39 taken 17 times.
|
24 | auto *reply = m_nam->sendCustomRequest(request, "REPORT", body); |
| 403 |
4/4✓ Branch 34 → 35 taken 7 times.
✓ Branch 35 → 36 taken 7 times.
✓ Branch 40 → 41 taken 17 times.
✓ Branch 41 → 42 taken 17 times.
|
24 | MailJD::Performance::PimPerformanceTracker::instance().davWaitStarted(); |
| 404 |
2/2✓ Branch 36 → 37 taken 7 times.
✓ Branch 42 → 43 taken 17 times.
|
24 | DavNetworkLimits::apply(reply); |
| 405 |
2/2✓ Branch 37 → 38 taken 7 times.
✓ Branch 43 → 44 taken 17 times.
|
24 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 406 |
2/2✓ Branch 39 → 40 taken 7 times.
✓ Branch 45 → 46 taken 17 times.
|
24 | connect(reply, &QNetworkReply::finished, this, |
| 407 | 48 | [this, reply, calendarPath]() { | |
| 408 | 24 | onSyncEventsReply(reply, calendarPath); | |
| 409 | 24 | }); | |
| 410 | 30 | } | |
| 411 | |||
| 412 | 24 | void CalDavClient::onSyncEventsReply(QNetworkReply *reply, | |
| 413 | const QString &calendarPath) { | ||
| 414 |
1/1✓ Branch 2 → 3 taken 24 times.
|
24 | reply->deleteLater(); |
| 415 | auto &performanceTracker = | ||
| 416 |
1/1✓ Branch 3 → 4 taken 24 times.
|
24 | MailJD::Performance::PimPerformanceTracker::instance(); |
| 417 |
2/2✓ Branch 4 → 5 taken 24 times.
✓ Branch 5 → 6 taken 24 times.
|
24 | performanceTracker.davWaitFinished(reply->bytesAvailable()); |
| 418 | |||
| 419 |
1/1✓ Branch 6 → 7 taken 24 times.
|
24 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 420 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 22 taken 23 times.
|
24 | if (!limitError.isEmpty()) { |
| 421 |
6/6✓ 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 19 → 11 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
|
2 | qCWarning(lcCalDav) << "REPORT (events) failed:" << limitError; |
| 422 |
1/1✓ Branch 20 → 21 taken 1 time.
|
1 | emit syncFailed(limitError); |
| 423 | 1 | return; | |
| 424 | } | ||
| 425 | |||
| 426 |
3/3✓ Branch 22 → 23 taken 23 times.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 42 taken 22 times.
|
23 | if (reply->error() != QNetworkReply::NoError) { |
| 427 |
5/5✓ Branch 24 → 25 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 36 → 26 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
|
2 | qCWarning(lcCalDav) << "REPORT (events) failed:" |
| 428 |
2/2✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
|
1 | << reply->errorString(); |
| 429 |
1/1✓ Branch 37 → 38 taken 1 time.
|
1 | m_lastNetworkError = reply->error(); |
| 430 |
2/2✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
|
1 | emit syncFailed(reply->errorString()); |
| 431 | 1 | return; | |
| 432 | } | ||
| 433 | |||
| 434 |
1/1✓ Branch 42 → 43 taken 22 times.
|
22 | QByteArray data = reply->readAll(); |
| 435 |
6/6✓ Branch 43 → 44 taken 22 times.
✓ Branch 47 → 48 taken 22 times.
✓ Branch 48 → 49 taken 22 times.
✓ Branch 50 → 51 taken 22 times.
✓ Branch 55 → 45 taken 22 times.
✓ Branch 55 → 56 taken 22 times.
|
44 | qCDebug(lcCalDav) << "REPORT (events) response:" << data.size() |
| 436 |
1/1✓ Branch 51 → 52 taken 22 times.
|
22 | << "bytes"; |
| 437 | |||
| 438 | 22 | QList<CalendarEvent> events; | |
| 439 | 22 | QString parseError; | |
| 440 |
1/1✓ Branch 56 → 57 taken 22 times.
|
22 | performanceTracker.parseStarted(); |
| 441 |
3/3✓ Branch 57 → 58 taken 22 times.
✓ Branch 58 → 59 taken 3 times.
✓ Branch 58 → 74 taken 19 times.
|
22 | if (!parseICalEvents(data, &events, &parseError)) { |
| 442 |
1/1✓ Branch 60 → 61 taken 3 times.
|
3 | performanceTracker.parseFinished(0, data.size()); |
| 443 |
6/6✓ Branch 61 → 62 taken 3 times.
✓ Branch 65 → 66 taken 3 times.
✓ Branch 66 → 67 taken 3 times.
✓ Branch 67 → 68 taken 3 times.
✓ Branch 71 → 63 taken 3 times.
✓ Branch 71 → 72 taken 3 times.
|
6 | qCWarning(lcCalDav) << "REPORT (events) parse failed:" << parseError; |
| 444 |
1/1✓ Branch 72 → 73 taken 3 times.
|
3 | emit syncFailed(parseError); |
| 445 | 3 | return; | |
| 446 | } | ||
| 447 |
1/1✓ Branch 76 → 77 taken 19 times.
|
19 | performanceTracker.parseFinished(events.size(), data.size()); |
| 448 | |||
| 449 | // Assign calendarPath to each event | ||
| 450 |
4/4✓ Branch 77 → 78 taken 19 times.
✓ Branch 78 → 79 taken 19 times.
✓ Branch 103 → 80 taken 19 times.
✓ Branch 103 → 104 taken 18 times.
|
37 | for (auto &ev : events) { |
| 451 | 19 | ev.calendarPath = calendarPath; | |
| 452 |
2/2✓ Branch 83 → 84 taken 18 times.
✓ Branch 83 → 101 taken 1 time.
|
19 | if (!ev.resourceHref.isEmpty()) { |
| 453 |
1/1✓ Branch 84 → 85 taken 18 times.
|
18 | const QString resolvedHref = resolveDavUrl(ev.resourceHref); |
| 454 |
2/2✓ Branch 86 → 87 taken 1 time.
✓ Branch 86 → 94 taken 17 times.
|
18 | if (resolvedHref.isEmpty()) { |
| 455 |
1/1✓ Branch 90 → 91 taken 1 time.
|
1 | emit syncFailed(QStringLiteral("Cross-origin resource href rejected")); |
| 456 | 1 | return; | |
| 457 | } | ||
| 458 | 17 | ev.resourceHref = resolvedHref; | |
| 459 | 18 | } | |
| 460 | } | ||
| 461 | |||
| 462 |
1/1✓ Branch 104 → 105 taken 18 times.
|
18 | const bool complete = queryComplete(data, calendarPath); |
| 463 |
7/7✓ Branch 105 → 106 taken 18 times.
✓ Branch 109 → 110 taken 18 times.
✓ Branch 110 → 111 taken 18 times.
✓ Branch 112 → 113 taken 18 times.
✓ Branch 113 → 114 taken 18 times.
✓ Branch 122 → 107 taken 18 times.
✓ Branch 122 → 123 taken 18 times.
|
36 | qCInfo(lcCalDav) << "Synced" << events.size() << "events from" |
| 464 |
4/4✓ Branch 114 → 115 taken 18 times.
✓ Branch 115 → 116 taken 14 times.
✓ Branch 115 → 117 taken 4 times.
✓ Branch 118 → 119 taken 18 times.
|
18 | << calendarPath << (complete ? "complete" : "incomplete"); |
| 465 |
1/1✓ Branch 123 → 124 taken 18 times.
|
18 | emit eventsSynced(calendarPath, events, complete); |
| 466 | 36 | } | |
| 467 | |||
| 468 | // ═══════════════════════════════════════════════════════ | ||
| 469 | // Incremental sync (REPORT sync-collection, RFC 6578) — Sprint 93 S93.6.2 | ||
| 470 | // ═══════════════════════════════════════════════════════ | ||
| 471 | |||
| 472 | |||
| 473 | 193 | void CalDavClient::syncCollection(const QString &calendarPath, const QString &syncToken) { | |
| 474 |
1/1✓ Branch 2 → 3 taken 193 times.
|
193 | const QString url = resolveDavUrl(calendarPath); |
| 475 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 193 times.
|
193 | if (url.isEmpty()) { |
| 476 | ✗ | emit syncFailed(QStringLiteral("Cross-origin URL rejected")); | |
| 477 | ✗ | return; | |
| 478 | } | ||
| 479 |
2/2✓ Branch 12 → 13 taken 193 times.
✓ Branch 13 → 14 taken 193 times.
|
193 | QNetworkRequest request{QUrl(url)}; |
| 480 |
3/3✓ Branch 15 → 16 taken 193 times.
✓ Branch 16 → 17 taken 193 times.
✓ Branch 17 → 18 taken 193 times.
|
193 | request.setRawHeader("Authorization", authHeader()); |
| 481 |
3/3✓ Branch 20 → 21 taken 193 times.
✓ Branch 21 → 22 taken 193 times.
✓ Branch 22 → 23 taken 193 times.
|
193 | request.setRawHeader("Depth", "0"); |
| 482 |
3/3✓ Branch 25 → 26 taken 193 times.
✓ Branch 26 → 27 taken 193 times.
✓ Branch 27 → 28 taken 193 times.
|
193 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 483 |
1/1✓ Branch 30 → 31 taken 193 times.
|
193 | QString escapedToken = syncToken.toHtmlEscaped(); |
| 484 | const QByteArray body = | ||
| 485 | "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" | ||
| 486 | "<d:sync-collection xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">" | ||
| 487 |
2/2✓ Branch 31 → 32 taken 193 times.
✓ Branch 32 → 33 taken 193 times.
|
386 | "<d:sync-token>" + escapedToken.toUtf8() + "</d:sync-token>" |
| 488 | "<d:sync-level>1</d:sync-level>" | ||
| 489 | "<d:prop><d:getetag/><c:calendar-data/></d:prop>" | ||
| 490 |
1/1✓ Branch 33 → 34 taken 193 times.
|
193 | "</d:sync-collection>"; |
| 491 | #ifdef MAILJD_E2E_TESTING | ||
| 492 |
1/1✓ Branch 39 → 40 taken 175 times.
|
175 | reportE2eTransportPhase(QStringLiteral("REPORT_SYNC_COLLECTION")); |
| 493 | #endif | ||
| 494 |
4/4✓ Branch 36 → 37 taken 18 times.
✓ Branch 37 → 38 taken 18 times.
✓ Branch 42 → 43 taken 175 times.
✓ Branch 43 → 44 taken 175 times.
|
193 | auto *reply = m_nam->sendCustomRequest(request, "REPORT", body); |
| 495 |
4/4✓ Branch 39 → 40 taken 18 times.
✓ Branch 40 → 41 taken 18 times.
✓ Branch 45 → 46 taken 175 times.
✓ Branch 46 → 47 taken 175 times.
|
193 | MailJD::Performance::PimPerformanceTracker::instance().davWaitStarted(); |
| 496 |
2/2✓ Branch 41 → 42 taken 18 times.
✓ Branch 47 → 48 taken 175 times.
|
193 | DavNetworkLimits::apply(reply); |
| 497 |
2/2✓ Branch 42 → 43 taken 18 times.
✓ Branch 48 → 49 taken 175 times.
|
193 | trackReply(reply); |
| 498 |
2/6✓ Branch 45 → 46 taken 18 times.
✓ Branch 51 → 52 taken 175 times.
✗ Branch 96 → 97 not taken.
✗ Branch 96 → 98 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
|
193 | connect(reply, &QNetworkReply::finished, this, [this, reply, calendarPath, syncToken]() { |
| 499 | 192 | onSyncCollectionReply(reply, calendarPath, syncToken); | |
| 500 | 192 | }); | |
| 501 | 193 | } | |
| 502 | |||
| 503 | // S96.3.0: a calendar-query result is complete when the server did not | ||
| 504 | // truncate it (507) and every member carried its calendar-data. | ||
| 505 | 27 | bool CalDavClient::queryComplete(const QByteArray &xml, const QString &calendarPath) const { | |
| 506 | 27 | MultistatusSummary summary; | |
| 507 |
6/7✓ Branch 2 → 3 taken 27 times.
✓ Branch 3 → 4 taken 27 times.
✗ Branch 3 → 9 not taken.
✓ Branch 4 → 5 taken 25 times.
✓ Branch 4 → 9 taken 2 times.
✓ Branch 7 → 8 taken 23 times.
✓ Branch 7 → 9 taken 2 times.
|
52 | return summarizeMultistatus(xml, &summary, true, calendarPath) && !summary.truncated && |
| 508 |
3/5✓ Branch 5 → 6 taken 25 times.
✓ Branch 10 → 11 taken 25 times.
✓ Branch 10 → 12 taken 2 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
|
79 | failedMembers(summary.failed, calendarPath).isEmpty(); |
| 509 | 27 | } | |
| 510 | |||
| 511 | // Failed members of a multistatus, resolved; the collection itself (some | ||
| 512 | // servers list it beside its members) is no failed member. | ||
| 513 | 267 | QStringList CalDavClient::failedMembers(const QStringList &hrefs, const QString &calendarPath) const { | |
| 514 | 267 | QStringList failed; | |
| 515 |
1/1✓ Branch 2 → 3 taken 267 times.
|
267 | const QString collection = resolveDavUrl(calendarPath); |
| 516 |
2/2✓ Branch 33 → 5 taken 577 times.
✓ Branch 33 → 34 taken 267 times.
|
844 | for (const QString &href : hrefs) { |
| 517 |
1/1✓ Branch 6 → 7 taken 577 times.
|
577 | const QString resolved = resolveDavUrl(href); |
| 518 |
5/11✓ Branch 8 → 9 taken 577 times.
✗ Branch 8 → 14 not taken.
✓ Branch 11 → 12 taken 577 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 577 times.
✓ Branch 16 → 17 taken 577 times.
✗ Branch 16 → 18 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 577 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
|
577 | if (resolved == collection || (resolved + QLatin1Char('/')) == collection) continue; |
| 519 |
2/3✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 577 times.
✓ Branch 24 → 25 taken 577 times.
|
577 | failed << (resolved.isEmpty() ? href : resolved); |
| 520 | 577 | } | |
| 521 | 267 | return failed; | |
| 522 | 267 | } | |
| 523 | |||
| 524 | // S106-704: dropping a member the reply left out let the enumeration prune | ||
| 525 | // delete the local copy while the sync token committed. | ||
| 526 | 13 | QStringList CalDavClient::unansweredMembers(const QStringList &requested, const QStringList &listed, | |
| 527 | const QStringList &failed, const QStringList &removed, | ||
| 528 | const QString &calendarPath) const { | ||
| 529 | 13 | QSet<QString> answered; | |
| 530 |
2/2✓ Branch 16 → 5 taken 39 times.
✓ Branch 16 → 17 taken 13 times.
|
52 | for (const QStringList *hrefs : {&listed, &failed, &removed}) |
| 531 |
6/6✓ Branch 5 → 6 taken 39 times.
✓ Branch 6 → 7 taken 39 times.
✓ Branch 7 → 8 taken 39 times.
✓ Branch 10 → 11 taken 131 times.
✓ Branch 13 → 9 taken 131 times.
✓ Branch 13 → 14 taken 39 times.
|
170 | for (const QString &href : failedMembers(*hrefs, calendarPath)) answered.insert(href); |
| 532 | 13 | QStringList unanswered; | |
| 533 |
5/5✓ Branch 17 → 18 taken 13 times.
✓ Branch 18 → 19 taken 13 times.
✓ Branch 19 → 20 taken 13 times.
✓ Branch 27 → 21 taken 282 times.
✓ Branch 27 → 28 taken 13 times.
|
295 | for (const QString &href : failedMembers(requested, calendarPath)) |
| 534 |
3/3✓ Branch 23 → 24 taken 151 times.
✓ Branch 23 → 25 taken 131 times.
✓ Branch 24 → 25 taken 151 times.
|
295 | if (!answered.contains(href)) unanswered << href; |
| 535 |
2/2✓ Branch 30 → 31 taken 3 times.
✓ Branch 30 → 46 taken 10 times.
|
13 | if (!unanswered.isEmpty()) |
| 536 |
6/6✓ Branch 31 → 32 taken 3 times.
✓ Branch 35 → 36 taken 3 times.
✓ Branch 36 → 37 taken 3 times.
✓ Branch 38 → 39 taken 3 times.
✓ Branch 44 → 33 taken 3 times.
✓ Branch 44 → 45 taken 3 times.
|
6 | qCWarning(lcCalDav) << "calendar-multiget left" << unanswered.size() |
| 537 |
2/2✓ Branch 39 → 40 taken 3 times.
✓ Branch 40 → 41 taken 3 times.
|
3 | << "requested member(s) unanswered in" << calendarPath; |
| 538 | 13 | return unanswered; | |
| 539 | 13 | } | |
| 540 | |||
| 541 | 192 | void CalDavClient::onSyncCollectionReply(QNetworkReply *reply, const QString &calendarPath, | |
| 542 | const QString &requestToken) { | ||
| 543 | 192 | const bool full = requestToken.isEmpty(); | |
| 544 |
1/1✓ Branch 3 → 4 taken 192 times.
|
192 | reply->deleteLater(); |
| 545 |
1/1✓ Branch 4 → 5 taken 192 times.
|
192 | auto &performanceTracker = MailJD::Performance::PimPerformanceTracker::instance(); |
| 546 |
2/2✓ Branch 5 → 6 taken 192 times.
✓ Branch 6 → 7 taken 192 times.
|
192 | performanceTracker.davWaitFinished(reply->bytesAvailable()); |
| 547 |
1/1✓ Branch 7 → 8 taken 192 times.
|
192 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 548 |
2/2✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 44 taken 185 times.
|
192 | if (!limitError.isEmpty()) { |
| 549 | // F-54 (SPRINT-104): a token-less full download that breaks the | ||
| 550 | // response bounds (10 MiB / 30 s) is retried as an ETag-only listing | ||
| 551 | // with batched calendar-multiget — every response stays bounded. The | ||
| 552 | // listing itself failing the bounds (≈85k members) keeps the failure. | ||
| 553 |
5/6✓ Branch 10 → 11 taken 6 times.
✓ Branch 10 → 14 taken 1 time.
✓ Branch 12 → 13 taken 6 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 6 times.
✓ Branch 15 → 31 taken 1 time.
|
7 | if (full && !m_listingFallback.contains(calendarPath)) { |
| 554 |
6/6✓ Branch 16 → 17 taken 6 times.
✓ Branch 20 → 21 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
✓ Branch 27 → 18 taken 6 times.
✓ Branch 27 → 28 taken 6 times.
|
12 | qCInfo(lcCalDav) << "full sync-collection exceeded the bounds for" << calendarPath |
| 555 |
1/1✓ Branch 23 → 24 taken 6 times.
|
6 | << "— retrying as an ETag listing with batched multiget"; |
| 556 |
1/1✓ Branch 28 → 29 taken 6 times.
|
6 | m_listingFallback.insert(calendarPath); |
| 557 |
1/1✓ Branch 29 → 30 taken 6 times.
|
6 | requestEtagListing(calendarPath); |
| 558 | 6 | return; | |
| 559 | } | ||
| 560 |
6/6✓ Branch 31 → 32 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 41 → 33 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
|
2 | qCWarning(lcCalDav) << "REPORT (sync-collection) failed:" << limitError; |
| 561 |
1/1✓ Branch 42 → 43 taken 1 time.
|
1 | emit syncFailed(limitError); |
| 562 | 1 | return; | |
| 563 | } | ||
| 564 |
2/2✓ Branch 44 → 45 taken 185 times.
✓ Branch 45 → 46 taken 185 times.
|
185 | const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 565 |
3/3✓ Branch 47 → 48 taken 185 times.
✓ Branch 48 → 49 taken 13 times.
✓ Branch 48 → 123 taken 172 times.
|
185 | if (reply->error() != QNetworkReply::NoError) { |
| 566 | // RFC 6578 §3.2: an expired token is a 403/409 valid-sync-token | ||
| 567 | // precondition; 400/404/405/415/501 mean the report is not supported. | ||
| 568 | // Sprint 96 S96.3.3: a 403 alone is no proof of a missing report — only | ||
| 569 | // the supported-report precondition is; a privilege error (or a 403 | ||
| 570 | // without a known precondition) fails this collection instead of | ||
| 571 | // switching it to calendar-query. | ||
| 572 |
1/1✓ Branch 49 → 50 taken 13 times.
|
13 | const QByteArray error = reply->readAll(); |
| 573 | // S106-049: 410 Gone and 412 Precondition Failed are how deployed servers | ||
| 574 | // invalidate a token too; they used to fail every later run with the | ||
| 575 | // same token. | ||
| 576 |
11/12✓ Branch 50 → 51 taken 11 times.
✓ Branch 50 → 59 taken 2 times.
✓ Branch 51 → 52 taken 5 times.
✓ Branch 51 → 53 taken 6 times.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 56 taken 5 times.
✓ Branch 55 → 56 taken 2 times.
✓ Branch 55 → 58 taken 4 times.
✓ Branch 56 → 57 taken 6 times.
✓ Branch 56 → 58 taken 1 time.
✓ Branch 60 → 61 taken 6 times.
✓ Branch 60 → 74 taken 7 times.
|
19 | if (!full && (((status == 403 || status == 409) && error.contains("valid-sync-token")) || |
| 577 |
2/2✓ Branch 57 → 58 taken 1 time.
✓ Branch 57 → 59 taken 5 times.
|
6 | status == 410 || status == 412)) { |
| 578 |
6/6✓ Branch 61 → 62 taken 6 times.
✓ Branch 65 → 66 taken 6 times.
✓ Branch 66 → 67 taken 6 times.
✓ Branch 67 → 68 taken 6 times.
✓ Branch 71 → 63 taken 6 times.
✓ Branch 71 → 72 taken 6 times.
|
12 | qCInfo(lcCalDav) << "sync-token no longer valid for" << calendarPath; |
| 579 |
1/1✓ Branch 72 → 73 taken 6 times.
|
6 | emit collectionSyncRejected(calendarPath, true); |
| 580 | 6 | return; | |
| 581 | } | ||
| 582 |
4/4✓ Branch 74 → 75 taken 3 times.
✓ Branch 74 → 79 taken 4 times.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 79 taken 2 times.
|
7 | const bool unsupported403 = status == 403 && error.contains("supported-report"); |
| 583 |
8/12✓ Branch 80 → 81 taken 7 times.
✗ Branch 80 → 87 not taken.
✓ Branch 81 → 82 taken 6 times.
✓ Branch 81 → 87 taken 1 time.
✓ Branch 82 → 83 taken 6 times.
✗ Branch 82 → 87 not taken.
✓ Branch 83 → 84 taken 6 times.
✗ Branch 83 → 87 not taken.
✓ Branch 84 → 85 taken 6 times.
✗ Branch 84 → 87 not taken.
✓ Branch 85 → 86 taken 4 times.
✓ Branch 85 → 87 taken 2 times.
|
7 | if (status == 400 || unsupported403 || status == 404 || status == 405 || status == 415 || |
| 584 |
1/2✗ Branch 86 → 87 not taken.
✓ Branch 86 → 102 taken 4 times.
|
4 | status == 501 || status == 409) { |
| 585 |
8/8✓ Branch 87 → 88 taken 3 times.
✓ Branch 91 → 92 taken 3 times.
✓ Branch 92 → 93 taken 3 times.
✓ Branch 93 → 94 taken 3 times.
✓ Branch 94 → 95 taken 3 times.
✓ Branch 95 → 96 taken 3 times.
✓ Branch 99 → 89 taken 3 times.
✓ Branch 99 → 100 taken 3 times.
|
6 | qCInfo(lcCalDav) << "sync-collection not supported for" << calendarPath << "status" << status; |
| 586 |
1/1✓ Branch 100 → 101 taken 3 times.
|
3 | emit collectionSyncRejected(calendarPath, false); |
| 587 | 3 | return; | |
| 588 | } | ||
| 589 |
8/8✓ Branch 102 → 103 taken 4 times.
✓ Branch 106 → 107 taken 4 times.
✓ Branch 107 → 108 taken 4 times.
✓ Branch 108 → 109 taken 4 times.
✓ Branch 109 → 110 taken 4 times.
✓ Branch 110 → 111 taken 4 times.
✓ Branch 115 → 104 taken 4 times.
✓ Branch 115 → 116 taken 4 times.
|
8 | qCWarning(lcCalDav) << "REPORT (sync-collection) failed:" << status << reply->errorString(); |
| 590 |
1/1✓ Branch 116 → 117 taken 4 times.
|
4 | m_lastNetworkError = reply->error(); |
| 591 |
2/2✓ Branch 117 → 118 taken 4 times.
✓ Branch 118 → 119 taken 4 times.
|
4 | emit syncFailed(reply->errorString()); |
| 592 | 4 | return; | |
| 593 | 13 | } | |
| 594 |
1/1✓ Branch 123 → 124 taken 172 times.
|
172 | const QByteArray data = reply->readAll(); |
| 595 | 172 | DavCollectionChanges changes; | |
| 596 | 172 | changes.full = full; | |
| 597 | 172 | changes.requestToken = requestToken; | |
| 598 | 172 | QString parseError; | |
| 599 | 172 | MultistatusSummary summary; | |
| 600 |
1/1✓ Branch 125 → 126 taken 172 times.
|
172 | performanceTracker.parseStarted(); |
| 601 |
1/1✓ Branch 126 → 127 taken 172 times.
|
172 | if (!summarizeMultistatus(data, &summary, true, calendarPath) || |
| 602 |
4/7✓ Branch 127 → 128 taken 172 times.
✗ Branch 127 → 132 not taken.
✓ Branch 128 → 129 taken 172 times.
✓ Branch 129 → 130 taken 172 times.
✗ Branch 129 → 132 not taken.
✗ Branch 134 → 135 not taken.
✓ Branch 134 → 159 taken 172 times.
|
344 | !parseICalEvents(data, &changes.events, &parseError) || |
| 603 |
2/3✓ Branch 130 → 131 taken 172 times.
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 133 taken 172 times.
|
172 | !parseICalTasks(data, &changes.tasks, &parseError)) { |
| 604 | ✗ | performanceTracker.parseFinished(0, data.size()); | |
| 605 | ✗ | qCWarning(lcCalDav) << "REPORT (sync-collection) parse failed:" << parseError; | |
| 606 | ✗ | emit syncFailed(parseError.isEmpty() ? QStringLiteral("Invalid XML response") : parseError); | |
| 607 | ✗ | return; | |
| 608 | } | ||
| 609 |
1/1✓ Branch 162 → 163 taken 172 times.
|
172 | performanceTracker.parseFinished(changes.events.size() + changes.tasks.size(), data.size()); |
| 610 | 172 | changes.syncToken = summary.token; | |
| 611 | 172 | changes.removedHrefs = summary.removed; | |
| 612 | 172 | changes.truncated = summary.truncated; | |
| 613 |
1/2✗ Branch 166 → 167 not taken.
✓ Branch 166 → 169 taken 172 times.
|
172 | if (changes.syncToken.isEmpty()) { |
| 614 | // Without a new token the result cannot be applied incrementally. | ||
| 615 | ✗ | emit collectionSyncRejected(calendarPath, false); | |
| 616 | ✗ | return; | |
| 617 | } | ||
| 618 | 344 | const auto resolveAll = [this](auto &items) { | |
| 619 |
8/8auto CalDavClient::onSyncCollectionReply(QNetworkReply*, QString const&, QString const&)::{lambda(auto:1&)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&) const:
✓ Branch 2 → 3 taken 172 times.
✓ Branch 3 → 4 taken 172 times.
✓ Branch 17 → 5 taken 7 times.
✓ Branch 17 → 18 taken 172 times.
auto CalDavClient::onSyncCollectionReply(QNetworkReply*, QString const&, QString const&)::{lambda(auto:1&)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&) const:
✓ Branch 2 → 3 taken 172 times.
✓ Branch 3 → 4 taken 172 times.
✓ Branch 17 → 5 taken 440 times.
✓ Branch 17 → 18 taken 172 times.
|
1135 | for (auto &item : items) { |
| 620 |
2/4auto CalDavClient::onSyncCollectionReply(QNetworkReply*, QString const&, QString const&)::{lambda(auto:1&)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&) const:
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 7 times.
auto CalDavClient::onSyncCollectionReply(QNetworkReply*, QString const&, QString const&)::{lambda(auto:1&)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&) const:
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 440 times.
|
447 | if (item.resourceHref.isEmpty()) continue; |
| 621 |
2/2auto CalDavClient::onSyncCollectionReply(QNetworkReply*, QString const&, QString const&)::{lambda(auto:1&)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&) const:
✓ Branch 9 → 10 taken 7 times.
auto CalDavClient::onSyncCollectionReply(QNetworkReply*, QString const&, QString const&)::{lambda(auto:1&)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&) const:
✓ Branch 9 → 10 taken 440 times.
|
447 | item.resourceHref = resolveDavUrl(item.resourceHref); |
| 622 |
2/4auto CalDavClient::onSyncCollectionReply(QNetworkReply*, QString const&, QString const&)::{lambda(auto:1&)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&) const:
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 7 times.
auto CalDavClient::onSyncCollectionReply(QNetworkReply*, QString const&, QString const&)::{lambda(auto:1&)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&) const:
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 440 times.
|
447 | if (item.resourceHref.isEmpty()) return false; |
| 623 | } | ||
| 624 | 344 | return true; | |
| 625 | 172 | }; | |
| 626 |
4/4✓ Branch 169 → 170 taken 172 times.
✓ Branch 170 → 171 taken 172 times.
✓ Branch 176 → 172 taken 440 times.
✓ Branch 176 → 177 taken 172 times.
|
612 | for (auto &event : changes.events) event.calendarPath = calendarPath; |
| 627 |
4/4✓ Branch 177 → 178 taken 172 times.
✓ Branch 178 → 179 taken 172 times.
✓ Branch 184 → 180 taken 7 times.
✓ Branch 184 → 185 taken 172 times.
|
179 | for (auto &task : changes.tasks) task.calendarPath = calendarPath; |
| 628 |
4/6✓ Branch 185 → 186 taken 172 times.
✓ Branch 186 → 187 taken 172 times.
✗ Branch 186 → 190 not taken.
✓ Branch 187 → 188 taken 172 times.
✓ Branch 188 → 189 taken 172 times.
✗ Branch 188 → 190 not taken.
|
172 | bool hrefsOk = resolveAll(changes.events) && resolveAll(changes.tasks); |
| 629 |
4/4✓ Branch 191 → 192 taken 172 times.
✓ Branch 192 → 193 taken 172 times.
✓ Branch 205 → 194 taken 7 times.
✓ Branch 205 → 206 taken 172 times.
|
179 | for (auto &href : changes.removedHrefs) { |
| 630 |
1/1✓ Branch 195 → 196 taken 7 times.
|
7 | href = resolveDavUrl(href); |
| 631 |
2/4✓ Branch 198 → 199 taken 7 times.
✗ Branch 198 → 202 not taken.
✓ Branch 200 → 201 taken 7 times.
✗ Branch 200 → 202 not taken.
|
7 | hrefsOk = hrefsOk && !href.isEmpty(); |
| 632 | } | ||
| 633 |
1/2✗ Branch 206 → 207 not taken.
✓ Branch 206 → 214 taken 172 times.
|
172 | if (!hrefsOk) { |
| 634 | ✗ | emit syncFailed(QStringLiteral("Cross-origin resource href rejected")); | |
| 635 | ✗ | return; | |
| 636 | } | ||
| 637 |
1/1✓ Branch 214 → 215 taken 172 times.
|
172 | changes.failedHrefs = failedMembers(summary.failed, calendarPath); |
| 638 |
9/9✓ Branch 217 → 218 taken 172 times.
✓ Branch 221 → 222 taken 172 times.
✓ Branch 222 → 223 taken 172 times.
✓ Branch 223 → 224 taken 172 times.
✓ Branch 224 → 225 taken 110 times.
✓ Branch 224 → 226 taken 62 times.
✓ Branch 227 → 228 taken 172 times.
✓ Branch 247 → 219 taken 172 times.
✓ Branch 247 → 248 taken 172 times.
|
344 | qCInfo(lcCalDav) << "sync-collection" << calendarPath << (full ? "full" : "delta") |
| 639 |
4/4✓ Branch 228 → 229 taken 172 times.
✓ Branch 230 → 231 taken 172 times.
✓ Branch 231 → 232 taken 172 times.
✓ Branch 233 → 234 taken 172 times.
|
172 | << "events" << changes.events.size() << "tasks" << changes.tasks.size() |
| 640 |
4/4✓ Branch 234 → 235 taken 172 times.
✓ Branch 236 → 237 taken 172 times.
✓ Branch 237 → 238 taken 172 times.
✓ Branch 239 → 240 taken 172 times.
|
172 | << "removed" << changes.removedHrefs.size() << "failed" << changes.failedHrefs.size() |
| 641 |
3/3✓ Branch 240 → 241 taken 5 times.
✓ Branch 240 → 242 taken 167 times.
✓ Branch 243 → 244 taken 172 times.
|
172 | << (changes.truncated ? "truncated" : "complete"); |
| 642 | // F-24 (SPRINT-104): RFC 6578 does not oblige a server to carry | ||
| 643 | // calendar-data in the sync-collection answer. Instead of failing the | ||
| 644 | // page forever, the members that came back without data are fetched | ||
| 645 | // with calendar-multiget (RFC 4791 §7.9) and merged; a multiget that | ||
| 646 | // also fails keeps the former failure path. A truncated page stays as | ||
| 647 | // it is (S96's paging owns it). | ||
| 648 | // S106-057: more than 100 data-less members are fetched in batches of 100 | ||
| 649 | // (the page used to fail wholesale and repeat identically every run). | ||
| 650 |
6/6✓ Branch 248 → 249 taken 167 times.
✓ Branch 248 → 252 taken 5 times.
✓ Branch 250 → 251 taken 6 times.
✓ Branch 250 → 252 taken 161 times.
✓ Branch 253 → 254 taken 6 times.
✓ Branch 253 → 258 taken 166 times.
|
172 | if (!changes.truncated && !changes.failedHrefs.isEmpty()) |
| 651 |
1/1✓ Branch 255 → 256 taken 6 times.
|
6 | fetchFailedMembers(calendarPath, changes); |
| 652 | else | ||
| 653 |
1/1✓ Branch 258 → 259 taken 166 times.
|
166 | emit collectionSynced(calendarPath, changes); |
| 654 | 192 | } | |
| 655 | |||
| 656 | 6 | void CalDavClient::fetchFailedMembers(const QString &calendarPath, | |
| 657 | DavCollectionChanges changes) { | ||
| 658 | // The members to fetch move out of failedHrefs; what stays unresolved is | ||
| 659 | // collected again and reported with the page. | ||
| 660 | 6 | const QStringList pending = std::exchange(changes.failedHrefs, {}); | |
| 661 |
1/1✓ Branch 8 → 9 taken 6 times.
|
6 | fetchFailedMemberBatch(calendarPath, std::move(changes), pending); |
| 662 | 6 | } | |
| 663 | |||
| 664 | 12 | void CalDavClient::fetchFailedMemberBatch(const QString &calendarPath, | |
| 665 | DavCollectionChanges changes, | ||
| 666 | QStringList pending) { | ||
| 667 |
2/2✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 6 taken 7 times.
|
12 | if (pending.isEmpty()) { |
| 668 |
1/1✓ Branch 4 → 5 taken 5 times.
|
5 | emit collectionSynced(calendarPath, changes); |
| 669 | 5 | return; | |
| 670 | } | ||
| 671 |
1/1✓ Branch 6 → 7 taken 7 times.
|
7 | const QStringList batch = pending.mid(0, MaxMultigetMembers); |
| 672 |
1/1✓ Branch 7 → 8 taken 7 times.
|
7 | pending = pending.mid(MaxMultigetMembers); |
| 673 |
1/1✓ Branch 10 → 11 taken 7 times.
|
7 | QNetworkReply *reply = sendCalendarMultiget(calendarPath, batch); |
| 674 |
1/2✗ Branch 11 → 12 not taken.
✓ Branch 11 → 17 taken 7 times.
|
7 | if (!reply) { |
| 675 | ✗ | changes.failedHrefs += batch + pending; | |
| 676 | ✗ | emit collectionSynced(calendarPath, changes); | |
| 677 | ✗ | return; | |
| 678 | } | ||
| 679 | 7 | connect(reply, &QNetworkReply::finished, this, | |
| 680 |
1/7✓ Branch 21 → 22 taken 7 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
✗ Branch 43 → 44 not taken.
✗ Branch 43 → 45 not taken.
|
14 | [this, reply, calendarPath, changes, batch, pending]() mutable { |
| 681 |
1/1✓ Branch 2 → 3 taken 7 times.
|
7 | reply->deleteLater(); |
| 682 |
3/3✓ Branch 3 → 4 taken 7 times.
✓ Branch 4 → 5 taken 7 times.
✓ Branch 5 → 6 taken 7 times.
|
7 | MailJD::Performance::PimPerformanceTracker::instance().davWaitFinished(reply->bytesAvailable()); |
| 683 |
6/12✓ Branch 6 → 7 taken 7 times.
✓ Branch 8 → 9 taken 7 times.
✗ Branch 8 → 11 not taken.
✓ Branch 9 → 10 taken 7 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 7 times.
✓ Branch 13 → 14 taken 7 times.
✗ Branch 13 → 15 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 31 taken 7 times.
✗ Branch 116 → 117 not taken.
✗ Branch 116 → 118 not taken.
|
7 | if (!DavNetworkLimits::failureReason(reply).isEmpty() || reply->error() != QNetworkReply::NoError) { |
| 684 | // The multiget is a fallback: keep the page's own failure path. | ||
| 685 | ✗ | qCWarning(lcCalDav) << "calendar-multiget fallback failed"; | |
| 686 | ✗ | changes.failedHrefs += batch + pending; | |
| 687 | ✗ | emit collectionSynced(calendarPath, changes); | |
| 688 | 1 | return; | |
| 689 | } | ||
| 690 |
1/1✓ Branch 31 → 32 taken 7 times.
|
7 | const QByteArray data = reply->readAll(); |
| 691 | 7 | QString parseError; | |
| 692 | 7 | MultistatusSummary multiget; | |
| 693 | // S106-046: the parsers clear their output, and changes.events/tasks | ||
| 694 | // already hold the page's inline calendar-data: parse into fresh lists | ||
| 695 | // and append (a replace dropped those members; the enumeration prune | ||
| 696 | // then deleted them locally). | ||
| 697 | 7 | QList<CalendarEvent> events; | |
| 698 | 7 | QList<CalendarTask> tasks; | |
| 699 |
2/4✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 60 taken 6 times.
✗ Branch 128 → 129 not taken.
✗ Branch 128 → 130 not taken.
|
7 | if (!summarizeMultistatus(data, &multiget) || |
| 700 |
6/8✓ Branch 33 → 34 taken 7 times.
✓ Branch 34 → 35 taken 6 times.
✓ Branch 34 → 39 taken 1 time.
✓ Branch 35 → 36 taken 6 times.
✓ Branch 36 → 37 taken 6 times.
✗ Branch 36 → 39 not taken.
✓ Branch 41 → 42 taken 7 times.
✗ Branch 41 → 43 not taken.
|
13 | !parseICalEvents(data, &events, &parseError) || |
| 701 |
2/3✓ Branch 37 → 38 taken 6 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 6 times.
|
6 | !parseICalTasks(data, &tasks, &parseError)) { |
| 702 |
6/6✓ Branch 44 → 45 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 54 → 46 taken 1 time.
✓ Branch 54 → 55 taken 1 time.
|
2 | qCWarning(lcCalDav) << "calendar-multiget parse failed:" << parseError; |
| 703 |
2/2✓ Branch 55 → 56 taken 1 time.
✓ Branch 56 → 57 taken 1 time.
|
1 | changes.failedHrefs += batch + pending; |
| 704 |
1/1✓ Branch 58 → 59 taken 1 time.
|
1 | emit collectionSynced(calendarPath, changes); |
| 705 | 1 | return; | |
| 706 | } | ||
| 707 | 18 | const auto resolve = [this, calendarPath](auto &items) { | |
| 708 |
7/8auto CalDavClient::fetchFailedMemberBatch(QString const&, DavCollectionChanges, QList<QString>)::{lambda()#1}::operator()()::{lambda(auto:1&)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&) const:
✓ Branch 2 → 3 taken 6 times.
✓ Branch 3 → 4 taken 6 times.
✗ Branch 15 → 5 not taken.
✓ Branch 15 → 16 taken 6 times.
auto CalDavClient::fetchFailedMemberBatch(QString const&, DavCollectionChanges, QList<QString>)::{lambda()#1}::operator()()::{lambda(auto:1&)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&) const:
✓ Branch 2 → 3 taken 6 times.
✓ Branch 3 → 4 taken 6 times.
✓ Branch 15 → 5 taken 4 times.
✓ Branch 15 → 16 taken 6 times.
|
28 | for (auto &item : items) { |
| 709 | 4 | item.calendarPath = calendarPath; | |
| 710 |
1/4auto CalDavClient::fetchFailedMemberBatch(QString const&, DavCollectionChanges, QList<QString>)::{lambda()#1}::operator()()::{lambda(auto:1&)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&) const:
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 13 not taken.
auto CalDavClient::fetchFailedMemberBatch(QString const&, DavCollectionChanges, QList<QString>)::{lambda()#1}::operator()()::{lambda(auto:1&)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&) const:
✓ Branch 8 → 9 taken 4 times.
✗ Branch 8 → 13 not taken.
|
4 | if (!item.resourceHref.isEmpty()) |
| 711 |
1/2auto CalDavClient::fetchFailedMemberBatch(QString const&, DavCollectionChanges, QList<QString>)::{lambda()#1}::operator()()::{lambda(auto:1&)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&) const:
✗ Branch 9 → 10 not taken.
auto CalDavClient::fetchFailedMemberBatch(QString const&, DavCollectionChanges, QList<QString>)::{lambda()#1}::operator()()::{lambda(auto:1&)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&) const:
✓ Branch 9 → 10 taken 4 times.
|
4 | item.resourceHref = resolveDavUrl(item.resourceHref); |
| 712 | } | ||
| 713 | 18 | }; | |
| 714 |
1/1✓ Branch 61 → 62 taken 6 times.
|
6 | resolve(events); |
| 715 |
1/1✓ Branch 62 → 63 taken 6 times.
|
6 | resolve(tasks); |
| 716 |
6/6✓ Branch 63 → 64 taken 6 times.
✓ Branch 67 → 68 taken 6 times.
✓ Branch 68 → 69 taken 6 times.
✓ Branch 71 → 72 taken 6 times.
✓ Branch 77 → 65 taken 6 times.
✓ Branch 77 → 78 taken 6 times.
|
12 | qCInfo(lcCalDav) << "calendar-multiget filled" << events.size() + tasks.size() |
| 717 |
2/2✓ Branch 72 → 73 taken 6 times.
✓ Branch 73 → 74 taken 6 times.
|
6 | << "member(s) of" << calendarPath; |
| 718 |
1/1✓ Branch 78 → 79 taken 6 times.
|
6 | changes.events += events; |
| 719 |
1/1✓ Branch 79 → 80 taken 6 times.
|
6 | changes.tasks += tasks; |
| 720 |
2/2✓ Branch 80 → 81 taken 6 times.
✓ Branch 81 → 82 taken 6 times.
|
6 | changes.failedHrefs += failedMembers(multiget.failed, calendarPath); |
| 721 | 6 | changes.failedHrefs += unansweredMembers(batch, multiget.listed, multiget.failed, | |
| 722 |
2/2✓ Branch 83 → 84 taken 6 times.
✓ Branch 84 → 85 taken 6 times.
|
6 | multiget.removed, calendarPath); |
| 723 |
1/1✓ Branch 89 → 90 taken 6 times.
|
6 | fetchFailedMemberBatch(calendarPath, std::move(changes), pending); |
| 724 |
10/10✓ Branch 95 → 96 taken 6 times.
✓ Branch 95 → 97 taken 1 time.
✓ Branch 99 → 100 taken 6 times.
✓ Branch 99 → 101 taken 1 time.
✓ Branch 103 → 104 taken 6 times.
✓ Branch 103 → 105 taken 1 time.
✓ Branch 107 → 108 taken 6 times.
✓ Branch 107 → 109 taken 1 time.
✓ Branch 111 → 112 taken 6 times.
✓ Branch 111 → 114 taken 1 time.
|
11 | }); |
| 725 | 7 | } | |
| 726 | |||
| 727 | // ── F-54 (SPRINT-104): bounded initial download ───────────────────── | ||
| 728 | |||
| 729 | 16 | QNetworkReply *CalDavClient::sendCalendarMultiget(const QString &calendarPath, | |
| 730 | const QStringList &hrefs) { | ||
| 731 |
1/1✓ Branch 2 → 3 taken 16 times.
|
16 | const QString url = resolveDavUrl(calendarPath); |
| 732 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 16 times.
|
16 | if (url.isEmpty()) return nullptr; |
| 733 | 16 | QByteArray hrefXml; | |
| 734 |
2/2✓ Branch 32 → 8 taken 287 times.
✓ Branch 32 → 33 taken 16 times.
|
303 | for (const QString &href : hrefs) { |
| 735 | // S106-048: the href keeps its percent-encoding (%23, %3F, %2F) — the | ||
| 736 | // decoded path named another resource and the member 404ed every run | ||
| 737 | // (CardDavClient does the same). | ||
| 738 |
2/2✓ Branch 9 → 10 taken 287 times.
✓ Branch 11 → 12 taken 287 times.
|
287 | const QString path = QUrl(href).path(QUrl::FullyEncoded); |
| 739 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 287 times.
|
287 | if (path.isEmpty()) return nullptr; |
| 740 |
5/5✓ Branch 16 → 17 taken 287 times.
✓ Branch 17 → 18 taken 287 times.
✓ Branch 18 → 19 taken 287 times.
✓ Branch 19 → 20 taken 287 times.
✓ Branch 20 → 21 taken 287 times.
|
287 | hrefXml += "<d:href>" + path.toHtmlEscaped().toUtf8() + "</d:href>"; |
| 741 | 287 | } | |
| 742 |
2/2✓ Branch 33 → 34 taken 16 times.
✓ Branch 34 → 35 taken 16 times.
|
16 | QNetworkRequest request{QUrl(url)}; |
| 743 |
3/3✓ Branch 36 → 37 taken 16 times.
✓ Branch 37 → 38 taken 16 times.
✓ Branch 38 → 39 taken 16 times.
|
16 | request.setRawHeader("Authorization", authHeader()); |
| 744 |
3/3✓ Branch 41 → 42 taken 16 times.
✓ Branch 42 → 43 taken 16 times.
✓ Branch 43 → 44 taken 16 times.
|
16 | request.setRawHeader("Depth", "1"); |
| 745 |
3/3✓ Branch 46 → 47 taken 16 times.
✓ Branch 47 → 48 taken 16 times.
✓ Branch 48 → 49 taken 16 times.
|
16 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 746 | const QByteArray body = | ||
| 747 | "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" | ||
| 748 | "<c:calendar-multiget xmlns:d=\"DAV:\" " | ||
| 749 | " xmlns:c=\"urn:ietf:params:xml:ns:caldav\">" | ||
| 750 | 16 | " <d:prop><d:getetag/><c:calendar-data/></d:prop>" + | |
| 751 |
2/2✓ Branch 51 → 52 taken 16 times.
✓ Branch 52 → 53 taken 16 times.
|
16 | hrefXml + "</c:calendar-multiget>"; |
| 752 |
2/2✓ Branch 54 → 55 taken 16 times.
✓ Branch 55 → 56 taken 16 times.
|
16 | auto *reply = m_nam->sendCustomRequest(request, "REPORT", body); |
| 753 |
2/2✓ Branch 57 → 58 taken 16 times.
✓ Branch 58 → 59 taken 16 times.
|
16 | MailJD::Performance::PimPerformanceTracker::instance().davWaitStarted(); |
| 754 |
1/1✓ Branch 59 → 60 taken 16 times.
|
16 | DavNetworkLimits::apply(reply); |
| 755 |
1/1✓ Branch 60 → 61 taken 16 times.
|
16 | trackReply(reply); |
| 756 | 16 | return reply; | |
| 757 | 16 | } | |
| 758 | |||
| 759 | 6 | void CalDavClient::requestEtagListing(const QString &calendarPath) { | |
| 760 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | const QString url = resolveDavUrl(calendarPath); |
| 761 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 6 times.
|
6 | if (url.isEmpty()) { |
| 762 | ✗ | emit syncFailed(QStringLiteral("Cross-origin URL rejected")); | |
| 763 | ✗ | return; | |
| 764 | } | ||
| 765 |
2/2✓ Branch 12 → 13 taken 6 times.
✓ Branch 13 → 14 taken 6 times.
|
6 | QNetworkRequest request{QUrl(url)}; |
| 766 |
3/3✓ Branch 15 → 16 taken 6 times.
✓ Branch 16 → 17 taken 6 times.
✓ Branch 17 → 18 taken 6 times.
|
6 | request.setRawHeader("Authorization", authHeader()); |
| 767 |
3/3✓ Branch 20 → 21 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
|
6 | request.setRawHeader("Depth", "0"); |
| 768 |
3/3✓ Branch 25 → 26 taken 6 times.
✓ Branch 26 → 27 taken 6 times.
✓ Branch 27 → 28 taken 6 times.
|
6 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 769 | // The same token-less sync-collection as syncCollection(), but the prop | ||
| 770 | // asks for ETags only: the answer is a small member list, never the | ||
| 771 | // whole calendar's data. | ||
| 772 | const QByteArray body = | ||
| 773 | "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" | ||
| 774 | "<d:sync-collection xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">" | ||
| 775 | "<d:sync-token></d:sync-token>" | ||
| 776 | "<d:sync-level>1</d:sync-level>" | ||
| 777 | "<d:prop><d:getetag/></d:prop>" | ||
| 778 |
1/1✓ Branch 30 → 31 taken 6 times.
|
6 | "</d:sync-collection>"; |
| 779 |
2/2✓ Branch 31 → 32 taken 6 times.
✓ Branch 32 → 33 taken 6 times.
|
6 | auto *reply = m_nam->sendCustomRequest(request, "REPORT", body); |
| 780 |
2/2✓ Branch 34 → 35 taken 6 times.
✓ Branch 35 → 36 taken 6 times.
|
6 | MailJD::Performance::PimPerformanceTracker::instance().davWaitStarted(); |
| 781 |
1/1✓ Branch 36 → 37 taken 6 times.
|
6 | DavNetworkLimits::apply(reply); |
| 782 |
1/1✓ Branch 37 → 38 taken 6 times.
|
6 | trackReply(reply); |
| 783 |
1/1✓ Branch 39 → 40 taken 6 times.
|
6 | connect(reply, &QNetworkReply::finished, this, |
| 784 | 12 | [this, reply, calendarPath]() { | |
| 785 | 6 | onEtagListingReply(reply, calendarPath); | |
| 786 | 6 | }); | |
| 787 | 6 | } | |
| 788 | |||
| 789 | 6 | void CalDavClient::onEtagListingReply(QNetworkReply *reply, const QString &calendarPath) { | |
| 790 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | reply->deleteLater(); |
| 791 |
1/1✓ Branch 3 → 4 taken 6 times.
|
6 | auto &performanceTracker = MailJD::Performance::PimPerformanceTracker::instance(); |
| 792 |
2/2✓ Branch 4 → 5 taken 6 times.
✓ Branch 5 → 6 taken 6 times.
|
6 | performanceTracker.davWaitFinished(reply->bytesAvailable()); |
| 793 |
1/1✓ Branch 6 → 7 taken 6 times.
|
6 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 794 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 22 taken 5 times.
|
6 | if (!limitError.isEmpty()) { |
| 795 | // Even the ETag listing breaks the bounds (≈85k members or a stalled | ||
| 796 | // server) — this collection is honestly too large for this client. | ||
| 797 |
6/6✓ 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 19 → 11 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
|
2 | qCWarning(lcCalDav) << "ETag listing failed:" << limitError; |
| 798 |
1/1✓ Branch 20 → 21 taken 1 time.
|
1 | emit syncFailed(limitError); |
| 799 | 1 | return; | |
| 800 | } | ||
| 801 |
2/2✓ Branch 22 → 23 taken 5 times.
✓ Branch 23 → 24 taken 5 times.
|
5 | const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 802 |
2/3✓ Branch 25 → 26 taken 5 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 67 taken 5 times.
|
5 | if (reply->error() != QNetworkReply::NoError) { |
| 803 | // The data-carrying request before this one was accepted, so the | ||
| 804 | // unsupported classification is mostly theoretical; kept for parity. | ||
| 805 | ✗ | if (status == 400 || status == 404 || status == 405 || status == 415 || status == 501 || | |
| 806 | status == 409) { | ||
| 807 | ✗ | qCInfo(lcCalDav) << "ETag listing not supported for" << calendarPath << "status" << status; | |
| 808 | ✗ | emit collectionSyncRejected(calendarPath, false); | |
| 809 | ✗ | return; | |
| 810 | } | ||
| 811 | ✗ | qCWarning(lcCalDav) << "ETag listing failed:" << status << reply->errorString(); | |
| 812 | ✗ | m_lastNetworkError = reply->error(); | |
| 813 | ✗ | emit syncFailed(reply->errorString()); | |
| 814 | ✗ | return; | |
| 815 | } | ||
| 816 |
1/1✓ Branch 67 → 68 taken 5 times.
|
5 | const QByteArray data = reply->readAll(); |
| 817 | 5 | MultistatusSummary summary; | |
| 818 |
2/3✓ Branch 68 → 69 taken 5 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 77 taken 5 times.
|
5 | if (!summarizeMultistatus(data, &summary, /*requireCalendarData=*/false, calendarPath)) { |
| 819 | ✗ | emit syncFailed(QStringLiteral("Invalid XML response")); | |
| 820 | ✗ | return; | |
| 821 | } | ||
| 822 |
1/2✗ Branch 78 → 79 not taken.
✓ Branch 78 → 81 taken 5 times.
|
5 | if (summary.token.isEmpty()) { |
| 823 | // Without a new token the result cannot be applied incrementally. | ||
| 824 | ✗ | emit collectionSyncRejected(calendarPath, false); | |
| 825 | ✗ | return; | |
| 826 | } | ||
| 827 |
1/2✗ Branch 81 → 82 not taken.
✓ Branch 81 → 126 taken 5 times.
|
5 | if (summary.truncated) { |
| 828 | // Checkpoint the truncated listing; the continuation rides the normal | ||
| 829 | // delta path (token + calendar-data), whose pages the server bounds. | ||
| 830 | ✗ | DavCollectionChanges changes; | |
| 831 | ✗ | changes.full = true; | |
| 832 | ✗ | changes.truncated = true; | |
| 833 | ✗ | changes.syncToken = summary.token; | |
| 834 | ✗ | for (const QString &removed : std::as_const(summary.removed)) { | |
| 835 | ✗ | const QString resolved = resolveDavUrl(removed); | |
| 836 | ✗ | if (resolved.isEmpty()) { | |
| 837 | ✗ | emit syncFailed(QStringLiteral("Cross-origin resource href rejected")); | |
| 838 | ✗ | return; | |
| 839 | } | ||
| 840 | ✗ | changes.removedHrefs << resolved; | |
| 841 | ✗ | } | |
| 842 | ✗ | changes.failedHrefs = failedMembers(summary.failed, calendarPath); | |
| 843 | ✗ | qCInfo(lcCalDav) << "ETag listing truncated for" << calendarPath | |
| 844 | ✗ | << "— checkpoint" << summary.token; | |
| 845 | ✗ | emit collectionSynced(calendarPath, changes); | |
| 846 | ✗ | return; | |
| 847 | ✗ | } | |
| 848 | // Complete listing: resolve the members, drop the collection itself and | ||
| 849 | // cross-origin answers, and start the batched multiget download. | ||
| 850 |
1/1✓ Branch 126 → 127 taken 5 times.
|
5 | const QString collectionUrl = resolveDavUrl(calendarPath); |
| 851 | 5 | EnumerationFetch fetch; | |
| 852 | 5 | fetch.syncToken = summary.token; | |
| 853 |
1/1✓ Branch 128 → 129 taken 5 times.
|
5 | fetch.failedHrefs = failedMembers(summary.failed, calendarPath); |
| 854 | 5 | bool hrefsOk = true; | |
| 855 |
2/2✓ Branch 160 → 134 taken 128 times.
✓ Branch 160 → 161 taken 5 times.
|
133 | for (const QString &listed : std::as_const(summary.listed)) { |
| 856 |
1/1✓ Branch 135 → 136 taken 128 times.
|
128 | const QString resolved = resolveDavUrl(listed); |
| 857 |
4/6✓ Branch 137 → 138 taken 128 times.
✗ Branch 137 → 145 not taken.
✓ Branch 139 → 140 taken 127 times.
✓ Branch 139 → 145 taken 1 time.
✗ Branch 144 → 145 not taken.
✓ Branch 144 → 146 taken 127 times.
|
255 | if (resolved.isEmpty() || resolved == collectionUrl || |
| 858 |
5/7✓ Branch 142 → 143 taken 127 times.
✓ Branch 147 → 148 taken 127 times.
✓ Branch 147 → 149 taken 1 time.
✓ Branch 149 → 150 taken 1 time.
✓ Branch 149 → 151 taken 127 times.
✗ Branch 277 → 278 not taken.
✗ Branch 277 → 279 not taken.
|
255 | resolved + QLatin1Char('/') == collectionUrl) |
| 859 | 1 | continue; | |
| 860 |
1/1✓ Branch 151 → 152 taken 127 times.
|
127 | fetch.pending << resolved; |
| 861 | 128 | } | |
| 862 |
2/2✓ Branch 177 → 164 taken 1 time.
✓ Branch 177 → 178 taken 5 times.
|
6 | for (const QString &removed : std::as_const(summary.removed)) { |
| 863 |
1/1✓ Branch 165 → 166 taken 1 time.
|
1 | const QString resolved = resolveDavUrl(removed); |
| 864 |
2/4✓ Branch 166 → 167 taken 1 time.
✗ Branch 166 → 170 not taken.
✓ Branch 168 → 169 taken 1 time.
✗ Branch 168 → 170 not taken.
|
1 | hrefsOk = hrefsOk && !resolved.isEmpty(); |
| 865 |
2/3✓ Branch 172 → 173 taken 1 time.
✗ Branch 172 → 174 not taken.
✓ Branch 173 → 174 taken 1 time.
|
1 | if (!resolved.isEmpty()) fetch.removedHrefs << resolved; |
| 866 | 1 | } | |
| 867 |
1/2✗ Branch 178 → 179 not taken.
✓ Branch 178 → 186 taken 5 times.
|
5 | if (!hrefsOk) { |
| 868 | ✗ | emit syncFailed(QStringLiteral("Cross-origin resource href rejected")); | |
| 869 | ✗ | return; | |
| 870 | } | ||
| 871 |
8/8✓ Branch 186 → 187 taken 5 times.
✓ Branch 190 → 191 taken 5 times.
✓ Branch 191 → 192 taken 5 times.
✓ Branch 192 → 193 taken 5 times.
✓ Branch 193 → 194 taken 5 times.
✓ Branch 195 → 196 taken 5 times.
✓ Branch 200 → 188 taken 5 times.
✓ Branch 200 → 201 taken 5 times.
|
10 | qCInfo(lcCalDav) << "ETag listing for" << calendarPath << "lists" << fetch.pending.size() |
| 872 |
1/1✓ Branch 196 → 197 taken 5 times.
|
5 | << "member(s) — batched multiget download starts"; |
| 873 |
1/1✓ Branch 202 → 203 taken 5 times.
|
5 | m_enumerations.insert(calendarPath, std::move(fetch)); |
| 874 |
1/1✓ Branch 203 → 204 taken 5 times.
|
5 | fetchNextEnumerationBatch(calendarPath); |
| 875 | 6 | } | |
| 876 | |||
| 877 | 14 | void CalDavClient::fetchNextEnumerationBatch(const QString &calendarPath) { | |
| 878 |
1/1✓ Branch 2 → 3 taken 14 times.
|
14 | auto it = m_enumerations.find(calendarPath); |
| 879 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 14 times.
|
19 | if (it == m_enumerations.end()) return; |
| 880 |
6/6✓ Branch 9 → 10 taken 12 times.
✓ Branch 9 → 14 taken 2 times.
✓ Branch 12 → 13 taken 7 times.
✓ Branch 12 → 14 taken 5 times.
✓ Branch 15 → 16 taken 7 times.
✓ Branch 15 → 35 taken 7 times.
|
14 | if (it->batches.isEmpty() && !it->pending.isEmpty()) { |
| 881 | // Group lazily so a halved batchSize shapes the groups still pending. | ||
| 882 | 7 | const int count = qMin(it->batchSize, it->pending.size()); | |
| 883 |
2/2✓ Branch 22 → 23 taken 7 times.
✓ Branch 23 → 24 taken 7 times.
|
7 | it->batches.append(it->pending.mid(0, count)); |
| 884 |
4/4✓ Branch 27 → 28 taken 7 times.
✓ Branch 28 → 29 taken 7 times.
✓ Branch 31 → 32 taken 7 times.
✓ Branch 33 → 34 taken 7 times.
|
7 | it->pending.erase(it->pending.begin(), it->pending.begin() + count); |
| 885 | } | ||
| 886 |
2/2✓ Branch 37 → 38 taken 5 times.
✓ Branch 37 → 72 taken 9 times.
|
14 | if (it->batches.isEmpty()) { |
| 887 | // Final page: everything arrived through bounded multiget batches. | ||
| 888 | 5 | DavCollectionChanges changes; | |
| 889 | 5 | changes.full = true; | |
| 890 | 5 | changes.syncToken = it->syncToken; | |
| 891 | 5 | changes.events = it->events; | |
| 892 | 5 | changes.tasks = it->tasks; | |
| 893 | 5 | changes.removedHrefs = it->removedHrefs; | |
| 894 | 5 | changes.failedHrefs = it->failedHrefs; | |
| 895 |
7/7✓ Branch 48 → 49 taken 5 times.
✓ Branch 52 → 53 taken 5 times.
✓ Branch 53 → 54 taken 5 times.
✓ Branch 54 → 55 taken 5 times.
✓ Branch 55 → 56 taken 5 times.
✓ Branch 66 → 50 taken 5 times.
✓ Branch 66 → 67 taken 5 times.
|
10 | qCInfo(lcCalDav) << "batched enumeration of" << calendarPath << "finished:" |
| 896 |
2/2✓ Branch 58 → 59 taken 5 times.
✓ Branch 59 → 60 taken 5 times.
|
5 | << changes.events.size() + changes.tasks.size() << "resource(s)," |
| 897 |
2/2✓ Branch 61 → 62 taken 5 times.
✓ Branch 62 → 63 taken 5 times.
|
5 | << changes.failedHrefs.size() << "failed"; |
| 898 |
1/1✓ Branch 67 → 68 taken 5 times.
|
5 | emit collectionSynced(calendarPath, changes); |
| 899 |
1/1✓ Branch 69 → 70 taken 5 times.
|
5 | m_enumerations.erase(it); |
| 900 | 5 | return; | |
| 901 | 5 | } | |
| 902 |
1/1✓ Branch 73 → 74 taken 9 times.
|
9 | const QStringList batch = it->batches.takeFirst(); |
| 903 |
1/1✓ Branch 74 → 75 taken 9 times.
|
9 | QNetworkReply *reply = sendCalendarMultiget(calendarPath, batch); |
| 904 |
1/2✗ Branch 75 → 76 not taken.
✓ Branch 75 → 85 taken 9 times.
|
9 | if (!reply) { |
| 905 | ✗ | emit syncFailed(QStringLiteral("Cross-origin resource href rejected")); | |
| 906 | ✗ | m_enumerations.erase(it); | |
| 907 | ✗ | return; | |
| 908 | } | ||
| 909 | 9 | connect(reply, &QNetworkReply::finished, this, | |
| 910 |
1/3✓ Branch 87 → 88 taken 9 times.
✗ Branch 121 → 122 not taken.
✗ Branch 121 → 123 not taken.
|
18 | [this, reply, calendarPath, batch]() { |
| 911 |
1/1✓ Branch 2 → 3 taken 9 times.
|
9 | reply->deleteLater(); |
| 912 |
3/3✓ Branch 3 → 4 taken 9 times.
✓ Branch 4 → 5 taken 9 times.
✓ Branch 5 → 6 taken 9 times.
|
9 | MailJD::Performance::PimPerformanceTracker::instance().davWaitFinished(reply->bytesAvailable()); |
| 913 |
1/1✓ Branch 6 → 7 taken 9 times.
|
9 | auto it = m_enumerations.find(calendarPath); |
| 914 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 9 times.
|
11 | if (it == m_enumerations.end()) return; |
| 915 |
1/1✓ Branch 11 → 12 taken 9 times.
|
9 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 916 |
2/2✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 68 taken 7 times.
|
9 | if (!limitError.isEmpty()) { |
| 917 |
2/2✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 50 taken 1 time.
|
2 | if (batch.size() > 1) { |
| 918 | // Split the failed batch (ContactBookSync's bounded-answer pattern) | ||
| 919 | // and halve the size of groups still to be formed. | ||
| 920 | 1 | it->batchSize = qMax(1, qMin(it->batchSize, batch.size()) / 2); | |
| 921 | 1 | const int half = batch.size() / 2; | |
| 922 |
2/2✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
|
1 | it->batches.prepend(batch.mid(half)); |
| 923 |
2/2✓ Branch 27 → 28 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
|
1 | it->batches.prepend(batch.mid(0, half)); |
| 924 |
8/8✓ Branch 30 → 31 taken 1 time.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 48 → 32 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
|
2 | qCInfo(lcCalDav) << "multiget batch of" << batch.size() << "for" << calendarPath |
| 925 |
4/4✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
|
1 | << "was too large — splitting into" << half << "+" << (batch.size() - half); |
| 926 | } else { | ||
| 927 | // A single resource above the response limit is a failed member; | ||
| 928 | // the collection sync is marked incomplete and retried next run. | ||
| 929 |
7/7✓ Branch 50 → 51 taken 1 time.
✓ Branch 54 → 55 taken 1 time.
✓ Branch 55 → 56 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 62 → 52 taken 1 time.
✓ Branch 62 → 63 taken 1 time.
|
2 | qCWarning(lcCalDav) << "resource" << batch.first() << "exceeds the response limit"; |
| 930 |
1/1✓ Branch 65 → 66 taken 1 time.
|
1 | it->failedHrefs << batch.first(); |
| 931 | } | ||
| 932 |
1/1✓ Branch 66 → 67 taken 2 times.
|
2 | fetchNextEnumerationBatch(calendarPath); |
| 933 | 2 | return; | |
| 934 | } | ||
| 935 |
2/3✓ Branch 68 → 69 taken 7 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 90 taken 7 times.
|
7 | if (reply->error() != QNetworkReply::NoError) { |
| 936 | ✗ | qCWarning(lcCalDav) << "batched multiget failed:" << reply->errorString(); | |
| 937 | ✗ | m_lastNetworkError = reply->error(); | |
| 938 | ✗ | emit syncFailed(reply->errorString()); | |
| 939 | ✗ | m_enumerations.erase(it); | |
| 940 | ✗ | return; | |
| 941 | } | ||
| 942 |
1/1✓ Branch 90 → 91 taken 7 times.
|
7 | const QByteArray data = reply->readAll(); |
| 943 | 7 | QString parseError; | |
| 944 | 7 | MultistatusSummary multiget; | |
| 945 | 7 | QList<CalendarEvent> events; // parseEvents clears its output — parse | |
| 946 | 7 | QList<CalendarTask> tasks; // per batch, then append to the totals | |
| 947 |
1/4✗ Branch 102 → 103 not taken.
✓ Branch 102 → 127 taken 7 times.
✗ Branch 208 → 209 not taken.
✗ Branch 208 → 210 not taken.
|
7 | if (!summarizeMultistatus(data, &multiget) || |
| 948 |
5/8✓ Branch 92 → 93 taken 7 times.
✓ Branch 93 → 94 taken 7 times.
✗ Branch 93 → 98 not taken.
✓ Branch 94 → 95 taken 7 times.
✓ Branch 95 → 96 taken 7 times.
✗ Branch 95 → 98 not taken.
✓ Branch 100 → 101 taken 7 times.
✗ Branch 100 → 102 not taken.
|
14 | !parseICalEvents(data, &events, &parseError) || |
| 949 |
2/3✓ Branch 96 → 97 taken 7 times.
✗ Branch 97 → 98 not taken.
✓ Branch 97 → 99 taken 7 times.
|
7 | !parseICalTasks(data, &tasks, &parseError)) { |
| 950 | ✗ | qCWarning(lcCalDav) << "batched multiget parse failed:" << parseError; | |
| 951 | ✗ | emit syncFailed(parseError.isEmpty() ? QStringLiteral("Invalid XML response") : parseError); | |
| 952 | ✗ | m_enumerations.erase(it); | |
| 953 | ✗ | return; | |
| 954 | } | ||
| 955 | 21 | const auto absorb = [this, &calendarPath](auto &items, bool *ok) { | |
| 956 |
7/8auto CalDavClient::fetchNextEnumerationBatch(QString const&)::{lambda()#1}::operator()() const::{lambda(auto:1&, bool*)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&, bool*) const:
✓ Branch 2 → 3 taken 7 times.
✓ Branch 3 → 4 taken 7 times.
✗ Branch 22 → 5 not taken.
✓ Branch 22 → 23 taken 7 times.
auto CalDavClient::fetchNextEnumerationBatch(QString const&)::{lambda()#1}::operator()() const::{lambda(auto:1&, bool*)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&, bool*) const:
✓ Branch 2 → 3 taken 7 times.
✓ Branch 3 → 4 taken 7 times.
✓ Branch 22 → 5 taken 126 times.
✓ Branch 22 → 23 taken 7 times.
|
154 | for (auto &item : items) { |
| 957 | 126 | item.calendarPath = calendarPath; | |
| 958 |
1/4auto CalDavClient::fetchNextEnumerationBatch(QString const&)::{lambda()#1}::operator()() const::{lambda(auto:1&, bool*)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&, bool*) const:
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 20 not taken.
auto CalDavClient::fetchNextEnumerationBatch(QString const&)::{lambda()#1}::operator()() const::{lambda(auto:1&, bool*)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&, bool*) const:
✓ Branch 8 → 9 taken 126 times.
✗ Branch 8 → 20 not taken.
|
126 | if (!item.resourceHref.isEmpty()) { |
| 959 |
1/2auto CalDavClient::fetchNextEnumerationBatch(QString const&)::{lambda()#1}::operator()() const::{lambda(auto:1&, bool*)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&, bool*) const:
✗ Branch 9 → 10 not taken.
auto CalDavClient::fetchNextEnumerationBatch(QString const&)::{lambda()#1}::operator()() const::{lambda(auto:1&, bool*)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&, bool*) const:
✓ Branch 9 → 10 taken 126 times.
|
126 | const QString resolved = resolveDavUrl(item.resourceHref); |
| 960 |
1/4auto CalDavClient::fetchNextEnumerationBatch(QString const&)::{lambda()#1}::operator()() const::{lambda(auto:1&, bool*)#1}::operator()<QList<CalendarTask> >(QList<CalendarTask>&, bool*) const:
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
auto CalDavClient::fetchNextEnumerationBatch(QString const&)::{lambda()#1}::operator()() const::{lambda(auto:1&, bool*)#1}::operator()<QList<CalendarEvent> >(QList<CalendarEvent>&, bool*) const:
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 126 times.
|
126 | if (resolved.isEmpty()) { |
| 961 | ✗ | *ok = false; | |
| 962 | ✗ | return; | |
| 963 | } | ||
| 964 | 126 | item.resourceHref = resolved; | |
| 965 | 126 | } | |
| 966 | } | ||
| 967 | 7 | }; | |
| 968 | 7 | bool hrefsOk = true; | |
| 969 |
1/1✓ Branch 127 → 128 taken 7 times.
|
7 | absorb(events, &hrefsOk); |
| 970 |
2/3✓ Branch 128 → 129 taken 7 times.
✗ Branch 128 → 130 not taken.
✓ Branch 129 → 130 taken 7 times.
|
7 | if (hrefsOk) absorb(tasks, &hrefsOk); |
| 971 |
1/2✗ Branch 130 → 131 not taken.
✓ Branch 130 → 140 taken 7 times.
|
7 | if (!hrefsOk) { |
| 972 | ✗ | emit syncFailed(QStringLiteral("Cross-origin resource href rejected")); | |
| 973 | ✗ | m_enumerations.erase(it); | |
| 974 | ✗ | return; | |
| 975 | } | ||
| 976 |
1/1✓ Branch 141 → 142 taken 7 times.
|
7 | it->events += events; |
| 977 |
1/1✓ Branch 143 → 144 taken 7 times.
|
7 | it->tasks += tasks; |
| 978 |
2/2✓ Branch 145 → 146 taken 7 times.
✓ Branch 146 → 147 taken 7 times.
|
7 | it->failedHrefs << failedMembers(multiget.failed, calendarPath); |
| 979 | 14 | it->failedHrefs << unansweredMembers(batch, multiget.listed, multiget.failed, | |
| 980 |
2/2✓ Branch 149 → 150 taken 7 times.
✓ Branch 150 → 151 taken 7 times.
|
7 | multiget.removed, calendarPath); |
| 981 |
1/1✓ Branch 152 → 153 taken 7 times.
|
7 | fetchNextEnumerationBatch(calendarPath); |
| 982 |
7/12✓ Branch 155 → 156 taken 7 times.
✗ Branch 155 → 157 not taken.
✓ Branch 159 → 160 taken 7 times.
✗ Branch 159 → 161 not taken.
✓ Branch 163 → 164 taken 7 times.
✗ Branch 163 → 165 not taken.
✓ Branch 167 → 168 taken 7 times.
✗ Branch 167 → 169 not taken.
✓ Branch 171 → 172 taken 7 times.
✗ Branch 171 → 173 not taken.
✓ Branch 175 → 176 taken 7 times.
✓ Branch 175 → 178 taken 2 times.
|
9 | }); |
| 983 | 9 | } | |
| 984 | |||
| 985 | // ═══════════════════════════════════════════════════════ | ||
| 986 | // Task Sync (REPORT calendar-query VTODO) | ||
| 987 | // ═══════════════════════════════════════════════════════ | ||
| 988 | |||
| 989 | 16 | void CalDavClient::syncTasks(const QString &calendarPath) { | |
| 990 |
1/1✓ Branch 2 → 3 taken 16 times.
|
16 | QString url = resolveDavUrl(calendarPath); |
| 991 |
2/2✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 12 taken 14 times.
|
16 | if (url.isEmpty()) { |
| 992 |
1/1✓ Branch 8 → 9 taken 2 times.
|
2 | emit syncFailed(QStringLiteral("Cross-origin URL rejected")); |
| 993 | 2 | return; | |
| 994 | } | ||
| 995 | |||
| 996 |
2/2✓ Branch 12 → 13 taken 14 times.
✓ Branch 13 → 14 taken 14 times.
|
14 | QNetworkRequest request{QUrl(url)}; |
| 997 |
3/3✓ Branch 15 → 16 taken 14 times.
✓ Branch 16 → 17 taken 14 times.
✓ Branch 17 → 18 taken 14 times.
|
14 | request.setRawHeader("Authorization", authHeader()); |
| 998 |
3/3✓ Branch 20 → 21 taken 14 times.
✓ Branch 21 → 22 taken 14 times.
✓ Branch 22 → 23 taken 14 times.
|
14 | request.setRawHeader("Depth", "1"); |
| 999 |
3/3✓ Branch 25 → 26 taken 14 times.
✓ Branch 26 → 27 taken 14 times.
✓ Branch 27 → 28 taken 14 times.
|
14 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 1000 | |||
| 1001 | QByteArray body = | ||
| 1002 | "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" | ||
| 1003 | "<c:calendar-query xmlns:d=\"DAV:\" " | ||
| 1004 | " xmlns:c=\"urn:ietf:params:xml:ns:caldav\">" | ||
| 1005 | " <d:prop>" | ||
| 1006 | " <d:getetag/>" | ||
| 1007 | " <c:calendar-data/>" | ||
| 1008 | " </d:prop>" | ||
| 1009 | " <c:filter>" | ||
| 1010 | " <c:comp-filter name=\"VCALENDAR\">" | ||
| 1011 | " <c:comp-filter name=\"VTODO\"/>" | ||
| 1012 | " </c:comp-filter>" | ||
| 1013 | " </c:filter>" | ||
| 1014 |
1/1✓ Branch 30 → 31 taken 14 times.
|
14 | "</c:calendar-query>"; |
| 1015 | |||
| 1016 | #ifdef MAILJD_E2E_TESTING | ||
| 1017 |
1/1✓ Branch 34 → 35 taken 10 times.
|
10 | reportE2eTransportPhase(QStringLiteral("REPORT_TASKS")); |
| 1018 | #endif | ||
| 1019 |
4/4✓ Branch 31 → 32 taken 4 times.
✓ Branch 32 → 33 taken 4 times.
✓ Branch 37 → 38 taken 10 times.
✓ Branch 38 → 39 taken 10 times.
|
14 | auto *reply = m_nam->sendCustomRequest(request, "REPORT", body); |
| 1020 |
4/4✓ Branch 34 → 35 taken 4 times.
✓ Branch 35 → 36 taken 4 times.
✓ Branch 40 → 41 taken 10 times.
✓ Branch 41 → 42 taken 10 times.
|
14 | MailJD::Performance::PimPerformanceTracker::instance().davWaitStarted(); |
| 1021 |
2/2✓ Branch 36 → 37 taken 4 times.
✓ Branch 42 → 43 taken 10 times.
|
14 | DavNetworkLimits::apply(reply); |
| 1022 |
2/2✓ Branch 37 → 38 taken 4 times.
✓ Branch 43 → 44 taken 10 times.
|
14 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 1023 |
2/2✓ Branch 39 → 40 taken 4 times.
✓ Branch 45 → 46 taken 10 times.
|
14 | connect(reply, &QNetworkReply::finished, this, |
| 1024 | 28 | [this, reply, calendarPath]() { | |
| 1025 | 14 | onSyncTasksReply(reply, calendarPath); | |
| 1026 | 14 | }); | |
| 1027 | 16 | } | |
| 1028 | |||
| 1029 | 14 | void CalDavClient::onSyncTasksReply(QNetworkReply *reply, | |
| 1030 | const QString &calendarPath) { | ||
| 1031 |
1/1✓ Branch 2 → 3 taken 14 times.
|
14 | reply->deleteLater(); |
| 1032 | auto &performanceTracker = | ||
| 1033 |
1/1✓ Branch 3 → 4 taken 14 times.
|
14 | MailJD::Performance::PimPerformanceTracker::instance(); |
| 1034 |
2/2✓ Branch 4 → 5 taken 14 times.
✓ Branch 5 → 6 taken 14 times.
|
14 | performanceTracker.davWaitFinished(reply->bytesAvailable()); |
| 1035 | |||
| 1036 |
1/1✓ Branch 6 → 7 taken 14 times.
|
14 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 1037 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 22 taken 13 times.
|
14 | if (!limitError.isEmpty()) { |
| 1038 |
6/6✓ 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 19 → 11 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
|
2 | qCWarning(lcCalDav) << "REPORT (tasks) failed:" << limitError; |
| 1039 |
1/1✓ Branch 20 → 21 taken 1 time.
|
1 | emit syncFailed(limitError); |
| 1040 | 1 | return; | |
| 1041 | } | ||
| 1042 | |||
| 1043 |
3/3✓ Branch 22 → 23 taken 13 times.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 42 taken 12 times.
|
13 | if (reply->error() != QNetworkReply::NoError) { |
| 1044 |
5/5✓ Branch 24 → 25 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 36 → 26 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
|
2 | qCWarning(lcCalDav) << "REPORT (tasks) failed:" |
| 1045 |
2/2✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
|
1 | << reply->errorString(); |
| 1046 |
1/1✓ Branch 37 → 38 taken 1 time.
|
1 | m_lastNetworkError = reply->error(); |
| 1047 |
2/2✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
|
1 | emit syncFailed(reply->errorString()); |
| 1048 | 1 | return; | |
| 1049 | } | ||
| 1050 | |||
| 1051 |
1/1✓ Branch 42 → 43 taken 12 times.
|
12 | QByteArray data = reply->readAll(); |
| 1052 | 12 | QList<CalendarTask> tasks; | |
| 1053 | 12 | QString parseError; | |
| 1054 |
1/1✓ Branch 43 → 44 taken 12 times.
|
12 | performanceTracker.parseStarted(); |
| 1055 |
3/3✓ Branch 44 → 45 taken 12 times.
✓ Branch 45 → 46 taken 2 times.
✓ Branch 45 → 61 taken 10 times.
|
12 | if (!parseICalTasks(data, &tasks, &parseError)) { |
| 1056 |
1/1✓ Branch 47 → 48 taken 2 times.
|
2 | performanceTracker.parseFinished(0, data.size()); |
| 1057 |
6/6✓ Branch 48 → 49 taken 2 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
✓ Branch 54 → 55 taken 2 times.
✓ Branch 58 → 50 taken 2 times.
✓ Branch 58 → 59 taken 2 times.
|
4 | qCWarning(lcCalDav) << "REPORT (tasks) parse failed:" << parseError; |
| 1058 |
1/1✓ Branch 59 → 60 taken 2 times.
|
2 | emit syncFailed(parseError); |
| 1059 | 2 | return; | |
| 1060 | } | ||
| 1061 |
1/1✓ Branch 63 → 64 taken 10 times.
|
10 | performanceTracker.parseFinished(tasks.size(), data.size()); |
| 1062 | |||
| 1063 |
4/4✓ Branch 64 → 65 taken 10 times.
✓ Branch 65 → 66 taken 10 times.
✓ Branch 90 → 67 taken 9 times.
✓ Branch 90 → 91 taken 9 times.
|
18 | for (auto &t : tasks) { |
| 1064 | 9 | t.calendarPath = calendarPath; | |
| 1065 |
2/2✓ Branch 70 → 71 taken 8 times.
✓ Branch 70 → 88 taken 1 time.
|
9 | if (!t.resourceHref.isEmpty()) { |
| 1066 |
1/1✓ Branch 71 → 72 taken 8 times.
|
8 | const QString resolvedHref = resolveDavUrl(t.resourceHref); |
| 1067 |
2/2✓ Branch 73 → 74 taken 1 time.
✓ Branch 73 → 81 taken 7 times.
|
8 | if (resolvedHref.isEmpty()) { |
| 1068 |
1/1✓ Branch 77 → 78 taken 1 time.
|
1 | emit syncFailed(QStringLiteral("Cross-origin resource href rejected")); |
| 1069 | 1 | return; | |
| 1070 | } | ||
| 1071 | 7 | t.resourceHref = resolvedHref; | |
| 1072 | 8 | } | |
| 1073 | } | ||
| 1074 | |||
| 1075 |
1/1✓ Branch 91 → 92 taken 9 times.
|
9 | const bool complete = queryComplete(data, calendarPath); |
| 1076 |
7/7✓ Branch 92 → 93 taken 9 times.
✓ Branch 96 → 97 taken 9 times.
✓ Branch 97 → 98 taken 9 times.
✓ Branch 99 → 100 taken 9 times.
✓ Branch 100 → 101 taken 9 times.
✓ Branch 109 → 94 taken 9 times.
✓ Branch 109 → 110 taken 9 times.
|
18 | qCInfo(lcCalDav) << "Synced" << tasks.size() << "tasks from" |
| 1077 |
3/4✓ Branch 101 → 102 taken 9 times.
✓ Branch 102 → 103 taken 9 times.
✗ Branch 102 → 104 not taken.
✓ Branch 105 → 106 taken 9 times.
|
9 | << calendarPath << (complete ? "complete" : "incomplete"); |
| 1078 |
1/1✓ Branch 110 → 111 taken 9 times.
|
9 | emit tasksSynced(calendarPath, tasks, complete); |
| 1079 | 23 | } | |
| 1080 | |||
| 1081 | // ═══════════════════════════════════════════════════════ | ||
| 1082 | // iCal Parsing (RFC 5545): CalDavXml (Sprint 102 S102.3.4) | ||
| 1083 | // ═══════════════════════════════════════════════════════ | ||
| 1084 | |||
| 1085 | QList<CalendarEvent> | ||
| 1086 | 21 | CalDavClient::parseICalEvents(const QByteArray &xmlData) { | |
| 1087 | 21 | QList<CalendarEvent> events; | |
| 1088 | 21 | QString error; | |
| 1089 |
1/1✓ Branch 2 → 3 taken 21 times.
|
21 | parseICalEvents(xmlData, &events, &error); |
| 1090 | 21 | return events; | |
| 1091 | 21 | } | |
| 1092 | |||
| 1093 | 233 | bool CalDavClient::parseICalEvents(const QByteArray &xmlData, | |
| 1094 | QList<CalendarEvent> *events, | ||
| 1095 | QString *error) { | ||
| 1096 | 233 | return CalDavXml::parseEvents(xmlData, events, error); | |
| 1097 | } | ||
| 1098 | |||
| 1099 | QList<CalendarTask> | ||
| 1100 | 37 | CalDavClient::parseICalTasks(const QByteArray &xmlData) { | |
| 1101 | 37 | QList<CalendarTask> tasks; | |
| 1102 | 37 | QString error; | |
| 1103 |
1/1✓ Branch 2 → 3 taken 37 times.
|
37 | parseICalTasks(xmlData, &tasks, &error); |
| 1104 | 37 | return tasks; | |
| 1105 | 37 | } | |
| 1106 | |||
| 1107 | 236 | bool CalDavClient::parseICalTasks(const QByteArray &xmlData, | |
| 1108 | QList<CalendarTask> *tasks, | ||
| 1109 | QString *error) { | ||
| 1110 | 236 | return CalDavXml::parseTasks(xmlData, tasks, error); | |
| 1111 | } | ||
| 1112 | |||
| 1113 | // ═══════════════════════════════════════════════════════ | ||
| 1114 | // Sprint 39 – T-530: Write API (iCal serialization + PUT/DELETE) | ||
| 1115 | // ═══════════════════════════════════════════════════════ | ||
| 1116 | |||
| 1117 | ✗ | static QByteArray escapeICalText(const QString &text) { | |
| 1118 | ✗ | QString escaped = text; | |
| 1119 | ✗ | escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\")); | |
| 1120 | ✗ | escaped.replace(QLatin1Char(';'), QStringLiteral("\\;")); | |
| 1121 | ✗ | escaped.replace(QLatin1Char(','), QStringLiteral("\\,")); | |
| 1122 | ✗ | escaped.replace(QLatin1Char('\n'), QStringLiteral("\\n")); | |
| 1123 | ✗ | escaped.remove(QLatin1Char('\r')); | |
| 1124 | ✗ | return escaped.toUtf8(); | |
| 1125 | ✗ | } | |
| 1126 | |||
| 1127 | ✗ | static QByteArray formatICalDate(const QDateTime &dt, bool allDay) { | |
| 1128 | ✗ | if (allDay) | |
| 1129 | ✗ | return dt.date().toString(QStringLiteral("yyyyMMdd")).toUtf8(); | |
| 1130 | // Always emit UTC | ||
| 1131 | ✗ | return dt.toUTC() | |
| 1132 | ✗ | .toString(QStringLiteral("yyyyMMdd'T'HHmmss'Z'")) | |
| 1133 | ✗ | .toUtf8(); | |
| 1134 | } | ||
| 1135 | |||
| 1136 | 153 | QByteArray CalDavClient::eventToICalendar(const CalendarEvent &event) { | |
| 1137 |
1/1✓ Branch 3 → 4 taken 153 times.
|
153 | return seriesToICalendar(event, {}); |
| 1138 | } | ||
| 1139 | |||
| 1140 | 370 | QByteArray CalDavClient::seriesToICalendar(const CalendarEvent &master, | |
| 1141 | const QList<CalendarEvent> &exceptions) { | ||
| 1142 | // Sprint 93 S93.1.1: patch the stored components instead of rebuilding | ||
| 1143 | // them from the interpreted fields (B-01). | ||
| 1144 | 370 | return ICalEventCodec::buildResource(master, exceptions, master.resourceIcal); | |
| 1145 | } | ||
| 1146 | |||
| 1147 | 122 | QByteArray CalDavClient::taskToICalendar(const CalendarTask &task) { | |
| 1148 | 122 | return ICalTodoCodec::buildResource(task); | |
| 1149 | } | ||
| 1150 | |||
| 1151 | 96 | QString CalDavClient::resourceUrl(const QString &calendarPath, | |
| 1152 | const QString &uid) const { | ||
| 1153 |
1/1✓ Branch 2 → 3 taken 96 times.
|
96 | QString basePath = resolveDavUrl(calendarPath); |
| 1154 |
2/2✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 6 taken 89 times.
|
96 | if (basePath.isEmpty()) |
| 1155 | 7 | return {}; | |
| 1156 |
3/3✓ Branch 8 → 9 taken 89 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 14 taken 86 times.
|
89 | if (!basePath.endsWith(QLatin1Char('/'))) |
| 1157 |
1/1✓ Branch 12 → 13 taken 3 times.
|
3 | basePath += QLatin1Char('/'); |
| 1158 |
3/3✓ Branch 19 → 20 taken 89 times.
✓ Branch 20 → 21 taken 89 times.
✓ Branch 21 → 22 taken 89 times.
|
178 | return basePath + QString::fromUtf8(QUrl::toPercentEncoding(uid)) + |
| 1159 |
1/1✓ Branch 22 → 23 taken 89 times.
|
267 | QStringLiteral(".ics"); |
| 1160 | 96 | } | |
| 1161 | |||
| 1162 | 196 | QString CalDavClient::resourceUrlForExistingResource( | |
| 1163 | const QString &resourceHref, const QString &calendarPath, | ||
| 1164 | const QString &uid) const { | ||
| 1165 |
2/2✓ Branch 3 → 4 taken 119 times.
✓ Branch 3 → 5 taken 77 times.
|
196 | if (!resourceHref.isEmpty()) |
| 1166 | 119 | return resolveDavUrl(resourceHref); | |
| 1167 | 77 | return resourceUrl(calendarPath, uid); | |
| 1168 | } | ||
| 1169 | |||
| 1170 | // ═══════════════════════════════════════════════════════ | ||
| 1171 | // Sprint 93 S93.1.6: raw resource requests (write outbox) | ||
| 1172 | // ═══════════════════════════════════════════════════════ | ||
| 1173 | |||
| 1174 | 168 | QString CalDavClient::eventResourceUrl(const QString &resourceHref, const QString &calendarPath, | |
| 1175 | const QString &uid) const { | ||
| 1176 | 168 | return resourceUrlForExistingResource(resourceHref, calendarPath, uid); | |
| 1177 | } | ||
| 1178 | |||
| 1179 | 234 | quint64 CalDavClient::sendResourceRequest(const QByteArray &method, const QString &url, | |
| 1180 | const QByteArray &body, | ||
| 1181 | const QList<QPair<QByteArray, QByteArray>> &headers) { | ||
| 1182 | 234 | const quint64 id = ++m_nextResourceRequest; | |
| 1183 |
2/3✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 234 times.
✓ Branch 5 → 6 taken 234 times.
|
234 | const QString resolved = url.isEmpty() ? QString() : resolveDavUrl(url); |
| 1184 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 13 taken 234 times.
|
234 | if (resolved.isEmpty()) { |
| 1185 | // Answer asynchronously like a network reply would. | ||
| 1186 | ✗ | QTimer::singleShot(0, this, [this, id, method, url] { | |
| 1187 | ✗ | DavResponse response; | |
| 1188 | ✗ | response.request = id; | |
| 1189 | ✗ | response.method = method; | |
| 1190 | ✗ | response.url = url; | |
| 1191 | ✗ | response.error = QStringLiteral("Rejected or empty resource URL"); | |
| 1192 | ✗ | emit resourceResponse(response); | |
| 1193 | ✗ | }); | |
| 1194 | ✗ | return id; | |
| 1195 | } | ||
| 1196 |
2/2✓ Branch 13 → 14 taken 234 times.
✓ Branch 14 → 15 taken 234 times.
|
234 | QNetworkRequest request{QUrl(resolved)}; |
| 1197 |
3/3✓ Branch 16 → 17 taken 234 times.
✓ Branch 17 → 18 taken 234 times.
✓ Branch 18 → 19 taken 234 times.
|
234 | request.setRawHeader("Authorization", authHeader()); |
| 1198 |
2/2✓ Branch 22 → 23 taken 128 times.
✓ Branch 22 → 29 taken 106 times.
|
234 | if (!body.isEmpty()) |
| 1199 |
3/3✓ Branch 23 → 24 taken 128 times.
✓ Branch 24 → 25 taken 128 times.
✓ Branch 25 → 26 taken 128 times.
|
128 | request.setRawHeader("Content-Type", "text/calendar; charset=utf-8"); |
| 1200 |
2/2✓ Branch 35 → 31 taken 142 times.
✓ Branch 35 → 36 taken 234 times.
|
376 | for (const auto &header : headers) |
| 1201 |
1/1✓ Branch 32 → 33 taken 142 times.
|
142 | request.setRawHeader(header.first, header.second); |
| 1202 |
8/8✓ Branch 37 → 38 taken 109 times.
✓ Branch 37 → 40 taken 125 times.
✓ Branch 39 → 40 taken 17 times.
✓ Branch 39 → 43 taken 92 times.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 43 taken 141 times.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 55 taken 233 times.
|
234 | if ((method == "PUT" || method == "DELETE") && !m_pushDontNotify.isEmpty()) |
| 1203 |
5/5✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
|
1 | request.setRawHeader("Push-Dont-Notify", "\"" + m_pushDontNotify.toUtf8() + "\""); |
| 1204 | #ifdef MAILJD_E2E_TESTING | ||
| 1205 |
3/3✓ Branch 58 → 59 taken 159 times.
✓ Branch 59 → 60 taken 159 times.
✓ Branch 60 → 61 taken 159 times.
|
159 | reportE2eTransportPhase(QString::fromLatin1(method) + QStringLiteral("_RESOURCE")); |
| 1206 | #endif | ||
| 1207 | // S106-058: a resource request never follows a redirect — QNAM re-sent a | ||
| 1208 | // followed 301/302 PUT/DELETE as GET, and the GET's status/ETag was taken | ||
| 1209 | // for the write result (the edit was lost, a DELETE looked done). The 3xx | ||
| 1210 | // is the answer. | ||
| 1211 |
2/2✓ Branch 56 → 57 taken 75 times.
✓ Branch 66 → 67 taken 159 times.
|
234 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); |
| 1212 |
6/6✓ Branch 59 → 60 taken 31 times.
✓ Branch 59 → 62 taken 44 times.
✓ Branch 60 → 61 taken 31 times.
✓ Branch 69 → 70 taken 94 times.
✓ Branch 69 → 72 taken 65 times.
✓ Branch 70 → 71 taken 94 times.
|
343 | QNetworkReply *reply = method == "PUT" ? m_nam->put(request, body) |
| 1213 |
6/6✓ Branch 63 → 64 taken 1 time.
✓ Branch 63 → 66 taken 43 times.
✓ Branch 64 → 65 taken 1 time.
✓ Branch 73 → 74 taken 16 times.
✓ Branch 73 → 76 taken 49 times.
✓ Branch 74 → 75 taken 16 times.
|
201 | : method == "DELETE" ? m_nam->deleteResource(request) |
| 1214 |
4/6✓ Branch 67 → 68 taken 3 times.
✓ Branch 67 → 70 taken 40 times.
✓ Branch 68 → 69 taken 3 times.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 80 taken 49 times.
✗ Branch 78 → 79 not taken.
|
181 | : method == "POST" ? m_nam->post(request, body) |
| 1215 |
2/12✗ Branch 71 → 72 not taken.
✓ Branch 71 → 75 taken 40 times.
✗ Branch 72 → 73 not taken.
✗ Branch 73 → 74 not taken.
✗ Branch 81 → 82 not taken.
✓ Branch 81 → 85 taken 49 times.
✗ Branch 82 → 83 not taken.
✗ Branch 83 → 84 not taken.
✗ Branch 130 → 131 not taken.
✗ Branch 130 → 132 not taken.
✗ Branch 152 → 153 not taken.
✗ Branch 152 → 154 not taken.
|
89 | : method == "OPTIONS" ? m_nam->sendCustomRequest(request, "OPTIONS") |
| 1216 |
4/6✓ Branch 75 → 76 taken 40 times.
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 82 taken 75 times.
✓ Branch 85 → 86 taken 49 times.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 92 taken 159 times.
|
323 | : m_nam->get(request); |
| 1217 |
2/2✓ Branch 82 → 83 taken 75 times.
✓ Branch 92 → 93 taken 159 times.
|
234 | DavNetworkLimits::apply(reply); |
| 1218 |
2/2✓ Branch 83 → 84 taken 75 times.
✓ Branch 93 → 94 taken 159 times.
|
234 | trackReply(reply); |
| 1219 |
2/6✓ Branch 86 → 87 taken 75 times.
✓ Branch 96 → 97 taken 159 times.
✗ Branch 140 → 141 not taken.
✗ Branch 140 → 142 not taken.
✗ Branch 162 → 163 not taken.
✗ Branch 162 → 164 not taken.
|
234 | connect(reply, &QNetworkReply::finished, this, [this, reply, id, method, resolved]() { |
| 1220 |
1/1✓ Branch 2 → 3 taken 233 times.
|
233 | reply->deleteLater(); |
| 1221 | 233 | DavResponse response; | |
| 1222 | 233 | response.request = id; | |
| 1223 | 233 | response.method = method; | |
| 1224 | 233 | response.url = resolved; | |
| 1225 |
1/1✓ Branch 5 → 6 taken 233 times.
|
233 | response.error = DavNetworkLimits::failureReason(reply); |
| 1226 |
1/2✓ Branch 9 → 10 taken 233 times.
✗ Branch 9 → 54 not taken.
|
233 | if (response.error.isEmpty()) { |
| 1227 |
2/2✓ Branch 10 → 11 taken 233 times.
✓ Branch 11 → 12 taken 233 times.
|
233 | response.status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 1228 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 18 taken 233 times.
|
233 | if (response.status == 0) |
| 1229 | ✗ | response.error = reply->errorString(); | |
| 1230 |
1/1✓ Branch 18 → 19 taken 233 times.
|
233 | response.body = reply->readAll(); |
| 1231 |
2/2✓ Branch 25 → 26 taken 233 times.
✓ Branch 26 → 27 taken 233 times.
|
466 | response.etag = QString::fromUtf8(reply->rawHeader("ETag")); |
| 1232 |
2/2✓ Branch 34 → 35 taken 233 times.
✓ Branch 35 → 36 taken 233 times.
|
466 | response.davHeader = reply->rawHeader("DAV").left(4096); |
| 1233 |
1/1✓ Branch 43 → 44 taken 233 times.
|
466 | const QByteArray location = reply->rawHeader("Location"); |
| 1234 |
1/2✗ Branch 45 → 46 not taken.
✓ Branch 45 → 52 taken 233 times.
|
233 | if (!location.isEmpty()) |
| 1235 | ✗ | response.location = resolveDavUrl(QString::fromUtf8(location)); | |
| 1236 | 233 | } | |
| 1237 |
9/9✓ Branch 54 → 55 taken 233 times.
✓ Branch 58 → 59 taken 233 times.
✓ Branch 59 → 60 taken 233 times.
✓ Branch 60 → 61 taken 233 times.
✓ Branch 61 → 62 taken 233 times.
✓ Branch 62 → 63 taken 233 times.
✓ Branch 63 → 64 taken 233 times.
✓ Branch 68 → 56 taken 233 times.
✓ Branch 68 → 69 taken 233 times.
|
466 | qCDebug(lcCalDav) << method << "resource" << response.url << "->" << response.status |
| 1238 |
1/1✓ Branch 64 → 65 taken 233 times.
|
233 | << response.error; |
| 1239 |
1/1✓ Branch 69 → 70 taken 233 times.
|
233 | emit resourceResponse(response); |
| 1240 | 233 | }); | |
| 1241 | 234 | return id; | |
| 1242 | 234 | } | |
| 1243 | |||
| 1244 | 125 | quint64 CalDavClient::putResource(const QString &url, const QByteArray &body, | |
| 1245 | const QString &ifMatch, bool createOnly) { | ||
| 1246 | 125 | QList<QPair<QByteArray, QByteArray>> headers; | |
| 1247 |
2/2✓ Branch 2 → 3 taken 23 times.
✓ Branch 2 → 11 taken 102 times.
|
125 | if (createOnly) { |
| 1248 |
4/4✓ Branch 3 → 4 taken 23 times.
✓ Branch 4 → 5 taken 23 times.
✓ Branch 5 → 6 taken 23 times.
✓ Branch 6 → 7 taken 23 times.
|
23 | headers.append(qMakePair(QByteArray("If-None-Match"), QByteArray("*"))); |
| 1249 |
2/3✓ Branch 11 → 12 taken 102 times.
✓ Branch 12 → 13 taken 102 times.
✗ Branch 12 → 21 not taken.
|
102 | } else if (isValidEtag(ifMatch)) { |
| 1250 |
4/4✓ Branch 13 → 14 taken 102 times.
✓ Branch 14 → 15 taken 102 times.
✓ Branch 15 → 16 taken 102 times.
✓ Branch 16 → 17 taken 102 times.
|
102 | headers.append(qMakePair(QByteArray("If-Match"), ifMatch.toUtf8())); |
| 1251 | } else { | ||
| 1252 | ✗ | qCWarning(lcCalDav) << "Refusing PUT without a valid entity-tag for" << url; | |
| 1253 | ✗ | return sendResourceRequest("PUT", QString(), {}, {}); | |
| 1254 | } | ||
| 1255 |
2/2✓ Branch 43 → 44 taken 125 times.
✓ Branch 44 → 45 taken 125 times.
|
125 | return sendResourceRequest("PUT", url, body, headers); |
| 1256 | 125 | } | |
| 1257 | |||
| 1258 | 17 | quint64 CalDavClient::deleteResource(const QString &url, const QString &ifMatch) { | |
| 1259 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 26 taken 17 times.
|
17 | if (!isValidEtag(ifMatch)) { |
| 1260 | ✗ | qCWarning(lcCalDav) << "Refusing DELETE without a valid entity-tag for" << url; | |
| 1261 | ✗ | return sendResourceRequest("DELETE", QString(), {}, {}); | |
| 1262 | } | ||
| 1263 |
5/7✓ Branch 27 → 28 taken 17 times.
✓ Branch 31 → 32 taken 17 times.
✓ Branch 32 → 33 taken 17 times.
✓ Branch 38 → 39 taken 17 times.
✓ Branch 38 → 40 taken 17 times.
✗ Branch 70 → 71 not taken.
✗ Branch 70 → 72 not taken.
|
51 | return sendResourceRequest("DELETE", url, {}, {qMakePair(QByteArray("If-Match"), ifMatch.toUtf8())}); |
| 1264 | 34 | } | |
| 1265 | |||
| 1266 | 89 | quint64 CalDavClient::getResource(const QString &url) { | |
| 1267 |
2/2✓ Branch 4 → 5 taken 89 times.
✓ Branch 5 → 6 taken 89 times.
|
89 | return sendResourceRequest("GET", url, {}, {}); |
| 1268 | } | ||
| 1269 | |||
| 1270 | 3 | quint64 CalDavClient::postFreeBusy(const QString &outboxUrl, const QByteArray &request) { | |
| 1271 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 4 → 5 taken 3 times.
|
3 | return sendResourceRequest("POST", outboxUrl, request, {}); |
| 1272 | } | ||
| 1273 | |||
| 1274 | ✗ | quint64 CalDavClient::options(const QString &url) { | |
| 1275 | ✗ | return sendResourceRequest("OPTIONS", url, {}, {}); | |
| 1276 | } | ||
| 1277 | |||
| 1278 | 8 | void CalDavClient::createEvent(const QString &calendarPath, | |
| 1279 | const CalendarEvent &event) { | ||
| 1280 |
1/1✓ Branch 2 → 3 taken 8 times.
|
8 | QString url = resourceUrl(calendarPath, event.uid); |
| 1281 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 12 taken 7 times.
|
8 | if (url.isEmpty()) { |
| 1282 |
1/1✓ Branch 8 → 9 taken 1 time.
|
1 | emit writeFailed(QStringLiteral("Invalid calendar path for event creation")); |
| 1283 | 1 | return; | |
| 1284 | } | ||
| 1285 | |||
| 1286 |
2/2✓ Branch 12 → 13 taken 7 times.
✓ Branch 13 → 14 taken 7 times.
|
7 | QNetworkRequest request{QUrl(url)}; |
| 1287 |
3/3✓ Branch 15 → 16 taken 7 times.
✓ Branch 16 → 17 taken 7 times.
✓ Branch 17 → 18 taken 7 times.
|
7 | request.setRawHeader("Authorization", authHeader()); |
| 1288 |
3/3✓ Branch 20 → 21 taken 7 times.
✓ Branch 21 → 22 taken 7 times.
✓ Branch 22 → 23 taken 7 times.
|
7 | request.setRawHeader("Content-Type", "text/calendar; charset=utf-8"); |
| 1289 |
3/3✓ Branch 25 → 26 taken 7 times.
✓ Branch 26 → 27 taken 7 times.
✓ Branch 27 → 28 taken 7 times.
|
7 | request.setRawHeader("If-None-Match", "*"); // Only create, don't overwrite |
| 1290 | |||
| 1291 |
1/1✓ Branch 30 → 31 taken 7 times.
|
7 | QByteArray body = eventToICalendar(event); |
| 1292 | #ifdef MAILJD_E2E_TESTING | ||
| 1293 |
1/1✓ Branch 34 → 35 taken 5 times.
|
5 | reportE2eTransportPhase(QStringLiteral("PUT_EVENT")); |
| 1294 | #endif | ||
| 1295 |
2/2✓ Branch 32 → 33 taken 2 times.
✓ Branch 38 → 39 taken 5 times.
|
7 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); // S106-058 |
| 1296 |
2/2✓ Branch 34 → 35 taken 2 times.
✓ Branch 40 → 41 taken 5 times.
|
7 | auto *reply = m_nam->put(request, body); |
| 1297 |
2/2✓ Branch 35 → 36 taken 2 times.
✓ Branch 41 → 42 taken 5 times.
|
7 | DavNetworkLimits::apply(reply); |
| 1298 |
2/2✓ Branch 36 → 37 taken 2 times.
✓ Branch 42 → 43 taken 5 times.
|
7 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 1299 | |||
| 1300 | 7 | connect(reply, &QNetworkReply::finished, this, | |
| 1301 |
2/10✓ Branch 40 → 41 taken 2 times.
✓ Branch 46 → 47 taken 5 times.
✗ Branch 84 → 85 not taken.
✗ Branch 84 → 86 not taken.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 89 not taken.
✗ Branch 96 → 97 not taken.
✗ Branch 96 → 98 not taken.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
|
14 | [this, reply, event, url, body]() { |
| 1302 |
1/1✓ Branch 2 → 3 taken 7 times.
|
7 | reply->deleteLater(); |
| 1303 |
1/1✓ Branch 3 → 4 taken 7 times.
|
7 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 1304 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 6 times.
|
7 | if (!limitError.isEmpty()) { |
| 1305 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | emit writeFailed(limitError); |
| 1306 | 1 | return; | |
| 1307 | } | ||
| 1308 |
1/1✓ Branch 8 → 9 taken 6 times.
|
6 | int status = reply->attribute( |
| 1309 |
1/1✓ Branch 9 → 10 taken 6 times.
|
6 | QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 1310 |
4/4✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 3 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 58 taken 2 times.
|
6 | if (status == 201 || status == 204) { |
| 1311 | // Update etag from response | ||
| 1312 | 4 | CalendarEvent saved = event; | |
| 1313 |
1/1✓ Branch 14 → 15 taken 4 times.
|
4 | ICalEventCodec::adoptPushedResource(&saved, body); |
| 1314 |
1/1✓ Branch 19 → 20 taken 4 times.
|
8 | QByteArray newEtag = reply->rawHeader("ETag"); |
| 1315 |
2/2✓ Branch 21 → 22 taken 3 times.
✓ Branch 21 → 26 taken 1 time.
|
4 | if (!newEtag.isEmpty()) |
| 1316 |
1/1✓ Branch 22 → 23 taken 3 times.
|
3 | saved.etag = QString::fromUtf8(newEtag); |
| 1317 |
1/1✓ Branch 30 → 31 taken 4 times.
|
8 | const QByteArray location = reply->rawHeader("Location"); |
| 1318 | 4 | const QString savedHref = location.isEmpty() | |
| 1319 | 6 | ? url | |
| 1320 |
6/8✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 34 taken 2 times.
✓ Branch 34 → 35 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 38 taken 2 times.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
|
4 | : resolveDavUrl(QString::fromUtf8(location)); |
| 1321 |
2/2✓ Branch 39 → 40 taken 3 times.
✓ Branch 39 → 41 taken 1 time.
|
4 | if (!savedHref.isEmpty()) |
| 1322 | 3 | saved.resourceHref = savedHref; | |
| 1323 |
6/6✓ Branch 41 → 42 taken 4 times.
✓ Branch 45 → 46 taken 4 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 47 → 48 taken 4 times.
✓ Branch 51 → 43 taken 4 times.
✓ Branch 51 → 52 taken 4 times.
|
8 | qCDebug(lcCalDav) << "Event created:" << saved.uid; |
| 1324 |
1/1✓ Branch 52 → 53 taken 4 times.
|
4 | emit eventSaved(saved); |
| 1325 | 4 | } else { | |
| 1326 | 4 | QString err = QStringLiteral("Create event failed (HTTP %1): %2") | |
| 1327 |
4/4✓ Branch 62 → 63 taken 2 times.
✓ Branch 64 → 65 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
✓ Branch 66 → 67 taken 2 times.
|
4 | .arg(status).arg(QString::fromUtf8(reply->readAll())); |
| 1328 | 4 | qCWarning(lcCalDav) << err; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; reply validation remains measured. | |
| 1329 |
1/1✓ Branch 82 → 83 taken 2 times.
|
2 | emit writeFailed(err); |
| 1330 | 2 | } | |
| 1331 |
2/2✓ Branch 87 → 88 taken 6 times.
✓ Branch 87 → 90 taken 1 time.
|
7 | }); |
| 1332 | 8 | } | |
| 1333 | |||
| 1334 | 8 | void CalDavClient::updateEvent(const CalendarEvent &event) { | |
| 1335 | 8 | QString url = resourceUrlForExistingResource(event.resourceHref, | |
| 1336 | 8 | event.calendarPath, | |
| 1337 |
1/1✓ Branch 2 → 3 taken 8 times.
|
8 | event.uid); |
| 1338 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 12 taken 7 times.
|
8 | if (url.isEmpty()) { |
| 1339 |
1/1✓ Branch 8 → 9 taken 1 time.
|
1 | emit writeFailed(QStringLiteral("Invalid calendar path for event update")); |
| 1340 | 1 | return; | |
| 1341 | } | ||
| 1342 | |||
| 1343 |
2/2✓ Branch 12 → 13 taken 7 times.
✓ Branch 13 → 14 taken 7 times.
|
7 | QNetworkRequest request{QUrl(url)}; |
| 1344 |
3/3✓ Branch 15 → 16 taken 7 times.
✓ Branch 16 → 17 taken 7 times.
✓ Branch 17 → 18 taken 7 times.
|
7 | request.setRawHeader("Authorization", authHeader()); |
| 1345 |
3/3✓ Branch 20 → 21 taken 7 times.
✓ Branch 21 → 22 taken 7 times.
✓ Branch 22 → 23 taken 7 times.
|
7 | request.setRawHeader("Content-Type", "text/calendar; charset=utf-8"); |
| 1346 |
3/3✓ Branch 25 → 26 taken 7 times.
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 44 taken 5 times.
|
7 | if (!isValidEtag(event.etag)) { |
| 1347 |
5/5✓ Branch 27 → 28 taken 2 times.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 32 → 33 taken 2 times.
✓ Branch 36 → 29 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
|
4 | qCWarning(lcCalDav) << "Rejecting PUT without a valid RFC entity-tag"; |
| 1348 |
1/1✓ Branch 40 → 41 taken 2 times.
|
2 | emit writeFailed(QStringLiteral("Invalid or missing server ETag — write " |
| 1349 | "refused to preserve concurrency protection")); | ||
| 1350 | 2 | return; | |
| 1351 | } | ||
| 1352 |
3/3✓ Branch 44 → 45 taken 5 times.
✓ Branch 45 → 46 taken 5 times.
✓ Branch 46 → 47 taken 5 times.
|
5 | request.setRawHeader("If-Match", event.etag.toUtf8()); |
| 1353 | |||
| 1354 |
1/1✓ Branch 49 → 50 taken 5 times.
|
5 | QByteArray body = eventToICalendar(event); |
| 1355 | #ifdef MAILJD_E2E_TESTING | ||
| 1356 |
1/1✓ Branch 53 → 54 taken 2 times.
|
2 | reportE2eTransportPhase(QStringLiteral("PUT_EVENT")); |
| 1357 | #endif | ||
| 1358 |
2/2✓ Branch 51 → 52 taken 3 times.
✓ Branch 57 → 58 taken 2 times.
|
5 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); // S106-058 |
| 1359 |
2/2✓ Branch 53 → 54 taken 3 times.
✓ Branch 59 → 60 taken 2 times.
|
5 | auto *reply = m_nam->put(request, body); |
| 1360 |
2/2✓ Branch 54 → 55 taken 3 times.
✓ Branch 60 → 61 taken 2 times.
|
5 | DavNetworkLimits::apply(reply); |
| 1361 |
2/2✓ Branch 55 → 56 taken 3 times.
✓ Branch 61 → 62 taken 2 times.
|
5 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 1362 | |||
| 1363 | 5 | connect(reply, &QNetworkReply::finished, this, | |
| 1364 |
2/6✓ Branch 58 → 59 taken 3 times.
✓ Branch 64 → 65 taken 2 times.
✗ Branch 116 → 117 not taken.
✗ Branch 116 → 118 not taken.
✗ Branch 128 → 129 not taken.
✗ Branch 128 → 130 not taken.
|
10 | [this, reply, event, body]() { |
| 1365 |
1/1✓ Branch 2 → 3 taken 5 times.
|
5 | reply->deleteLater(); |
| 1366 |
1/1✓ Branch 3 → 4 taken 5 times.
|
5 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 1367 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 4 times.
|
5 | if (!limitError.isEmpty()) { |
| 1368 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | emit writeFailed(limitError); |
| 1369 | 1 | return; | |
| 1370 | } | ||
| 1371 |
1/1✓ Branch 8 → 9 taken 4 times.
|
4 | int status = reply->attribute( |
| 1372 |
1/1✓ Branch 9 → 10 taken 4 times.
|
4 | QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 1373 |
4/4✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 41 taken 2 times.
|
4 | if (status == 200 || status == 204) { |
| 1374 | 2 | CalendarEvent saved = event; | |
| 1375 |
1/1✓ Branch 14 → 15 taken 2 times.
|
2 | ICalEventCodec::adoptPushedResource(&saved, body); |
| 1376 |
1/1✓ Branch 19 → 20 taken 2 times.
|
4 | QByteArray newEtag = reply->rawHeader("ETag"); |
| 1377 |
1/2✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 26 not taken.
|
2 | if (!newEtag.isEmpty()) |
| 1378 |
1/1✓ Branch 22 → 23 taken 2 times.
|
2 | saved.etag = QString::fromUtf8(newEtag); |
| 1379 |
6/6✓ Branch 26 → 27 taken 2 times.
✓ Branch 30 → 31 taken 2 times.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 32 → 33 taken 2 times.
✓ Branch 36 → 28 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
|
4 | qCDebug(lcCalDav) << "Event updated:" << saved.uid; |
| 1380 |
1/1✓ Branch 37 → 38 taken 2 times.
|
2 | emit eventSaved(saved); |
| 1381 |
2/2✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 49 taken 1 time.
|
4 | } else if (status == 412) { |
| 1382 |
1/1✓ Branch 45 → 46 taken 1 time.
|
2 | emit writeFailed(QStringLiteral( |
| 1383 | "Conflict: event was modified on server (ETag mismatch)")); | ||
| 1384 | } else { | ||
| 1385 | 2 | QString err = QStringLiteral("Update event failed (HTTP %1): %2") | |
| 1386 |
4/4✓ Branch 53 → 54 taken 1 time.
✓ Branch 55 → 56 taken 1 time.
✓ Branch 56 → 57 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
|
2 | .arg(status).arg(QString::fromUtf8(reply->readAll())); |
| 1387 |
5/5✓ Branch 63 → 64 taken 1 time.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 68 → 69 taken 1 time.
✓ Branch 72 → 65 taken 1 time.
✓ Branch 72 → 73 taken 1 time.
|
2 | qCWarning(lcCalDav) << err; |
| 1388 |
1/1✓ Branch 73 → 74 taken 1 time.
|
1 | emit writeFailed(err); |
| 1389 | 1 | } | |
| 1390 |
2/2✓ Branch 78 → 79 taken 4 times.
✓ Branch 78 → 81 taken 1 time.
|
5 | }); |
| 1391 | 10 | } | |
| 1392 | |||
| 1393 | 1 | void CalDavClient::setCalendarColor(const QString &calendarPath, const QString &color) { | |
| 1394 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | const QString url = resolveDavUrl(calendarPath); |
| 1395 |
3/7✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 16 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 16 not taken.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 79 → 80 not taken.
✗ Branch 79 → 81 not taken.
|
2 | static const QRegularExpression rgb(QStringLiteral("^#[0-9A-Fa-f]{6}$")); |
| 1396 |
6/12✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 22 not taken.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 26 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 34 taken 1 time.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
|
2 | if (url.isEmpty() || !rgb.match(color).hasMatch()) { |
| 1397 | ✗ | emit writeFailed(QStringLiteral("Invalid calendar color request")); | |
| 1398 | ✗ | return; | |
| 1399 | } | ||
| 1400 |
2/2✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
|
1 | QNetworkRequest request{QUrl(url)}; |
| 1401 |
3/3✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
|
1 | request.setRawHeader("Authorization", authHeader()); |
| 1402 |
3/3✓ Branch 42 → 43 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
|
1 | request.setRawHeader("Content-Type", "application/xml; charset=utf-8"); |
| 1403 | // Apple's form #RRGGBBAA; Nextcloud, Radicale and iCloud accept it. | ||
| 1404 | const QByteArray body = | ||
| 1405 | "<?xml version=\"1.0\" encoding=\"utf-8\"?>" | ||
| 1406 | "<d:propertyupdate xmlns:d=\"DAV:\" xmlns:apple=\"http://apple.com/ns/ical/\">" | ||
| 1407 |
3/3✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
|
2 | "<d:set><d:prop><apple:calendar-color>" + color.toUpper().toLatin1() + "FF" |
| 1408 |
1/1✓ Branch 50 → 51 taken 1 time.
|
1 | "</apple:calendar-color></d:prop></d:set></d:propertyupdate>"; |
| 1409 |
2/2✓ Branch 54 → 55 taken 1 time.
✓ Branch 55 → 56 taken 1 time.
|
1 | auto *reply = m_nam->sendCustomRequest(request, "PROPPATCH", body); |
| 1410 |
1/1✓ Branch 57 → 58 taken 1 time.
|
1 | DavNetworkLimits::apply(reply); |
| 1411 |
1/1✓ Branch 58 → 59 taken 1 time.
|
1 | trackReply(reply); |
| 1412 |
1/1✓ Branch 60 → 61 taken 1 time.
|
1 | connect(reply, &QNetworkReply::finished, this, [this, reply, calendarPath]() { |
| 1413 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | reply->deleteLater(); |
| 1414 |
1/1✓ Branch 3 → 4 taken 1 time.
|
1 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 1415 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
|
1 | const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 1416 |
2/3✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
✓ Branch 9 → 11 taken 1 time.
|
1 | const QByteArray answer = limitError.isEmpty() ? reply->readAll() : QByteArray(); |
| 1417 | // 207: the property's own propstat decides (403 = not allowed). | ||
| 1418 |
2/4✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 23 not taken.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 22 not taken.
|
2 | const bool saved = limitError.isEmpty() && |
| 1419 |
4/8✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 22 not taken.
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 23 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 23 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 23 not taken.
|
1 | (status == 200 || status == 204 || (status == 207 && answer.contains(" 200 ") && !answer.contains(" 403 "))); |
| 1420 |
1/2✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 37 not taken.
|
1 | if (saved) { |
| 1421 |
6/6✓ Branch 25 → 26 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 35 → 27 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
|
2 | qCInfo(lcCalDav) << "Calendar color saved for" << calendarPath; |
| 1422 |
1/1✓ Branch 36 → 62 taken 1 time.
|
1 | emit calendarColorSaved(calendarPath); |
| 1423 | } else { | ||
| 1424 | ✗ | const QString error = !limitError.isEmpty() ? limitError | |
| 1425 | ✗ | : QStringLiteral("Calendar color not saved (HTTP %1)").arg(status); | |
| 1426 | ✗ | qCWarning(lcCalDav) << error; | |
| 1427 | ✗ | emit writeFailed(error); | |
| 1428 | ✗ | } | |
| 1429 | 1 | }); | |
| 1430 | 1 | } | |
| 1431 | |||
| 1432 | 6 | void CalDavClient::deleteEvent(const CalendarEvent &event) { | |
| 1433 | 6 | QString url = resourceUrlForExistingResource(event.resourceHref, | |
| 1434 | 6 | event.calendarPath, | |
| 1435 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | event.uid); |
| 1436 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 12 taken 5 times.
|
6 | if (url.isEmpty()) { |
| 1437 |
1/1✓ Branch 8 → 9 taken 1 time.
|
1 | emit writeFailed(QStringLiteral("Invalid calendar path for event deletion")); |
| 1438 | 1 | return; | |
| 1439 | } | ||
| 1440 | |||
| 1441 |
2/2✓ Branch 12 → 13 taken 5 times.
✓ Branch 13 → 14 taken 5 times.
|
5 | QNetworkRequest request{QUrl(url)}; |
| 1442 |
3/3✓ Branch 15 → 16 taken 5 times.
✓ Branch 16 → 17 taken 5 times.
✓ Branch 17 → 18 taken 5 times.
|
5 | request.setRawHeader("Authorization", authHeader()); |
| 1443 |
2/3✓ Branch 20 → 21 taken 5 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 39 taken 5 times.
|
5 | if (!isValidEtag(event.etag)) { |
| 1444 | ✗ | qCWarning(lcCalDav) << "Rejecting DELETE without a valid RFC entity-tag"; | |
| 1445 | ✗ | emit writeFailed(QStringLiteral("Invalid or missing server ETag — delete " | |
| 1446 | "refused to preserve concurrency protection")); | ||
| 1447 | ✗ | return; | |
| 1448 | } | ||
| 1449 |
3/3✓ Branch 39 → 40 taken 5 times.
✓ Branch 40 → 41 taken 5 times.
✓ Branch 41 → 42 taken 5 times.
|
5 | request.setRawHeader("If-Match", event.etag.toUtf8()); |
| 1450 | |||
| 1451 | #ifdef MAILJD_E2E_TESTING | ||
| 1452 |
1/1✓ Branch 47 → 48 taken 3 times.
|
3 | reportE2eTransportPhase(QStringLiteral("DELETE_EVENT")); |
| 1453 | #endif | ||
| 1454 |
2/2✓ Branch 45 → 46 taken 2 times.
✓ Branch 51 → 52 taken 3 times.
|
5 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); // S106-058 |
| 1455 |
2/2✓ Branch 47 → 48 taken 2 times.
✓ Branch 53 → 54 taken 3 times.
|
5 | auto *reply = m_nam->deleteResource(request); |
| 1456 |
2/2✓ Branch 48 → 49 taken 2 times.
✓ Branch 54 → 55 taken 3 times.
|
5 | DavNetworkLimits::apply(reply); |
| 1457 |
2/2✓ Branch 49 → 50 taken 2 times.
✓ Branch 55 → 56 taken 3 times.
|
5 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 1458 | |||
| 1459 |
2/2✓ Branch 51 → 52 taken 2 times.
✓ Branch 57 → 58 taken 3 times.
|
5 | connect(reply, &QNetworkReply::finished, this, |
| 1460 | 10 | [this, reply, uid = event.uid]() { | |
| 1461 |
1/1✓ Branch 2 → 3 taken 5 times.
|
5 | reply->deleteLater(); |
| 1462 |
1/1✓ Branch 3 → 4 taken 5 times.
|
5 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 1463 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 4 times.
|
5 | if (!limitError.isEmpty()) { |
| 1464 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | emit writeFailed(limitError); |
| 1465 | 1 | return; | |
| 1466 | } | ||
| 1467 |
1/1✓ Branch 8 → 9 taken 4 times.
|
4 | int status = reply->attribute( |
| 1468 |
1/1✓ Branch 9 → 10 taken 4 times.
|
4 | QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 1469 |
4/4✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 26 taken 2 times.
|
4 | if (status == 200 || status == 204) { |
| 1470 |
6/6✓ Branch 13 → 14 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 23 → 15 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
|
4 | qCDebug(lcCalDav) << "Event deleted:" << uid; |
| 1471 |
1/1✓ Branch 24 → 25 taken 2 times.
|
2 | emit eventDeleted(uid); |
| 1472 |
2/2✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 34 taken 1 time.
|
4 | } else if (status == 412) { |
| 1473 |
1/1✓ Branch 30 → 31 taken 1 time.
|
2 | emit writeFailed(QStringLiteral( |
| 1474 | "Conflict: event was modified on server (ETag mismatch)")); | ||
| 1475 | } else { | ||
| 1476 | 2 | QString err = QStringLiteral("Delete event failed (HTTP %1)") | |
| 1477 |
1/1✓ Branch 38 → 39 taken 1 time.
|
1 | .arg(status); |
| 1478 |
5/5✓ Branch 41 → 42 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
✓ Branch 50 → 43 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
|
2 | qCWarning(lcCalDav) << err; |
| 1479 |
1/1✓ Branch 51 → 52 taken 1 time.
|
1 | emit writeFailed(err); |
| 1480 | 1 | } | |
| 1481 |
2/2✓ Branch 56 → 57 taken 4 times.
✓ Branch 56 → 59 taken 1 time.
|
5 | }); |
| 1482 | 6 | } | |
| 1483 | |||
| 1484 | 7 | void CalDavClient::createTask(const QString &calendarPath, | |
| 1485 | const CalendarTask &task) { | ||
| 1486 |
1/1✓ Branch 2 → 3 taken 7 times.
|
7 | QString url = resourceUrl(calendarPath, task.uid); |
| 1487 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 12 taken 6 times.
|
7 | if (url.isEmpty()) { |
| 1488 |
1/1✓ Branch 8 → 9 taken 1 time.
|
1 | emit writeFailed(QStringLiteral("Invalid calendar path for task creation")); |
| 1489 | 1 | return; | |
| 1490 | } | ||
| 1491 | |||
| 1492 |
2/2✓ Branch 12 → 13 taken 6 times.
✓ Branch 13 → 14 taken 6 times.
|
6 | QNetworkRequest request{QUrl(url)}; |
| 1493 |
3/3✓ Branch 15 → 16 taken 6 times.
✓ Branch 16 → 17 taken 6 times.
✓ Branch 17 → 18 taken 6 times.
|
6 | request.setRawHeader("Authorization", authHeader()); |
| 1494 |
3/3✓ Branch 20 → 21 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
|
6 | request.setRawHeader("Content-Type", "text/calendar; charset=utf-8"); |
| 1495 |
3/3✓ Branch 25 → 26 taken 6 times.
✓ Branch 26 → 27 taken 6 times.
✓ Branch 27 → 28 taken 6 times.
|
6 | request.setRawHeader("If-None-Match", "*"); |
| 1496 | |||
| 1497 |
1/1✓ Branch 30 → 31 taken 6 times.
|
6 | QByteArray body = taskToICalendar(task); |
| 1498 | #ifdef MAILJD_E2E_TESTING | ||
| 1499 |
1/1✓ Branch 34 → 35 taken 4 times.
|
4 | reportE2eTransportPhase(QStringLiteral("PUT_TASK")); |
| 1500 | #endif | ||
| 1501 |
2/2✓ Branch 32 → 33 taken 2 times.
✓ Branch 38 → 39 taken 4 times.
|
6 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); // S106-058 |
| 1502 |
2/2✓ Branch 34 → 35 taken 2 times.
✓ Branch 40 → 41 taken 4 times.
|
6 | auto *reply = m_nam->put(request, body); |
| 1503 |
2/2✓ Branch 35 → 36 taken 2 times.
✓ Branch 41 → 42 taken 4 times.
|
6 | DavNetworkLimits::apply(reply); |
| 1504 |
2/2✓ Branch 36 → 37 taken 2 times.
✓ Branch 42 → 43 taken 4 times.
|
6 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 1505 | |||
| 1506 | 6 | connect(reply, &QNetworkReply::finished, this, | |
| 1507 |
2/6✓ Branch 39 → 40 taken 2 times.
✓ Branch 45 → 46 taken 4 times.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
|
12 | [this, reply, task, url]() { |
| 1508 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | reply->deleteLater(); |
| 1509 |
1/1✓ Branch 3 → 4 taken 6 times.
|
6 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 1510 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 5 times.
|
6 | if (!limitError.isEmpty()) { |
| 1511 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | emit writeFailed(limitError); |
| 1512 | 1 | return; | |
| 1513 | } | ||
| 1514 |
1/1✓ Branch 8 → 9 taken 5 times.
|
5 | int status = reply->attribute( |
| 1515 |
1/1✓ Branch 9 → 10 taken 5 times.
|
5 | QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 1516 |
4/4✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 2 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 57 taken 2 times.
|
5 | if (status == 201 || status == 204) { |
| 1517 | 3 | CalendarTask saved = task; | |
| 1518 |
1/1✓ Branch 18 → 19 taken 3 times.
|
6 | QByteArray newEtag = reply->rawHeader("ETag"); |
| 1519 |
2/2✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 25 taken 1 time.
|
3 | if (!newEtag.isEmpty()) |
| 1520 |
1/1✓ Branch 21 → 22 taken 2 times.
|
2 | saved.etag = QString::fromUtf8(newEtag); |
| 1521 |
1/1✓ Branch 29 → 30 taken 3 times.
|
6 | const QByteArray location = reply->rawHeader("Location"); |
| 1522 | 3 | const QString savedHref = location.isEmpty() | |
| 1523 | 4 | ? url | |
| 1524 |
6/8✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 2 times.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 34 → 35 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 37 taken 1 time.
✗ Branch 97 → 98 not taken.
✗ Branch 97 → 99 not taken.
|
3 | : resolveDavUrl(QString::fromUtf8(location)); |
| 1525 |
2/2✓ Branch 38 → 39 taken 2 times.
✓ Branch 38 → 40 taken 1 time.
|
3 | if (!savedHref.isEmpty()) |
| 1526 | 2 | saved.resourceHref = savedHref; | |
| 1527 |
6/6✓ Branch 40 → 41 taken 3 times.
✓ Branch 44 → 45 taken 3 times.
✓ Branch 45 → 46 taken 3 times.
✓ Branch 46 → 47 taken 3 times.
✓ Branch 50 → 42 taken 3 times.
✓ Branch 50 → 51 taken 3 times.
|
6 | qCDebug(lcCalDav) << "Task created:" << saved.uid; |
| 1528 |
1/1✓ Branch 51 → 52 taken 3 times.
|
3 | emit taskSaved(saved); |
| 1529 | 3 | } else { | |
| 1530 | 4 | QString err = QStringLiteral("Create task failed (HTTP %1): %2") | |
| 1531 |
4/4✓ Branch 61 → 62 taken 2 times.
✓ Branch 63 → 64 taken 2 times.
✓ Branch 64 → 65 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
|
4 | .arg(status).arg(QString::fromUtf8(reply->readAll())); |
| 1532 |
5/5✓ Branch 71 → 72 taken 2 times.
✓ Branch 75 → 76 taken 2 times.
✓ Branch 76 → 77 taken 2 times.
✓ Branch 80 → 73 taken 2 times.
✓ Branch 80 → 81 taken 2 times.
|
4 | qCWarning(lcCalDav) << err; |
| 1533 |
1/1✓ Branch 81 → 82 taken 2 times.
|
2 | emit writeFailed(err); |
| 1534 | 2 | } | |
| 1535 |
2/2✓ Branch 86 → 87 taken 5 times.
✓ Branch 86 → 89 taken 1 time.
|
6 | }); |
| 1536 | 7 | } | |
| 1537 | |||
| 1538 | 6 | void CalDavClient::updateTask(const CalendarTask &task) { | |
| 1539 | 6 | QString url = resourceUrlForExistingResource(task.resourceHref, | |
| 1540 | 6 | task.calendarPath, | |
| 1541 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | task.uid); |
| 1542 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 12 taken 5 times.
|
6 | if (url.isEmpty()) { |
| 1543 |
1/1✓ Branch 8 → 9 taken 1 time.
|
1 | emit writeFailed(QStringLiteral("Invalid calendar path for task update")); |
| 1544 | 1 | return; | |
| 1545 | } | ||
| 1546 | |||
| 1547 |
2/2✓ Branch 12 → 13 taken 5 times.
✓ Branch 13 → 14 taken 5 times.
|
5 | QNetworkRequest request{QUrl(url)}; |
| 1548 |
3/3✓ Branch 15 → 16 taken 5 times.
✓ Branch 16 → 17 taken 5 times.
✓ Branch 17 → 18 taken 5 times.
|
5 | request.setRawHeader("Authorization", authHeader()); |
| 1549 |
3/3✓ Branch 20 → 21 taken 5 times.
✓ Branch 21 → 22 taken 5 times.
✓ Branch 22 → 23 taken 5 times.
|
5 | request.setRawHeader("Content-Type", "text/calendar; charset=utf-8"); |
| 1550 |
2/3✓ Branch 25 → 26 taken 5 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 44 taken 5 times.
|
5 | if (!isValidEtag(task.etag)) { |
| 1551 | ✗ | qCWarning(lcCalDav) << "Rejecting task PUT without a valid RFC entity-tag"; | |
| 1552 | ✗ | emit writeFailed(QStringLiteral("Invalid or missing server ETag — write " | |
| 1553 | "refused to preserve concurrency protection")); | ||
| 1554 | ✗ | return; | |
| 1555 | } | ||
| 1556 |
3/3✓ Branch 44 → 45 taken 5 times.
✓ Branch 45 → 46 taken 5 times.
✓ Branch 46 → 47 taken 5 times.
|
5 | request.setRawHeader("If-Match", task.etag.toUtf8()); |
| 1557 | |||
| 1558 |
1/1✓ Branch 49 → 50 taken 5 times.
|
5 | QByteArray body = taskToICalendar(task); |
| 1559 | #ifdef MAILJD_E2E_TESTING | ||
| 1560 |
1/1✓ Branch 53 → 54 taken 3 times.
|
3 | reportE2eTransportPhase(QStringLiteral("PUT_TASK")); |
| 1561 | #endif | ||
| 1562 |
2/2✓ Branch 51 → 52 taken 2 times.
✓ Branch 57 → 58 taken 3 times.
|
5 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); // S106-058 |
| 1563 |
2/2✓ Branch 53 → 54 taken 2 times.
✓ Branch 59 → 60 taken 3 times.
|
5 | auto *reply = m_nam->put(request, body); |
| 1564 |
2/2✓ Branch 54 → 55 taken 2 times.
✓ Branch 60 → 61 taken 3 times.
|
5 | DavNetworkLimits::apply(reply); |
| 1565 |
2/2✓ Branch 55 → 56 taken 2 times.
✓ Branch 61 → 62 taken 3 times.
|
5 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 1566 | |||
| 1567 |
2/2✓ Branch 57 → 58 taken 2 times.
✓ Branch 63 → 64 taken 3 times.
|
5 | connect(reply, &QNetworkReply::finished, this, |
| 1568 | 10 | [this, reply, task]() { | |
| 1569 |
1/1✓ Branch 2 → 3 taken 5 times.
|
5 | reply->deleteLater(); |
| 1570 |
1/1✓ Branch 3 → 4 taken 5 times.
|
5 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 1571 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 4 times.
|
5 | if (!limitError.isEmpty()) { |
| 1572 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | emit writeFailed(limitError); |
| 1573 | 1 | return; | |
| 1574 | } | ||
| 1575 |
1/1✓ Branch 8 → 9 taken 4 times.
|
4 | int status = reply->attribute( |
| 1576 |
1/1✓ Branch 9 → 10 taken 4 times.
|
4 | QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 1577 |
4/4✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 1 time.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 40 taken 1 time.
|
4 | if (status == 200 || status == 204) { |
| 1578 | 3 | CalendarTask saved = task; | |
| 1579 |
1/1✓ Branch 18 → 19 taken 3 times.
|
6 | QByteArray newEtag = reply->rawHeader("ETag"); |
| 1580 |
2/2✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 25 taken 1 time.
|
3 | if (!newEtag.isEmpty()) |
| 1581 |
1/1✓ Branch 21 → 22 taken 2 times.
|
2 | saved.etag = QString::fromUtf8(newEtag); |
| 1582 |
6/6✓ Branch 25 → 26 taken 3 times.
✓ Branch 29 → 30 taken 3 times.
✓ Branch 30 → 31 taken 3 times.
✓ Branch 31 → 32 taken 3 times.
✓ Branch 35 → 27 taken 3 times.
✓ Branch 35 → 36 taken 3 times.
|
6 | qCDebug(lcCalDav) << "Task updated:" << saved.uid; |
| 1583 |
1/1✓ Branch 36 → 37 taken 3 times.
|
3 | emit taskSaved(saved); |
| 1584 |
1/2✓ Branch 40 → 41 taken 1 time.
✗ Branch 40 → 48 not taken.
|
4 | } else if (status == 412) { |
| 1585 |
1/1✓ Branch 44 → 45 taken 1 time.
|
2 | emit writeFailed(QStringLiteral( |
| 1586 | "Conflict: task was modified on server (ETag mismatch)")); | ||
| 1587 | } else { | ||
| 1588 | ✗ | QString err = QStringLiteral("Update task failed (HTTP %1): %2") | |
| 1589 | ✗ | .arg(status).arg(QString::fromUtf8(reply->readAll())); | |
| 1590 | ✗ | qCWarning(lcCalDav) << err; | |
| 1591 | ✗ | emit writeFailed(err); | |
| 1592 | ✗ | } | |
| 1593 |
2/2✓ Branch 77 → 78 taken 4 times.
✓ Branch 77 → 80 taken 1 time.
|
5 | }); |
| 1594 | 6 | } | |
| 1595 | |||
| 1596 | 6 | void CalDavClient::deleteTask(const CalendarTask &task) { | |
| 1597 | 6 | QString url = resourceUrlForExistingResource(task.resourceHref, | |
| 1598 | 6 | task.calendarPath, | |
| 1599 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | task.uid); |
| 1600 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 12 taken 5 times.
|
6 | if (url.isEmpty()) { |
| 1601 |
1/1✓ Branch 8 → 9 taken 1 time.
|
1 | emit writeFailed(QStringLiteral("Invalid calendar path for task deletion")); |
| 1602 | 1 | return; | |
| 1603 | } | ||
| 1604 | |||
| 1605 |
2/2✓ Branch 12 → 13 taken 5 times.
✓ Branch 13 → 14 taken 5 times.
|
5 | QNetworkRequest request{QUrl(url)}; |
| 1606 |
3/3✓ Branch 15 → 16 taken 5 times.
✓ Branch 16 → 17 taken 5 times.
✓ Branch 17 → 18 taken 5 times.
|
5 | request.setRawHeader("Authorization", authHeader()); |
| 1607 |
2/3✓ Branch 20 → 21 taken 5 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 39 taken 5 times.
|
5 | if (!isValidEtag(task.etag)) { |
| 1608 | ✗ | qCWarning(lcCalDav) << "Rejecting task DELETE without a valid RFC entity-tag"; | |
| 1609 | ✗ | emit writeFailed(QStringLiteral("Invalid or missing server ETag — delete " | |
| 1610 | "refused to preserve concurrency protection")); | ||
| 1611 | ✗ | return; | |
| 1612 | } | ||
| 1613 |
3/3✓ Branch 39 → 40 taken 5 times.
✓ Branch 40 → 41 taken 5 times.
✓ Branch 41 → 42 taken 5 times.
|
5 | request.setRawHeader("If-Match", task.etag.toUtf8()); |
| 1614 | |||
| 1615 | #ifdef MAILJD_E2E_TESTING | ||
| 1616 |
1/1✓ Branch 47 → 48 taken 3 times.
|
3 | reportE2eTransportPhase(QStringLiteral("DELETE_TASK")); |
| 1617 | #endif | ||
| 1618 |
2/2✓ Branch 45 → 46 taken 2 times.
✓ Branch 51 → 52 taken 3 times.
|
5 | request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); // S106-058 |
| 1619 |
2/2✓ Branch 47 → 48 taken 2 times.
✓ Branch 53 → 54 taken 3 times.
|
5 | auto *reply = m_nam->deleteResource(request); |
| 1620 |
2/2✓ Branch 48 → 49 taken 2 times.
✓ Branch 54 → 55 taken 3 times.
|
5 | DavNetworkLimits::apply(reply); |
| 1621 |
2/2✓ Branch 49 → 50 taken 2 times.
✓ Branch 55 → 56 taken 3 times.
|
5 | trackReply(reply); // T-79.F1: lifetime follows request completion |
| 1622 | |||
| 1623 |
2/2✓ Branch 51 → 52 taken 2 times.
✓ Branch 57 → 58 taken 3 times.
|
5 | connect(reply, &QNetworkReply::finished, this, |
| 1624 | 10 | [this, reply, uid = task.uid]() { | |
| 1625 |
1/1✓ Branch 2 → 3 taken 5 times.
|
5 | reply->deleteLater(); |
| 1626 |
1/1✓ Branch 3 → 4 taken 5 times.
|
5 | const QString limitError = DavNetworkLimits::failureReason(reply); |
| 1627 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 4 times.
|
5 | if (!limitError.isEmpty()) { |
| 1628 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | emit writeFailed(limitError); |
| 1629 | 1 | return; | |
| 1630 | } | ||
| 1631 |
1/1✓ Branch 8 → 9 taken 4 times.
|
4 | int status = reply->attribute( |
| 1632 |
1/1✓ Branch 9 → 10 taken 4 times.
|
4 | QNetworkRequest::HttpStatusCodeAttribute).toInt(); |
| 1633 |
4/4✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 26 taken 2 times.
|
4 | if (status == 200 || status == 204) { |
| 1634 |
6/6✓ Branch 13 → 14 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 23 → 15 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
|
4 | qCDebug(lcCalDav) << "Task deleted:" << uid; |
| 1635 |
1/1✓ Branch 24 → 25 taken 2 times.
|
2 | emit taskDeleted(uid); |
| 1636 |
2/2✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 34 taken 1 time.
|
4 | } else if (status == 412) { |
| 1637 |
1/1✓ Branch 30 → 31 taken 1 time.
|
2 | emit writeFailed(QStringLiteral( |
| 1638 | "Conflict: task was modified on server (ETag mismatch)")); | ||
| 1639 | } else { | ||
| 1640 | 2 | QString err = QStringLiteral("Delete task failed (HTTP %1)") | |
| 1641 |
1/1✓ Branch 38 → 39 taken 1 time.
|
1 | .arg(status); |
| 1642 |
5/5✓ Branch 41 → 42 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
✓ Branch 50 → 43 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
|
2 | qCWarning(lcCalDav) << err; |
| 1643 |
1/1✓ Branch 51 → 52 taken 1 time.
|
1 | emit writeFailed(err); |
| 1644 | 1 | } | |
| 1645 |
2/2✓ Branch 56 → 57 taken 4 times.
✓ Branch 56 → 59 taken 1 time.
|
5 | }); |
| 1646 | 6 | } | |
| 1647 |