MailJDTourDownloadDocsQualityTestsCoverage

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

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 95.8% 457 / 0 / 477
Functions: 97.4% 37 / 0 / 38
Branches: 84.8% 579 / 0 / 683

app/core/DavPushAccount.cpp
Line Branch Exec Source
1 #include "DavPushAccount.h"
2
3 #include <utility>
4
5 #include "DavPushStatistics.h"
6 #include "service/DavPushXml.h"
7 #include "service/WebPushPayloadCodec.h"
8 #include "util/SecureUtil.h"
9
10 #include <QCryptographicHash>
11 #include <QLoggingCategory>
12 #include <QRandomGenerator>
13 #include <QUuid>
14
15
4/7
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 8 taken 92 times.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 3 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
95 Q_LOGGING_CATEGORY(lcDavPush, "mailjd.davpush")
16
17 namespace {
18 constexpr int kMinRetrySeconds = 60;
19 constexpr int kMaxRetrySeconds = 3600;
20
21 6 int retryDelay(int failures, int retryAfter) {
22 6 const qint64 exponential = qint64(kMinRetrySeconds) << qMin(failures, 6);
23 6 return int(qMax<qint64>(retryAfter, qMin<qint64>(exponential, kMaxRetrySeconds)));
24 }
25 } // namespace
26
27 100 DavPushAccount::DavPushAccount(const QString &accountId, Context context, QObject *parent)
28
1/1
✓ Branch 18 → 19 taken 100 times.
100 : QObject(parent), m_accountId(accountId), m_ctx(std::move(context)) {
29 100 m_monotonic.start();
30
1/1
✓ Branch 23 → 24 taken 100 times.
100 m_renewal.setSingleShot(true);
31
1/1
✓ Branch 24 → 25 taken 100 times.
100 connect(&m_renewal, &QTimer::timeout, this, &DavPushAccount::converge);
32
1/1
✓ Branch 26 → 27 taken 100 times.
100 m_receiverRecord = m_ctx.store->receiver(accountId);
33 100 }
34
35 200 DavPushAccount::~DavPushAccount() {
36 100 stopReceiver();
37 200 }
38
39 68 QString DavPushAccount::collectionScope(const QString &path) const {
40 68 return DavPushStatistics::collectionScope(m_accountId, path);
41 }
42
43 191 std::optional<qint64> DavPushAccount::pendingPushSince(const QString &path) const {
44
2/2
✓ Branch 3 → 4 taken 190 times.
✓ Branch 3 → 7 taken 1 time.
191 if (!m_pushSince.contains(path)) return std::nullopt;
45 1 return m_monotonic.elapsed() - m_pushSince.value(path);
46 }
47
48 167 void DavPushAccount::apply(const Target &target) {
49 167 const bool receiverChanged = !(target.receiver == m_target.receiver);
50 167 m_target = target;
51
2/2
✓ Branch 4 → 5 taken 43 times.
✓ Branch 4 → 8 taken 124 times.
167 if (receiverChanged) {
52 43 ++m_generation;
53 43 m_secretLoading = false;
54 43 m_tokenLoaded = false;
55 // S106-073: the subscriber token belongs to the previous service. A new
56 // service configured without a token must not receive it as a bearer.
57 43 SecureUtil::zeroMemory(m_providerToken);
58 43 m_providerToken.clear();
59 43 stopReceiver(); // restarted with the new service/token below
60 }
61
6/6
✓ Branch 8 → 9 taken 92 times.
✓ Branch 8 → 11 taken 75 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 91 times.
✓ Branch 13 → 14 taken 76 times.
✓ Branch 13 → 16 taken 91 times.
167 if (!target.active || !target.receiver.isConfigured()) {
62 76 pause();
63 76 return;
64 }
65
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 30 taken 91 times.
91 if (!WebPushPayloadCodec::available(&m_receiverProblem)) {
66 ✗ m_receiverProblem = QStringLiteral("cryptography unavailable: %1").arg(m_receiverProblem);
67 ✗ stopReceiver();
68 ✗ emit changed();
69 ✗ return;
70 }
71 91 m_receiverProblem.clear();
72
9/11
✓ Branch 32 → 33 taken 38 times.
✓ Branch 32 → 37 taken 53 times.
✓ Branch 33 → 34 taken 38 times.
✓ Branch 35 → 36 taken 36 times.
✓ Branch 35 → 37 taken 2 times.
✓ Branch 38 → 39 taken 38 times.
✓ Branch 38 → 40 taken 53 times.
✓ Branch 40 → 41 taken 36 times.
✓ Branch 40 → 44 taken 55 times.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
91 if (target.desiredPaths.isEmpty() && m_ctx.store->collections(m_accountId).isEmpty()) {
73 36 stopReceiver();
74 36 emit changed();
75 36 return;
76 }
77 55 ensureReceiver();
78 }
79
80 246 void DavPushAccount::pause() {
81 246 stopReceiver();
82 246 m_renewal.stop();
83 246 m_nextRenewalAt = 0;
84 246 emit changed();
85 246 }
86
87 1 void DavPushAccount::forget() {
88 1 ++m_generation;
89 1 stopReceiver();
90 1 m_renewal.stop();
91
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 14 not taken.
1 if (m_receiverRecord) {
92 1 releaseCurrentReceiver();
93
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
1 m_ctx.secrets->remove(this, DavPushSecretKeys::receiver(m_ctx.installationId, m_receiverRecord->receiverId),
94 ✗ [](bool) {});
95 1 m_ctx.store->removeReceiver(m_accountId);
96 }
97
6/6
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 22 → 18 taken 3 times.
✓ Branch 22 → 23 taken 1 time.
4 for (const auto &record : m_ctx.store->collections(m_accountId)) m_ctx.store->removeCollection(m_accountId, record.path);
98 1 m_ctx.store->removeCleanupForAccount(m_accountId);
99 1 m_receiverRecord.reset();
100 1 m_secret.reset();
101 1 emit changed();
102 1 }
103
104 1 void DavPushAccount::reconnect() {
105
6/6
✓ Branch 2 → 3 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 12 → 4 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
2 qCInfo(lcDavPush) << "Reconnect push for" << m_accountId;
106 1 ++m_generation;
107 1 stopReceiver();
108 // Renew every own registration (same endpoint: the server updates them).
109
4/5
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 26 → 18 not taken.
✓ Branch 26 → 27 taken 1 time.
1 for (auto record : m_ctx.store->collections(m_accountId)) {
110 ✗ record.nextRetryAt = 0;
111 ✗ record.failures = 0;
112 ✗ record.expiresAt = qMin(record.expiresAt, m_ctx.now());
113 ✗ m_ctx.store->saveCollection(record);
114 1 }
115 1 m_reportedFailure = false;
116 1 apply(m_target);
117 1 }
118
119 1 void DavPushAccount::reloadToken() {
120 1 ++m_generation;
121 1 m_secretLoading = false;
122 1 m_tokenLoaded = false;
123 1 m_providerToken.clear();
124 1 stopReceiver();
125 1 }
126
127 // --- receiver -------------------------------------------------------------
128
129 92 void DavPushAccount::ensureReceiver() {
130
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 92 times.
181 if (m_secretLoading) return;
131 92 const auto &wanted = m_target.receiver;
132
7/8
✓ Branch 5 → 6 taken 64 times.
✓ Branch 5 → 13 taken 28 times.
✓ Branch 8 → 9 taken 60 times.
✓ Branch 8 → 12 taken 4 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 60 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 31 taken 88 times.
92 if (m_receiverRecord && (m_receiverRecord->server != wanted.server || m_receiverRecord->provider != wanted.provider)) {
133
6/6
✓ Branch 15 → 16 taken 4 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 20 → 21 taken 4 times.
✓ Branch 21 → 22 taken 4 times.
✓ Branch 25 → 17 taken 4 times.
✓ Branch 25 → 26 taken 4 times.
8 qCInfo(lcDavPush) << "Push service changed, preparing a replacement receiver for" << m_accountId;
134
1/1
✓ Branch 26 → 27 taken 4 times.
4 stopReceiver();
135
1/1
✓ Branch 27 → 28 taken 4 times.
4 releaseCurrentReceiver();
136 4 m_secret.reset();
137
1/1
✓ Branch 29 → 30 taken 4 times.
4 createReceiver();
138 4 return;
139 }
140
2/2
✓ Branch 32 → 33 taken 28 times.
✓ Branch 32 → 35 taken 60 times.
88 if (!m_receiverRecord) {
141
1/1
✓ Branch 33 → 34 taken 28 times.
28 createReceiver();
142 28 return;
143 }
144
8/8
✓ Branch 36 → 37 taken 57 times.
✓ Branch 36 → 40 taken 3 times.
✓ Branch 37 → 38 taken 6 times.
✓ Branch 37 → 40 taken 51 times.
✓ Branch 38 → 39 taken 3 times.
✓ Branch 38 → 40 taken 3 times.
✓ Branch 41 → 42 taken 3 times.
✓ Branch 41 → 48 taken 57 times.
60 if (m_secret && wanted.hasToken && !m_tokenLoaded) {
145 // Optional subscriber token of the push service (never the DAV password).
146 3 m_secretLoading = true;
147 3 const quint64 generation = m_generation;
148
2/2
✓ Branch 43 → 44 taken 3 times.
✓ Branch 44 → 45 taken 3 times.
3 m_ctx.secrets->read(this, DavPushSecretKeys::providerToken(m_ctx.installationId),
149 3 [this, generation](bool found, QByteArray value) {
150 3 m_secretLoading = false;
151
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 3 times.
3 if (generation != m_generation) return;
152 3 m_tokenLoaded = true;
153
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 1 time.
3 m_providerToken = found ? value : QByteArray();
154 3 SecureUtil::zeroMemory(value);
155 3 ensureReceiver();
156 });
157 3 return;
158 }
159
2/2
✓ Branch 49 → 50 taken 54 times.
✓ Branch 49 → 56 taken 3 times.
57 if (m_secret) {
160 // S106-074: without a wanted collection the stream has no purpose; only
161 // the cleanup of the remaining registrations runs (it needs no stream).
162
2/2
✓ Branch 51 → 52 taken 2 times.
✓ Branch 51 → 53 taken 52 times.
54 if (m_target.desiredPaths.isEmpty())
163
1/1
✓ Branch 52 → 54 taken 2 times.
2 stopReceiver();
164 else
165
1/1
✓ Branch 53 → 54 taken 52 times.
52 startReceiver();
166
1/1
✓ Branch 54 → 55 taken 54 times.
54 converge();
167 54 return;
168 }
169 3 m_secretLoading = true;
170 3 const quint64 generation = m_generation;
171
1/1
✓ Branch 57 → 58 taken 3 times.
3 const QString key = DavPushSecretKeys::receiver(m_ctx.installationId, m_receiverRecord->receiverId);
172
1/1
✓ Branch 59 → 60 taken 3 times.
3 m_ctx.secrets->read(this, key, [this, generation](bool found, QByteArray value) {
173 3 m_secretLoading = false;
174
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 3 times.
3 if (generation != m_generation) return;
175
3/3
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 1 time.
✓ Branch 5 → 7 taken 2 times.
3 m_secret = found ? DavPushReceiverSecret::parse(value) : std::nullopt;
176 3 SecureUtil::zeroMemory(value);
177
6/6
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 16 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 33 taken 2 times.
3 if (m_secret && !m_secret->hasProviderChannel()) {
178 // Channel of an earlier build that ntfy servers with subscriber-based
179 // rate limiting refuse (507): a new endpoint, registered anew.
180
7/7
✓ Branch 18 → 19 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 29 → 20 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
2 qCInfo(lcDavPush) << "Receiver channel of an earlier format for" << m_accountId << "- replacing the receiver";
181 1 m_secret.reset();
182 1 createReceiver();
183 1 return;
184 }
185
2/2
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 49 taken 1 time.
2 if (!m_secret) {
186 // Lost or unreadable keyring entry: a new endpoint and new keys —
187 // never renew the old registration with keys it does not know.
188
7/7
✓ Branch 35 → 36 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
✓ Branch 46 → 37 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
2 qCWarning(lcDavPush) << "Receiver secret missing for" << m_accountId << "- replacing the receiver";
189 1 createReceiver();
190 1 return;
191 }
192 1 ensureReceiver();
193 });
194 3 }
195
196 34 void DavPushAccount::createReceiver() {
197 34 QString error;
198
1/1
✓ Branch 2 → 3 taken 34 times.
34 const auto keys = WebPushPayloadCodec::generateKeys(&error);
199
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 34 times.
34 if (!keys) {
200 ✗ m_receiverProblem = error;
201 ✗ emit changed();
202 ✗ return;
203 }
204 34 DavPushReceiverSecret secret;
205
1/1
✓ Branch 8 → 9 taken 34 times.
34 secret.channel = DavPushReceiverSecret::newChannel();
206 34 secret.keys = *keys;
207 34 DavPushReceiverRecord record;
208 34 record.accountId = m_accountId;
209
2/2
✓ Branch 14 → 15 taken 34 times.
✓ Branch 15 → 16 taken 34 times.
34 record.receiverId = QUuid::createUuid().toString(QUuid::WithoutBraces);
210 34 record.provider = m_target.receiver.provider;
211 34 record.server = m_target.receiver.server;
212
1/1
✓ Branch 20 → 21 taken 34 times.
34 record.createdAt = m_ctx.now();
213
2/2
✓ Branch 22 → 23 taken 6 times.
✓ Branch 22 → 25 taken 28 times.
34 record.generation = m_receiverRecord ? m_receiverRecord->generation + 1 : 1;
214
1/1
✓ Branch 26 → 27 taken 34 times.
34 const QString key = DavPushSecretKeys::receiver(m_ctx.installationId, record.receiverId);
215 34 m_secretLoading = true;
216 34 const quint64 generation = ++m_generation;
217 // Durable before use: the secret is in the keyring before any POST names
218 // its endpoint (§3.3).
219
3/5
✓ Branch 29 → 30 taken 34 times.
✓ Branch 30 → 31 taken 34 times.
✓ Branch 31 → 32 taken 34 times.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
34 m_ctx.secrets->write(this, key, secret.serialize(), [this, generation, record, secret](bool ok) {
220 34 m_secretLoading = false;
221
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 34 times.
35 if (generation != m_generation) return;
222
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 21 taken 33 times.
34 if (!ok) {
223
1/1
✓ Branch 5 → 6 taken 1 time.
1 m_receiverProblem = tr("The keyring is not available; push stays off.");
224
6/6
✓ Branch 8 → 9 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 10 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
2 qCWarning(lcDavPush) << "Cannot store the receiver secret for" << m_accountId;
225
1/1
✓ Branch 19 → 20 taken 1 time.
1 emit changed();
226 1 return;
227 }
228
1/1
✓ Branch 21 → 22 taken 33 times.
33 const auto previous = m_receiverRecord;
229
2/3
✓ Branch 22 → 23 taken 33 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 29 taken 33 times.
33 if (!m_ctx.store->saveReceiver(record)) {
230 ✗ m_receiverProblem = tr("The push state could not be saved; push stays off.");
231 ✗ emit changed();
232 ✗ return;
233 }
234
2/2
✓ Branch 30 → 31 taken 6 times.
✓ Branch 30 → 38 taken 27 times.
33 if (previous)
235
2/2
✓ Branch 33 → 34 taken 6 times.
✓ Branch 34 → 35 taken 6 times.
6 m_ctx.secrets->remove(this, DavPushSecretKeys::receiver(m_ctx.installationId, previous->receiverId),
236 6 [](bool) {});
237 33 m_receiverRecord = record;
238 33 m_secret = secret;
239
2/2
✓ Branch 44 → 45 taken 33 times.
✓ Branch 45 → 46 taken 33 times.
66 m_ctx.stats->event(DavPushStatistics::accountScope(m_accountId), QStringLiteral("receiver-created"));
240
1/1
✓ Branch 50 → 51 taken 33 times.
33 ensureReceiver();
241
1/2
✓ Branch 53 → 54 taken 33 times.
✗ Branch 53 → 56 not taken.
33 });
242 34 }
243
244 5 void DavPushAccount::releaseCurrentReceiver() {
245 // Needs the loaded secret (the distributor knows the registration by its
246 // token); without it the distributor's entry stays until it expires there.
247
4/8
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 9 not taken.
✓ Branch 7 → 8 taken 5 times.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 15 not taken.
5 if (m_ctx.releaseReceiver && m_receiverRecord && m_secret)
248 5 m_ctx.releaseReceiver(m_receiverRecord->provider, m_receiverRecord->server, m_secret->channel);
249 5 }
250
251 130 QString DavPushAccount::endpointHash() const {
252
2/3
✓ Branch 3 → 4 taken 130 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 7 taken 130 times.
130 const QString endpoint = m_receiver ? m_receiver->endpoint() : QString();
253
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 130 times.
130 if (endpoint.isEmpty()) return {};
254
5/5
✓ Branch 10 → 11 taken 130 times.
✓ Branch 12 → 13 taken 130 times.
✓ Branch 13 → 14 taken 130 times.
✓ Branch 14 → 15 taken 130 times.
✓ Branch 15 → 16 taken 130 times.
130 return QString::fromLatin1(QCryptographicHash::hash(endpoint.toUtf8(), QCryptographicHash::Sha256).toHex().left(32));
255 130 }
256
257 52 void DavPushAccount::startReceiver() {
258
5/6
✓ Branch 3 → 4 taken 38 times.
✓ Branch 3 → 6 taken 14 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 38 times.
✓ Branch 8 → 9 taken 14 times.
✓ Branch 8 → 10 taken 38 times.
52 if (m_receiver || !m_secret) return;
259
2/2
✓ Branch 11 → 12 taken 38 times.
✓ Branch 12 → 13 taken 38 times.
38 m_receiver = m_ctx.makeReceiver(m_receiverRecord->provider, this);
260
1/1
✓ Branch 14 → 15 taken 38 times.
38 connect(m_receiver, &PushReceiver::messageReceived, this, &DavPushAccount::onMessage);
261
1/1
✓ Branch 17 → 18 taken 38 times.
38 connect(m_receiver, &PushReceiver::stateChanged, this, &DavPushAccount::onReceiverState);
262
1/1
✓ Branch 20 → 21 taken 38 times.
38 connect(m_receiver, &PushReceiver::bytesReceived, this, [this](qint64 bytes) {
263 ✗ m_ctx.stats->count(DavPushStatistics::accountScope(m_accountId), QStringLiteral("provider.bytes"), bytes);
264 ✗ });
265
1/1
✓ Branch 23 → 24 taken 38 times.
38 connect(m_receiver, &PushReceiver::streamResumed, this, [this](bool afterGap) {
266
2/2
✓ Branch 6 → 7 taken 10 times.
✓ Branch 7 → 8 taken 10 times.
20 m_ctx.stats->event(DavPushStatistics::accountScope(m_accountId), QStringLiteral("receiver-connected"));
267 // S106-082: the first stream of this process catches up too — the replay
268 // cursor is not persisted, so pushes sent while MailJD was not running
269 // are lost, and with a sync interval of 0 no startup sync covers them.
270 10 const bool firstStream = !std::exchange(m_streamSeen, true);
271
4/4
✓ Branch 13 → 14 taken 9 times.
✓ Branch 13 → 16 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 8 times.
10 if (!afterGap && !firstStream) return;
272 // Messages may have been lost while the stream was down.
273 9 QStringList registered;
274
5/5
✓ Branch 16 → 17 taken 9 times.
✓ Branch 17 → 18 taken 9 times.
✓ Branch 18 → 19 taken 9 times.
✓ Branch 34 → 20 taken 26 times.
✓ Branch 34 → 35 taken 9 times.
35 for (const auto &record : m_ctx.store->collections(m_accountId))
275
7/8
✓ Branch 22 → 23 taken 16 times.
✓ Branch 22 → 29 taken 10 times.
✓ Branch 24 → 25 taken 16 times.
✗ Branch 24 → 29 not taken.
✓ Branch 25 → 26 taken 14 times.
✓ Branch 25 → 28 taken 2 times.
✓ Branch 30 → 31 taken 3 times.
✓ Branch 30 → 32 taken 23 times.
40 if (!record.registrationUrl.isEmpty() && m_target.desiredPaths.contains(record.path) &&
276
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 29 taken 13 times.
14 (afterGap || !m_caughtUpThisProcess.contains(record.path)))
277
1/1
✓ Branch 31 → 32 taken 3 times.
12 registered << record.path;
278
3/3
✓ Branch 37 → 38 taken 2 times.
✓ Branch 37 → 39 taken 7 times.
✓ Branch 38 → 39 taken 2 times.
9 if (!registered.isEmpty()) m_ctx.requestSync(m_accountId, registered, DavSyncReasonRecovery);
279 9 });
280 // The DAV server's VAPID key (one per server) for distributors that ask.
281 38 QString vapid;
282
5/5
✓ Branch 25 → 26 taken 38 times.
✓ Branch 26 → 27 taken 38 times.
✓ Branch 27 → 28 taken 38 times.
✓ Branch 38 → 29 taken 105 times.
✓ Branch 38 → 39 taken 37 times.
142 for (const auto &record : m_ctx.store->collections(m_accountId))
283
2/2
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 36 taken 104 times.
105 if (!record.offer.vapidPublicKey.isEmpty()) {
284
1/1
✓ Branch 32 → 33 taken 1 time.
1 vapid = WebPushPayloadCodec::encodeBase64Url(record.offer.vapidPublicKey);
285 1 break;
286 38 }
287 // Only a service configured with a token gets one (S106-073).
288
2/2
✓ Branch 40 → 41 taken 3 times.
✓ Branch 40 → 42 taken 35 times.
38 const QByteArray token = m_target.receiver.hasToken ? m_providerToken : QByteArray();
289 76 m_receiver->start({m_receiverRecord->server, m_secret->channel, token, {}, vapid});
290 76 }
291
292 434 void DavPushAccount::stopReceiver() {
293
2/2
✓ Branch 3 → 4 taken 396 times.
✓ Branch 3 → 5 taken 38 times.
434 if (!m_receiver) return;
294 38 PushReceiver *receiver = m_receiver;
295 38 m_receiver = nullptr;
296 38 receiver->disconnect(this);
297 38 receiver->stop();
298 38 receiver->deleteLater();
299 }
300
301 50 void DavPushAccount::onReceiverState() {
302
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 50 times.
50 if (!m_receiver) return;
303 50 const auto state = m_receiver->state();
304
1/1
✓ Branch 7 → 8 taken 50 times.
50 const QString scope = DavPushStatistics::accountScope(m_accountId);
305
2/2
✓ Branch 8 → 9 taken 11 times.
✓ Branch 8 → 13 taken 39 times.
50 if (state == DavPushReceiverState::Connected) {
306
3/3
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 9 times.
✓ Branch 10 → 11 taken 2 times.
11 if (m_reportedFailure) m_ctx.pushHealth(m_accountId, false);
307 11 m_reportedFailure = false;
308 // A (new) endpoint is known: register or re-register what needs it.
309
1/1
✓ Branch 11 → 12 taken 11 times.
11 QMetaObject::invokeMethod(this, &DavPushAccount::converge, Qt::QueuedConnection);
310
4/6
✓ Branch 13 → 14 taken 37 times.
✓ Branch 13 → 15 taken 2 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 32 taken 37 times.
✓ Branch 15 → 16 taken 2 times.
✗ Branch 15 → 32 not taken.
39 } else if ((state == DavPushReceiverState::Backoff || state == DavPushReceiverState::Failed) && !m_reportedFailure) {
311 2 m_reportedFailure = true;
312
1/1
✓ Branch 19 → 20 taken 2 times.
4 m_ctx.stats->count(scope, QStringLiteral("receiver.failures"));
313
1/1
✓ Branch 27 → 28 taken 2 times.
4 m_ctx.stats->event(scope, QStringLiteral("receiver-failed"), m_receiver->failureDetail());
314
1/1
✓ Branch 31 → 32 taken 2 times.
2 m_ctx.pushHealth(m_accountId, true);
315 }
316
3/3
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 40 taken 48 times.
✓ Branch 36 → 37 taken 2 times.
52 if (state == DavPushReceiverState::Backoff) m_ctx.stats->count(scope, QStringLiteral("receiver.reconnects"));
317
1/1
✓ Branch 40 → 41 taken 50 times.
50 emit changed();
318 50 }
319
320 // --- registrations ----------------------------------------------------------
321
322 130 bool DavPushAccount::needsRegistration(const DavPushCollectionRecord &record) const {
323
6/6
✓ Branch 3 → 4 taken 74 times.
✓ Branch 3 → 7 taken 56 times.
✓ Branch 6 → 7 taken 12 times.
✓ Branch 6 → 8 taken 62 times.
✓ Branch 9 → 10 taken 68 times.
✓ Branch 9 → 11 taken 62 times.
130 if (record.registrationUrl.isEmpty() || record.registrationReceiver != m_receiverRecord->receiverId) return true;
324 // The push service handed out a new endpoint (UnifiedPush distributors).
325
1/1
✓ Branch 11 → 12 taken 62 times.
62 const QString current = endpointHash();
326
6/8
✓ Branch 13 → 14 taken 62 times.
✗ Branch 13 → 19 not taken.
✓ Branch 15 → 16 taken 62 times.
✗ Branch 15 → 19 not taken.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 60 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 60 times.
62 if (!record.registrationEndpoint.isEmpty() && !current.isEmpty() && record.registrationEndpoint != current) return true;
327
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 60 times.
60 const qint64 base = record.renewedAt > 0 ? record.renewedAt : record.registeredAt;
328 60 const qint64 lifetime = qMax<qint64>(0, record.expiresAt - base);
329 // 75 % of the confirmed lifetime; never later than 10 min before expiry.
330 60 const qint64 renewAt = qMin(base + lifetime * 3 / 4, record.expiresAt - 600);
331
1/1
✓ Branch 27 → 28 taken 60 times.
60 return m_ctx.now() >= renewAt;
332 62 }
333
334 74 void DavPushAccount::converge() {
335
4/8
✓ Branch 2 → 3 taken 74 times.
✗ Branch 2 → 7 not taken.
✓ Branch 4 → 5 taken 74 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 74 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 74 times.
74 if (!m_target.active || !m_receiverRecord || !m_secret) return;
336 // Registrations need the endpoint; UnifiedPush delivers it asynchronously
337 // (converge runs again when the receiver reports Connected).
338
7/9
✓ Branch 12 → 13 taken 72 times.
✓ Branch 12 → 18 taken 2 times.
✓ Branch 14 → 15 taken 72 times.
✓ Branch 16 → 17 taken 67 times.
✓ Branch 16 → 18 taken 5 times.
✓ Branch 19 → 20 taken 72 times.
✓ Branch 19 → 21 taken 2 times.
✗ Branch 139 → 140 not taken.
✗ Branch 139 → 141 not taken.
74 const bool endpointKnown = m_receiver && !m_receiver->endpoint().isEmpty();
339
1/1
✓ Branch 21 → 22 taken 74 times.
74 const qint64 now = m_ctx.now();
340 74 QStringList toRegister;
341 74 QList<QPair<QString, QString>> toRemove; // path, registration URL
342
2/2
✓ Branch 53 → 25 taken 141 times.
✓ Branch 53 → 54 taken 74 times.
215 for (const QString &path : std::as_const(m_target.desiredPaths)) {
343
1/1
✓ Branch 26 → 27 taken 141 times.
141 auto record = m_ctx.store->collection(m_accountId, path);
344
6/8
✓ Branch 28 → 29 taken 141 times.
✗ Branch 28 → 33 not taken.
✓ Branch 30 → 31 taken 137 times.
✓ Branch 30 → 33 taken 4 times.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 137 times.
✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 37 taken 137 times.
141 if (!record || m_inFlight.contains(path) || record->nextRetryAt > now) continue;
345
8/8
✓ Branch 37 → 38 taken 130 times.
✓ Branch 37 → 42 taken 7 times.
✓ Branch 39 → 40 taken 130 times.
✓ Branch 40 → 41 taken 72 times.
✓ Branch 40 → 42 taken 58 times.
✓ Branch 43 → 44 taken 72 times.
✓ Branch 43 → 45 taken 65 times.
✓ Branch 44 → 45 taken 72 times.
137 if (endpointKnown && needsRegistration(*record)) toRegister << path;
346 141 }
347
5/5
✓ Branch 54 → 55 taken 74 times.
✓ Branch 55 → 56 taken 74 times.
✓ Branch 56 → 57 taken 74 times.
✓ Branch 74 → 58 taken 216 times.
✓ Branch 74 → 75 taken 74 times.
290 for (const auto &record : m_ctx.store->collections(m_accountId))
348
6/6
✓ Branch 60 → 61 taken 80 times.
✓ Branch 60 → 66 taken 136 times.
✓ Branch 62 → 63 taken 4 times.
✓ Branch 62 → 66 taken 76 times.
✓ Branch 67 → 68 taken 4 times.
✓ Branch 67 → 72 taken 212 times.
220 if (!record.registrationUrl.isEmpty() && !m_target.desiredPaths.contains(record.path) &&
349
1/2
✓ Branch 64 → 65 taken 4 times.
✗ Branch 64 → 66 not taken.
4 !m_unregistering.contains(record.path))
350
1/1
✓ Branch 69 → 70 taken 4 times.
78 toRemove.append({record.path, record.registrationUrl});
351 74 QList<DavPushCleanupEntry> cleanup;
352
5/5
✓ Branch 76 → 77 taken 74 times.
✓ Branch 77 → 78 taken 74 times.
✓ Branch 78 → 79 taken 74 times.
✓ Branch 86 → 80 taken 26 times.
✓ Branch 86 → 87 taken 74 times.
100 for (const auto &entry : m_ctx.store->cleanupEntries(m_accountId))
353
3/3
✓ Branch 82 → 83 taken 12 times.
✓ Branch 82 → 84 taken 14 times.
✓ Branch 83 → 84 taken 12 times.
100 if (!m_cleanupInFlight.contains(entry.id)) cleanup.append(entry);
354 // Marked as in flight at once: a second converge() while the credentials
355 // are still being read must not queue the same request again.
356
3/3
✓ Branch 92 → 93 taken 72 times.
✓ Branch 95 → 91 taken 72 times.
✓ Branch 95 → 96 taken 74 times.
146 for (const QString &path : std::as_const(toRegister)) m_inFlight.insert(path);
357
3/3
✓ Branch 102 → 103 taken 4 times.
✓ Branch 105 → 99 taken 4 times.
✓ Branch 105 → 106 taken 74 times.
78 for (const auto &[path, url] : std::as_const(toRemove)) m_unregistering.insert(path);
358
3/3
✓ Branch 110 → 111 taken 12 times.
✓ Branch 113 → 109 taken 12 times.
✓ Branch 113 → 114 taken 74 times.
86 for (const auto &entry : std::as_const(cleanup)) m_cleanupInFlight.insert(entry.id);
359
8/8
✓ Branch 115 → 116 taken 36 times.
✓ Branch 115 → 120 taken 38 times.
✓ Branch 117 → 118 taken 32 times.
✓ Branch 117 → 120 taken 4 times.
✓ Branch 119 → 120 taken 6 times.
✓ Branch 119 → 121 taken 26 times.
✓ Branch 122 → 123 taken 48 times.
✓ Branch 122 → 131 taken 26 times.
74 if (!toRegister.isEmpty() || !toRemove.isEmpty() || !cleanup.isEmpty()) {
360
2/6
✓ Branch 126 → 127 taken 48 times.
✓ Branch 127 → 128 taken 48 times.
✗ Branch 166 → 167 not taken.
✗ Branch 166 → 168 not taken.
✗ Branch 169 → 170 not taken.
✗ Branch 169 → 171 not taken.
48 withClient([this, toRegister, toRemove, cleanup](DavPushClient *client) {
361
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 28 taken 45 times.
48 if (!client) {
362 // No credentials: nothing was sent; retry with the next converge.
363
3/3
✓ Branch 6 → 7 taken 4 times.
✓ Branch 9 → 5 taken 4 times.
✓ Branch 9 → 10 taken 3 times.
7 for (const QString &path : toRegister) m_inFlight.remove(path);
364
1/3
✗ Branch 15 → 16 not taken.
✗ Branch 18 → 12 not taken.
✓ Branch 18 → 19 taken 3 times.
3 for (const auto &[path, url] : toRemove) m_unregistering.remove(path);
365
1/3
✗ Branch 22 → 23 not taken.
✗ Branch 25 → 21 not taken.
✓ Branch 25 → 26 taken 3 times.
3 for (const auto &entry : cleanup) m_cleanupInFlight.remove(entry.id);
366 3 emit changed();
367 3 return;
368 }
369
3/3
✓ Branch 31 → 32 taken 68 times.
✓ Branch 34 → 30 taken 68 times.
✓ Branch 34 → 35 taken 45 times.
113 for (const QString &path : toRegister) registerPath(client, path);
370
3/3
✓ Branch 40 → 41 taken 4 times.
✓ Branch 43 → 37 taken 4 times.
✓ Branch 43 → 44 taken 45 times.
49 for (const auto &[path, url] : toRemove) unregisterUrl(client, path, url, 0);
371
3/3
✓ Branch 48 → 49 taken 12 times.
✓ Branch 52 → 46 taken 12 times.
✓ Branch 52 → 53 taken 45 times.
57 for (const auto &entry : cleanup) unregisterUrl(client, QString(), entry.registrationUrl, entry.id);
372 });
373 }
374
1/1
✓ Branch 131 → 132 taken 74 times.
74 scheduleRenewal();
375
1/1
✓ Branch 132 → 133 taken 74 times.
74 emit changed();
376 74 }
377
378 48 void DavPushAccount::withClient(std::function<void(DavPushClient *client)> work) {
379
1/1
✓ Branch 3 → 4 taken 48 times.
48 m_clientWork.append(std::move(work));
380
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 48 times.
48 if (m_clientPending) return;
381 48 m_clientPending = true;
382 48 const quint64 generation = m_generation;
383 48 const CalendarSyncAccount account = m_target.account;
384
2/2
✓ Branch 8 → 9 taken 48 times.
✓ Branch 9 → 10 taken 48 times.
48 m_ctx.resolveCredentials(this, account.accountId, account.serverUrl, account.username,
385 96 [this, generation, account](QByteArray password, const QString &error) {
386 48 m_clientPending = false;
387 48 const auto work = std::exchange(m_clientWork, {});
388
5/6
✓ Branch 5 → 6 taken 48 times.
✗ Branch 5 → 8 not taken.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 45 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 35 taken 45 times.
48 if (generation != m_generation || password.isEmpty()) {
389
8/9
✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 26 not taken.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
✓ Branch 18 → 19 taken 3 times.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 24 → 15 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
6 if (password.isEmpty()) qCInfo(lcDavPush) << "No DAV credentials for push requests of" << m_accountId << error;
390
1/1
✓ Branch 26 → 27 taken 3 times.
3 SecureUtil::zeroMemory(password);
391
3/3
✓ Branch 30 → 31 taken 3 times.
✓ Branch 33 → 29 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
6 for (const auto &item : work) item(nullptr);
392 3 return;
393 }
394
2/2
✓ Branch 35 → 36 taken 45 times.
✓ Branch 36 → 37 taken 45 times.
45 auto *client = m_ctx.makeClient(account.serverUrl, account.username, QString::fromUtf8(password));
395
1/1
✓ Branch 38 → 39 taken 45 times.
45 SecureUtil::zeroMemory(password);
396
1/1
✓ Branch 39 → 40 taken 45 times.
45 client->setParent(this);
397 // Released after its requests (DAV timeouts bound them to 30 s).
398
1/1
✓ Branch 40 → 41 taken 45 times.
45 QTimer::singleShot(120000, client, &QObject::deleteLater);
399
1/1
✓ Branch 41 → 42 taken 45 times.
45 connect(client, &DavPushClient::registered, this, [this, generation](const DavPushClient::Result &result) {
400 66 onRegistered(result, generation);
401 66 });
402
3/3
✓ Branch 46 → 47 taken 45 times.
✓ Branch 49 → 45 taken 45 times.
✓ Branch 49 → 50 taken 45 times.
90 for (const auto &item : work) item(client);
403
2/2
✓ Branch 52 → 53 taken 45 times.
✓ Branch 52 → 55 taken 3 times.
48 });
404 48 }
405
406 68 void DavPushAccount::registerPath(DavPushClient *client, const QString &path) {
407
1/1
✓ Branch 2 → 3 taken 68 times.
68 const auto record = m_ctx.store->collection(m_accountId, path);
408
1/1
✓ Branch 3 → 4 taken 68 times.
68 const QString hash = endpointHash();
409
5/6
✓ Branch 5 → 6 taken 68 times.
✗ Branch 5 → 20 not taken.
✓ Branch 8 → 9 taken 14 times.
✓ Branch 8 → 20 taken 54 times.
✓ Branch 21 → 22 taken 12 times.
✓ Branch 21 → 24 taken 56 times.
82 if (record && !record->registrationUrl.isEmpty() &&
410
2/2
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 19 taken 10 times.
14 (record->registrationReceiver != m_receiverRecord->receiverId ||
411
3/4
✓ Branch 15 → 16 taken 4 times.
✗ Branch 15 → 20 not taken.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 2 times.
4 (!record->registrationEndpoint.isEmpty() && record->registrationEndpoint != hash)))
412
1/1
✓ Branch 23 → 24 taken 12 times.
12 m_replaced.insert(path, record->registrationUrl);
413
2/3
✓ Branch 25 → 26 taken 68 times.
✗ Branch 25 → 28 not taken.
✓ Branch 27 → 29 taken 68 times.
68 const QString endpoint = m_receiver ? m_receiver->endpoint() : QString();
414
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 33 taken 68 times.
68 if (endpoint.isEmpty()) {
415 ✗ m_inFlight.remove(path);
416 ✗ return;
417 }
418
1/1
✓ Branch 33 → 34 taken 68 times.
68 m_registeringEndpoint.insert(path, hash);
419
1/1
✓ Branch 36 → 37 taken 68 times.
68 client->registerSubscription(path, endpoint, m_secret->keys.publicKey, m_secret->keys.authSecret);
420
1/1
✓ Branch 37 → 38 taken 68 times.
68 emit changed();
421 68 }
422
423 66 void DavPushAccount::onRegistered(const DavPushClient::Result &result, quint64 generation) {
424 66 const QString path = result.collectionPath;
425
1/1
✓ Branch 3 → 4 taken 66 times.
66 m_inFlight.remove(path);
426
1/1
✓ Branch 4 → 5 taken 66 times.
66 const QString registeredEndpoint = m_registeringEndpoint.take(path);
427
1/1
✓ Branch 5 → 6 taken 66 times.
66 auto record = m_ctx.store->collection(m_accountId, path);
428
5/6
✓ Branch 6 → 7 taken 64 times.
✓ Branch 6 → 9 taken 2 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 64 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 14 taken 64 times.
66 if (generation != m_generation || !record) {
429
1/1
✓ Branch 12 → 13 taken 2 times.
2 emit changed();
430 2 return;
431 }
432
1/1
✓ Branch 14 → 15 taken 64 times.
64 const QString scope = collectionScope(path);
433
4/4
✓ Branch 17 → 18 taken 8 times.
✓ Branch 17 → 21 taken 56 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 6 times.
64 const bool renewal = record->registrationUrl == result.location && !result.location.isEmpty();
434
2/2
✓ Branch 22 → 23 taken 58 times.
✓ Branch 22 → 123 taken 6 times.
64 if (result.outcome == DavPushClient::Outcome::Ok) {
435
1/1
✓ Branch 23 → 24 taken 58 times.
58 const qint64 now = m_ctx.now();
436
4/4
✓ Branch 26 → 27 taken 12 times.
✓ Branch 26 → 29 taken 46 times.
✓ Branch 28 → 29 taken 10 times.
✓ Branch 28 → 30 taken 2 times.
58 const bool first = record->registrationUrl.isEmpty() || m_replaced.contains(path);
437 58 record->registrationUrl = result.location;
438
1/2
✓ Branch 34 → 35 taken 58 times.
✗ Branch 34 → 37 not taken.
58 record->registrationReceiver = m_receiverRecord ? m_receiverRecord->receiverId : QString();
439 58 record->registrationEndpoint = registeredEndpoint;
440 58 record->expiresAt = result.expiresAt;
441
2/2
✓ Branch 44 → 45 taken 2 times.
✓ Branch 44 → 47 taken 56 times.
58 if (renewal) {
442 2 record->renewedAt = now;
443 } else {
444 56 record->registeredAt = now;
445 56 record->renewedAt = 0;
446 }
447 58 record->failures = 0;
448 58 record->nextRetryAt = 0;
449 58 record->lastError.clear();
450
1/1
✓ Branch 55 → 56 taken 58 times.
58 m_ctx.store->saveCollection(*record);
451
7/11
✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 60 taken 56 times.
✓ Branch 63 → 64 taken 58 times.
✓ Branch 65 → 66 taken 56 times.
✓ Branch 65 → 67 taken 2 times.
✓ Branch 67 → 68 taken 2 times.
✓ Branch 67 → 69 taken 56 times.
✗ Branch 171 → 172 not taken.
✗ Branch 171 → 173 not taken.
✗ Branch 175 → 176 not taken.
✗ Branch 175 → 177 not taken.
116 m_ctx.stats->count(scope, renewal ? QStringLiteral("subscription.renewed") : QStringLiteral("subscription.registered"));
452
7/11
✓ Branch 70 → 71 taken 2 times.
✓ Branch 70 → 74 taken 56 times.
✓ Branch 77 → 78 taken 58 times.
✓ Branch 79 → 80 taken 56 times.
✓ Branch 79 → 81 taken 2 times.
✓ Branch 81 → 82 taken 2 times.
✓ Branch 81 → 83 taken 56 times.
✗ Branch 182 → 183 not taken.
✗ Branch 182 → 184 not taken.
✗ Branch 186 → 187 not taken.
✗ Branch 186 → 188 not taken.
116 m_ctx.stats->event(scope, renewal ? QStringLiteral("renewed") : QStringLiteral("registered"));
453
10/10
✓ Branch 84 → 85 taken 58 times.
✓ Branch 88 → 89 taken 58 times.
✓ Branch 89 → 90 taken 2 times.
✓ Branch 89 → 91 taken 56 times.
✓ Branch 92 → 93 taken 58 times.
✓ Branch 93 → 94 taken 58 times.
✓ Branch 94 → 95 taken 58 times.
✓ Branch 95 → 96 taken 58 times.
✓ Branch 99 → 86 taken 58 times.
✓ Branch 99 → 100 taken 58 times.
116 qCInfo(lcDavPush) << (renewal ? "Renewed" : "Registered") << "push for" << m_accountId << path;
454 // The replaced registration goes once the new one is confirmed.
455
2/2
✓ Branch 101 → 102 taken 10 times.
✓ Branch 101 → 110 taken 48 times.
58 if (m_replaced.contains(path)) {
456
1/1
✓ Branch 102 → 103 taken 10 times.
10 const QString old = m_replaced.take(path);
457
1/2
✓ Branch 104 → 105 taken 10 times.
✗ Branch 104 → 108 not taken.
10 if (old != result.location) {
458
1/1
✓ Branch 105 → 106 taken 10 times.
10 m_ctx.store->queueCleanup(m_accountId, old, now);
459
1/1
✓ Branch 106 → 107 taken 10 times.
10 QMetaObject::invokeMethod(this, &DavPushAccount::converge, Qt::QueuedConnection);
460 }
461 10 }
462 // Close the gap between the last sync and the subscription.
463
5/7
✓ Branch 110 → 111 taken 56 times.
✓ Branch 110 → 119 taken 2 times.
✓ Branch 113 → 114 taken 56 times.
✓ Branch 116 → 117 taken 56 times.
✓ Branch 116 → 118 taken 56 times.
✗ Branch 206 → 207 not taken.
✗ Branch 206 → 208 not taken.
170 if (first) m_ctx.requestSync(m_accountId, {path}, DavSyncReasonInitial);
464
1/1
✓ Branch 119 → 120 taken 58 times.
58 m_caughtUpThisProcess.insert(path); // S106-082: no second catch-up needed
465
1/1
✓ Branch 120 → 121 taken 58 times.
58 scheduleRenewal();
466
1/1
✓ Branch 121 → 122 taken 58 times.
58 emit changed();
467 58 return;
468 }
469
1/1
✓ Branch 126 → 127 taken 6 times.
12 m_ctx.stats->count(scope, QStringLiteral("subscription.failed"));
470
1/1
✓ Branch 130 → 131 taken 6 times.
6 int retry = retryDelay(record->failures, result.retryAfterSeconds);
471
3/3
✓ Branch 131 → 132 taken 2 times.
✓ Branch 131 → 133 taken 2 times.
✓ Branch 131 → 137 taken 2 times.
6 switch (result.outcome) {
472 2 case DavPushClient::Outcome::Forbidden:
473 case DavPushClient::Outcome::Incompatible:
474 2 retry = kMaxRetrySeconds; // profile problem: re-checked by discovery
475 2 break;
476 2 case DavPushClient::Outcome::NotFound:
477
1/1
✓ Branch 134 → 135 taken 2 times.
2 m_ctx.requestSync(m_accountId, {}, DavSyncReasonRecovery); // collection gone: rediscover
478 2 break;
479 2 default:
480 2 break;
481 }
482
1/1
✓ Branch 140 → 141 taken 6 times.
6 recordError(*record, result.detail, retry);
483
1/1
✓ Branch 145 → 146 taken 6 times.
12 m_ctx.stats->event(scope, QStringLiteral("register-failed"), result.detail);
484
1/1
✓ Branch 148 → 149 taken 6 times.
6 m_ctx.pushHealth(m_accountId, true);
485
1/1
✓ Branch 149 → 150 taken 6 times.
6 emit changed();
486 300 }
487
488 6 void DavPushAccount::recordError(DavPushCollectionRecord record, const QString &error, int retrySeconds) {
489 6 const qint64 now = m_ctx.now();
490 6 record.lastError = error;
491 6 record.lastErrorAt = now;
492 6 record.failures += 1;
493 6 record.nextRetryAt = now + retrySeconds;
494 6 m_ctx.store->saveCollection(record);
495 6 scheduleRenewal();
496 6 }
497
498 16 void DavPushAccount::unregisterUrl(DavPushClient *client, const QString &path, const QString &url, qint64 cleanupId) {
499 16 connect(client, &DavPushClient::unregistered, this,
500
2/6
✓ Branch 4 → 5 taken 16 times.
✓ Branch 5 → 6 taken 16 times.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
32 [this, path, url, cleanupId, request = client->unregisterSubscription(url)](const DavPushClient::Result &result) {
501
2/2
✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 4 taken 15 times.
25 if (result.request != request) return;
502
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 12 times.
15 if (!path.isEmpty()) m_unregistering.remove(path);
503 15 m_cleanupInFlight.remove(cleanupId);
504 15 const bool done = result.outcome == DavPushClient::Outcome::Ok;
505
2/2
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 29 taken 12 times.
15 if (!path.isEmpty()) {
506
1/1
✓ Branch 10 → 11 taken 3 times.
3 auto record = m_ctx.store->collection(m_accountId, path);
507
3/6
✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 17 not taken.
✓ Branch 15 → 16 taken 3 times.
✗ Branch 15 → 17 not taken.
✓ Branch 18 → 19 taken 3 times.
✗ Branch 18 → 24 not taken.
3 if (record && record->registrationUrl == url) {
508 3 record->registrationUrl.clear();
509 3 record->expiresAt = 0;
510
1/1
✓ Branch 23 → 24 taken 3 times.
3 m_ctx.store->saveCollection(*record);
511 }
512
4/4
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 27 taken 2 times.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
3 if (!done) m_ctx.store->queueCleanup(m_accountId, url, m_ctx.now());
513
1/2
✓ Branch 29 → 30 taken 12 times.
✗ Branch 29 → 31 not taken.
15 } else if (done) {
514 12 m_ctx.store->removeCleanup(cleanupId);
515 } else {
516 ✗ m_ctx.store->updateCleanupAttempt(cleanupId);
517 }
518
2/2
✓ Branch 39 → 40 taken 15 times.
✓ Branch 40 → 41 taken 15 times.
15 m_ctx.stats->count(DavPushStatistics::accountScope(m_accountId),
519
6/10
✓ Branch 32 → 33 taken 14 times.
✓ Branch 32 → 36 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 14 times.
✓ Branch 45 → 46 taken 14 times.
✓ Branch 45 → 47 taken 1 time.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 63 not taken.
45 done ? QStringLiteral("subscription.removed") : QStringLiteral("subscription.remove-failed"));
520 15 emit changed();
521 });
522 16 }
523
524 138 void DavPushAccount::scheduleRenewal() {
525 138 qint64 next = 0;
526
2/2
✓ Branch 42 → 5 taken 272 times.
✓ Branch 42 → 43 taken 138 times.
410 for (const QString &path : std::as_const(m_target.desiredPaths)) {
527
1/1
✓ Branch 6 → 7 taken 272 times.
272 const auto record = m_ctx.store->collection(m_accountId, path);
528
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 272 times.
272 if (!record) continue;
529 272 qint64 due = 0;
530
2/2
✓ Branch 11 → 12 taken 9 times.
✓ Branch 11 → 14 taken 263 times.
272 if (record->nextRetryAt > 0) {
531 9 due = record->nextRetryAt;
532
2/2
✓ Branch 16 → 17 taken 171 times.
✓ Branch 16 → 30 taken 92 times.
263 } else if (!record->registrationUrl.isEmpty()) {
533
2/2
✓ Branch 18 → 19 taken 3 times.
✓ Branch 18 → 21 taken 168 times.
171 const qint64 base = record->renewedAt > 0 ? record->renewedAt : record->registeredAt;
534 171 const qint64 lifetime = qMax<qint64>(0, record->expiresAt - base);
535 // Earlier-only jitter up to 5 % of the lifetime.
536
2/3
✓ Branch 25 → 26 taken 171 times.
✗ Branch 25 → 52 not taken.
✓ Branch 26 → 27 taken 171 times.
171 const qint64 jitter = qint64(lifetime * 0.05 * QRandomGenerator::global()->generateDouble());
537 171 due = qMin(base + lifetime * 3 / 4, record->expiresAt - 600) - jitter;
538 }
539
6/6
✓ Branch 30 → 31 taken 180 times.
✓ Branch 30 → 34 taken 92 times.
✓ Branch 31 → 32 taken 77 times.
✓ Branch 31 → 33 taken 103 times.
✓ Branch 32 → 33 taken 39 times.
✓ Branch 32 → 34 taken 38 times.
272 if (due > 0 && (next == 0 || due < next)) next = due;
540 272 }
541 138 m_nextRenewalAt = next;
542
2/2
✓ Branch 43 → 44 taken 35 times.
✓ Branch 43 → 46 taken 103 times.
138 if (next == 0) {
543
1/1
✓ Branch 44 → 45 taken 35 times.
35 m_renewal.stop();
544 35 return;
545 }
546
1/1
✓ Branch 46 → 47 taken 103 times.
103 const qint64 delay = qMax<qint64>(kMinRetrySeconds, next - m_ctx.now());
547
1/1
✓ Branch 49 → 50 taken 103 times.
103 m_renewal.start(int(qMin<qint64>(delay, 24 * 3600) * 1000));
548 }
549
550 // --- messages ---------------------------------------------------------------
551
552 9 void DavPushAccount::onMessage(const QByteArray &payload, const QString &messageId) {
553 Q_UNUSED(messageId);
554
1/1
✓ Branch 2 → 3 taken 9 times.
9 const QString scope = DavPushStatistics::accountScope(m_accountId);
555
1/1
✓ Branch 6 → 7 taken 9 times.
18 m_ctx.stats->count(scope, QStringLiteral("messages.received"));
556
8/8
✓ Branch 9 → 10 taken 9 times.
✓ Branch 13 → 14 taken 9 times.
✓ Branch 14 → 15 taken 9 times.
✓ Branch 15 → 16 taken 9 times.
✓ Branch 17 → 18 taken 9 times.
✓ Branch 18 → 19 taken 9 times.
✓ Branch 22 → 11 taken 9 times.
✓ Branch 22 → 23 taken 9 times.
18 qCDebug(lcDavPush) << "Push payload received for" << m_accountId << payload.size() << "bytes";
557
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 9 times.
9 if (!m_secret) return;
558 9 QString error;
559
1/1
✓ Branch 27 → 28 taken 9 times.
9 const auto plain = WebPushPayloadCodec::decrypt(payload, m_secret->keys, &error);
560
2/2
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 43 taken 7 times.
9 if (!plain) {
561
1/1
✓ Branch 33 → 34 taken 2 times.
4 m_ctx.stats->count(scope, QStringLiteral("messages.discarded.decrypt"));
562
1/1
✓ Branch 39 → 40 taken 2 times.
4 m_ctx.stats->event(scope, QStringLiteral("message-discarded"), error);
563 2 return;
564 }
565
1/1
✓ Branch 44 → 45 taken 7 times.
7 const auto message = DavPushXml::parsePushMessage(*plain, &error);
566
2/2
✓ Branch 46 → 47 taken 1 time.
✓ Branch 46 → 60 taken 6 times.
7 if (!message) {
567
1/1
✓ Branch 50 → 51 taken 1 time.
2 m_ctx.stats->count(scope, QStringLiteral("messages.discarded.parse"));
568
1/1
✓ Branch 56 → 57 taken 1 time.
2 m_ctx.stats->event(scope, QStringLiteral("message-discarded"), error);
569 1 return;
570 }
571
2/2
✓ Branch 62 → 63 taken 1 time.
✓ Branch 62 → 73 taken 5 times.
6 if (message->isKeyRotation()) {
572 // Read the new VAPID key through authenticated discovery (§3.3); never
573 // change endpoints from push fields alone.
574
1/1
✓ Branch 66 → 67 taken 1 time.
2 m_ctx.stats->count(scope, QStringLiteral("messages.key-rotation"));
575
1/1
✓ Branch 70 → 71 taken 1 time.
1 m_ctx.requestSync(m_accountId, {}, DavSyncReasonRecovery);
576 1 return;
577 }
578 5 QStringList paths;
579
5/5
✓ Branch 73 → 74 taken 5 times.
✓ Branch 74 → 75 taken 5 times.
✓ Branch 75 → 76 taken 5 times.
✓ Branch 89 → 77 taken 17 times.
✓ Branch 89 → 90 taken 5 times.
22 for (const auto &record : m_ctx.store->collections(m_accountId))
580
5/6
✓ Branch 80 → 81 taken 4 times.
✓ Branch 80 → 84 taken 13 times.
✓ Branch 82 → 83 taken 4 times.
✗ Branch 82 → 84 not taken.
✓ Branch 85 → 86 taken 4 times.
✓ Branch 85 → 87 taken 13 times.
17 if (record.offer.topic == message->topic && m_target.knownPaths.contains(record.path))
581
1/1
✓ Branch 86 → 87 taken 4 times.
9 paths << record.path;
582
2/2
✓ Branch 92 → 93 taken 1 time.
✓ Branch 92 → 100 taken 4 times.
5 if (paths.isEmpty()) {
583
1/1
✓ Branch 96 → 97 taken 1 time.
2 m_ctx.stats->count(scope, QStringLiteral("messages.discarded.unknown-topic"));
584 1 return;
585 }
586
1/1
✓ Branch 103 → 104 taken 4 times.
8 m_ctx.stats->count(scope, QStringLiteral("messages.accepted"));
587 4 QStringList dirty;
588
1/1
✓ Branch 106 → 107 taken 4 times.
4 const qint64 now = m_ctx.now();
589
2/2
✓ Branch 173 → 110 taken 4 times.
✓ Branch 173 → 174 taken 4 times.
8 for (const QString &path : std::as_const(paths)) {
590
1/1
✓ Branch 111 → 112 taken 4 times.
4 auto record = m_ctx.store->collection(m_accountId, path);
591
1/2
✓ Branch 113 → 114 taken 4 times.
✗ Branch 113 → 117 not taken.
4 if (record) {
592 4 record->lastPushAt = now;
593
1/1
✓ Branch 116 → 117 taken 4 times.
4 m_ctx.store->saveCollection(*record);
594 }
595 // Equal tokens are a possible no-op; never compare them otherwise.
596
3/4
✓ Branch 121 → 122 taken 4 times.
✗ Branch 121 → 129 not taken.
✓ Branch 125 → 126 taken 2 times.
✓ Branch 125 → 129 taken 2 times.
8 if (message->contentUpdate && !message->syncToken.isEmpty() &&
597
6/11
✓ Branch 118 → 119 taken 4 times.
✗ Branch 118 → 129 not taken.
✓ Branch 122 → 123 taken 4 times.
✓ Branch 127 → 128 taken 2 times.
✗ Branch 127 → 129 not taken.
✓ Branch 130 → 131 taken 4 times.
✗ Branch 130 → 132 not taken.
✓ Branch 132 → 133 taken 2 times.
✓ Branch 132 → 142 taken 2 times.
✗ Branch 280 → 281 not taken.
✗ Branch 280 → 282 not taken.
12 message->syncToken == m_ctx.storedToken(m_accountId, path) && !message->propertyUpdate) {
598
2/2
✓ Branch 136 → 137 taken 2 times.
✓ Branch 137 → 138 taken 2 times.
4 m_ctx.stats->count(collectionScope(path), QStringLiteral("messages.token-noop"));
599 2 continue;
600 }
601
1/4
✗ Branch 143 → 144 not taken.
✓ Branch 143 → 153 taken 2 times.
✗ Branch 147 → 148 not taken.
✗ Branch 148 → 149 not taken.
2 if (m_pushSince.contains(path)) m_ctx.stats->count(collectionScope(path), QStringLiteral("messages.coalesced"));
602
1/1
✓ Branch 154 → 155 taken 2 times.
2 else m_pushSince.insert(path, m_monotonic.elapsed());
603
2/2
✓ Branch 159 → 160 taken 2 times.
✓ Branch 160 → 161 taken 2 times.
4 m_ctx.stats->count(collectionScope(path), QStringLiteral("messages.mapped"));
604
1/1
✓ Branch 164 → 165 taken 2 times.
2 dirty << path;
605 4 }
606
9/9
✓ Branch 174 → 175 taken 4 times.
✓ Branch 178 → 179 taken 4 times.
✓ Branch 179 → 180 taken 4 times.
✓ Branch 180 → 181 taken 4 times.
✓ Branch 181 → 182 taken 4 times.
✓ Branch 183 → 184 taken 4 times.
✓ Branch 184 → 185 taken 4 times.
✓ Branch 192 → 176 taken 4 times.
✓ Branch 192 → 193 taken 4 times.
8 qCInfo(lcDavPush) << "Push message for" << m_accountId << "mapped" << paths.size() << "collection(s),"
607
2/2
✓ Branch 187 → 188 taken 4 times.
✓ Branch 188 → 189 taken 4 times.
4 << paths.size() - dirty.size() << "already applied (same token)";
608
3/3
✓ Branch 194 → 195 taken 2 times.
✓ Branch 194 → 196 taken 2 times.
✓ Branch 195 → 196 taken 2 times.
4 if (!dirty.isEmpty()) m_ctx.requestSync(m_accountId, dirty, DavSyncReasonPush);
609
1/1
✓ Branch 196 → 197 taken 4 times.
4 emit changed();
610 23 }
611