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: 94.7% 251 / 0 / 265
Functions: 100.0% 16 / 0 / 16
Branches: 82.9% 271 / 0 / 327

service/NextcloudAuth.cpp
Line Branch Exec Source
1 #include "NextcloudAuth.h"
2 #include "NextcloudLoginJson.h"
3
4 #include <QDesktopServices>
5 #include <QLoggingCategory>
6 #include <QNetworkAccessManager>
7 #include <QNetworkReply>
8 #include <QNetworkRequest>
9 #include <QTimer>
10 #include <QUrl>
11 #include <QUrlQuery>
12
13 #include <utility>
14
15
4/7
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 76 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
78 Q_LOGGING_CATEGORY(lcNextcloudAuth, "mailjd.nextcloudauth")
16
17
18 12 NextcloudAuth::NextcloudAuth(QObject *parent) : QObject(parent) {
19
3/6
✓ Branch 7 → 8 taken 12 times.
✓ Branch 8 → 9 taken 12 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 12 times.
✗ Branch 27 → 28 not taken.
✗ Branch 27 → 29 not taken.
12 m_nam = new QNetworkAccessManager(this);
20
1/1
✓ Branch 11 → 12 taken 12 times.
12 m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy);
21
3/6
✓ Branch 12 → 13 taken 12 times.
✓ Branch 13 → 14 taken 12 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 12 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
12 m_pollTimer = new QTimer(this);
22
1/1
✓ Branch 16 → 17 taken 12 times.
12 m_pollTimer->setInterval(2000);
23
1/1
✓ Branch 17 → 18 taken 12 times.
12 connect(m_pollTimer, &QTimer::timeout, this, &NextcloudAuth::poll);
24
3/6
✓ Branch 19 → 20 taken 12 times.
✓ Branch 20 → 21 taken 12 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 12 times.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
12 m_replyDeadlineTimer = new QTimer(this);
25
1/1
✓ Branch 23 → 24 taken 12 times.
12 m_replyDeadlineTimer->setSingleShot(true);
26 12 connect(m_replyDeadlineTimer, &QTimer::timeout, this,
27
1/1
✓ Branch 24 → 25 taken 12 times.
12 &NextcloudAuth::onReplyDeadline);
28 12 }
29
30 12 NextcloudAuth::~NextcloudAuth() { cancel(); }
31
32 6 void NextcloudAuth::setNetworkAccessManager(QNetworkAccessManager *nam) {
33
3/6
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 6 times.
✗ Branch 7 → 10 not taken.
6 if (m_nam && m_nam->parent() == this)
34
1/2
✓ Branch 8 → 9 taken 6 times.
✗ Branch 8 → 10 not taken.
6 delete m_nam;
35 6 m_nam = nam;
36
1/2
✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 13 not taken.
6 if (m_nam) {
37 6 m_nam->setParent(this);
38 6 m_nam->setRedirectPolicy(QNetworkRequest::SameOriginRedirectPolicy);
39 }
40 6 }
41
42 15 void NextcloudAuth::startLogin(const QString &serverUrl) {
43
1/1
✓ Branch 2 → 3 taken 15 times.
15 cancel(); // Cancel any ongoing flow
44
45 15 QString url = serverUrl;
46
3/3
✓ Branch 5 → 6 taken 15 times.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 14 times.
15 if (url.endsWith('/'))
47
1/1
✓ Branch 7 → 8 taken 1 time.
1 url.chop(1);
48
49
1/1
✓ Branch 8 → 9 taken 15 times.
15 QUrl parsedUrl(url);
50
3/3
✓ Branch 9 → 10 taken 15 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 26 taken 13 times.
15 if (!isServerUrlAllowedForLogin(parsedUrl)) {
51
4/4
✓ Branch 11 → 12 taken 2 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 21 → 13 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
4 qCWarning(lcNextcloudAuth)
52
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
2 << "Rejected insecure Nextcloud Login Flow URL:" << url;
53
2/2
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
2 emit loginFailed(tr("Nextcloud Login Flow requires HTTPS"));
54 2 return;
55 }
56
57 // T-611/SEC-10: Save original server URL for SSRF origin validation
58 13 m_originalServerUrl = url;
59
60 QNetworkRequest request{
61
3/3
✓ Branch 30 → 31 taken 13 times.
✓ Branch 31 → 32 taken 13 times.
✓ Branch 32 → 33 taken 13 times.
13 QUrl(url + QStringLiteral("/index.php/login/v2"))};
62
1/1
✓ Branch 41 → 42 taken 13 times.
13 request.setHeader(QNetworkRequest::ContentTypeHeader,
63 26 QStringLiteral("application/x-www-form-urlencoded"));
64 // Nextcloud requires a User-Agent for the app name display
65
3/3
✓ Branch 45 → 46 taken 13 times.
✓ Branch 46 → 47 taken 13 times.
✓ Branch 47 → 48 taken 13 times.
13 request.setRawHeader("User-Agent", "MailJD/1.0");
66
67
2/2
✓ Branch 51 → 52 taken 13 times.
✓ Branch 52 → 53 taken 13 times.
13 beginReply(m_nam->post(request, QByteArray()));
68 13 connect(m_currentReply, &QNetworkReply::finished, this,
69
1/1
✓ Branch 54 → 55 taken 13 times.
13 &NextcloudAuth::onInitReply);
70 17 }
71
72 30 void NextcloudAuth::cancel() {
73 30 m_pollTimer->stop();
74 30 m_replyDeadlineTimer->stop();
75 30 m_pollCount = 0;
76 30 m_pollEndpoint.clear();
77 30 m_pollToken.clear();
78
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 11 taken 28 times.
30 if (m_currentReply) {
79 // Qt 6.8: QNetworkReply::abort() emits 'finished' synchronously, which
80 // would re-enter the connected poll/onInit lambda (it nulls m_currentReply
81 // and may emit loginFailed/loginSuccess). Disconnect first so cancel()
82 // owns the cleanup exclusively, then abort+deleteLater safely.
83 2 disconnect(m_currentReply, nullptr, this, nullptr);
84 2 m_currentReply->abort();
85 2 m_currentReply->deleteLater();
86 2 m_currentReply = nullptr;
87 }
88 30 m_replyData.clear();
89 30 m_replyFailure = ReplyFailure::None;
90 30 }
91
92 36 bool NextcloudAuth::isPolling() const { return m_pollTimer->isActive(); }
93
94 27 bool NextcloudAuth::isServerUrlAllowedForLogin(const QUrl &url) {
95
6/14
✓ Branch 2 → 3 taken 27 times.
✓ Branch 3 → 4 taken 27 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 27 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 27 times.
✓ Branch 10 → 11 taken 27 times.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 27 times.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
54 if (!url.isValid() || url.host().isEmpty())
96 ✗ return false;
97
98
3/3
✓ Branch 15 → 16 taken 27 times.
✓ Branch 18 → 19 taken 18 times.
✓ Branch 18 → 20 taken 9 times.
27 if (url.scheme() == QLatin1String("https"))
99 18 return true;
100
101 #ifdef MAILJD_UNIT_TEST
102
2/3
✓ Branch 21 → 22 taken 8 times.
✓ Branch 24 → 25 taken 8 times.
✗ Branch 24 → 42 not taken.
8 if (url.scheme() == QLatin1String("http")) {
103
2/2
✓ Branch 26 → 27 taken 8 times.
✓ Branch 27 → 28 taken 8 times.
8 const QString host = url.host().toLower();
104
2/2
✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 38 taken 5 times.
15 return host == QLatin1String("localhost") ||
105
3/4
✓ Branch 31 → 32 taken 7 times.
✓ Branch 31 → 38 taken 1 time.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 2 times.
15 host == QLatin1String("127.0.0.1") ||
106 2 host == QLatin1String("::1");
107 8 }
108 #endif
109
110 1 return false;
111 }
112
113 21 void NextcloudAuth::beginReply(QNetworkReply *reply) {
114
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 21 times.
21 Q_ASSERT(reply);
115
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 21 times.
21 Q_ASSERT(!m_currentReply);
116 21 m_currentReply = reply;
117 21 m_replyData.clear();
118 21 m_replyFailure = ReplyFailure::None;
119 // Backpressure keeps QNetworkReply itself bounded while readyRead drains
120 // into the explicitly capped accumulator below.
121 21 reply->setReadBufferSize(MaxResponseBytes + 1);
122 21 connect(reply, &QNetworkReply::readyRead, this,
123
1/1
✓ Branch 8 → 9 taken 21 times.
21 &NextcloudAuth::onReplyReadyRead);
124 21 connect(reply, &QNetworkReply::metaDataChanged, this,
125
1/1
✓ Branch 10 → 11 taken 21 times.
21 &NextcloudAuth::onReplyReadyRead);
126 21 m_replyDeadlineTimer->start(ReplyDeadlineMs);
127 21 }
128
129 3 void NextcloudAuth::abortCurrentReply(ReplyFailure failure) {
130
2/4
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
3 if (!m_currentReply || m_replyFailure != ReplyFailure::None)
131 ✗ return;
132 3 m_replyFailure = failure;
133 3 m_replyDeadlineTimer->stop();
134 3 m_currentReply->abort();
135 }
136
137 19 void NextcloudAuth::onReplyReadyRead() {
138
2/4
✓ Branch 2 → 3 taken 19 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 19 times.
19 if (!m_currentReply || m_replyFailure != ReplyFailure::None)
139 5 return;
140
141 19 bool lengthOk = false;
142 const qint64 contentLength =
143
1/1
✓ Branch 5 → 6 taken 19 times.
19 m_currentReply->header(QNetworkRequest::ContentLengthHeader)
144
1/1
✓ Branch 6 → 7 taken 19 times.
19 .toLongLong(&lengthOk);
145
3/4
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 23 taken 17 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 23 taken 2 times.
19 if (lengthOk && contentLength > MaxResponseBytes) {
146 ✗ qCWarning(lcNextcloudAuth) << "Rejecting oversized Nextcloud response by"
147 ✗ " Content-Length:"
148 ✗ << contentLength;
149 ✗ abortCurrentReply(ReplyFailure::TooLarge);
150 ✗ return;
151 }
152
153 19 const qint64 remaining = MaxResponseBytes - m_replyData.size();
154
1/1
✓ Branch 24 → 25 taken 19 times.
19 const qint64 available = m_currentReply->bytesAvailable();
155
2/2
✓ Branch 25 → 26 taken 3 times.
✓ Branch 25 → 27 taken 16 times.
19 if (available <= 0)
156 3 return;
157
158
1/1
✓ Branch 28 → 29 taken 16 times.
16 const QByteArray chunk = m_currentReply->read(qMin(available, remaining + 1));
159
2/2
✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 44 taken 14 times.
16 if (chunk.size() > remaining) {
160
4/4
✓ Branch 31 → 32 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 41 → 33 taken 2 times.
✓ Branch 41 → 42 taken 2 times.
4 qCWarning(lcNextcloudAuth) << "Rejecting streamed Nextcloud response above"
161
1/1
✓ Branch 36 → 37 taken 2 times.
2 " byte limit"
162
1/1
✓ Branch 37 → 38 taken 2 times.
2 << MaxResponseBytes;
163
1/1
✓ Branch 42 → 43 taken 2 times.
2 abortCurrentReply(ReplyFailure::TooLarge);
164 2 return;
165 }
166
1/1
✓ Branch 44 → 45 taken 14 times.
14 m_replyData.append(chunk);
167 16 }
168
169 1 void NextcloudAuth::onReplyDeadline() {
170
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 if (!m_currentReply)
171 ✗ return;
172
5/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 15 → 6 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
2 qCWarning(lcNextcloudAuth) << "Nextcloud Login Flow request exceeded"
173
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
1 << ReplyDeadlineMs << "ms deadline";
174 1 abortCurrentReply(ReplyFailure::Timeout);
175 }
176
177 19 QByteArray NextcloudAuth::finishReply(QNetworkReply *reply,
178 QString *failureReason) {
179
2/4
✓ Branch 2 → 3 taken 19 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 10 taken 19 times.
19 if (!reply || reply != m_currentReply) {
180 ✗ if (failureReason)
181 ✗ *failureReason = tr("Unexpected Nextcloud Login Flow reply");
182 ✗ return {};
183 }
184
185
1/1
✓ Branch 10 → 11 taken 19 times.
19 m_replyDeadlineTimer->stop();
186
2/2
✓ Branch 11 → 12 taken 16 times.
✓ Branch 11 → 28 taken 3 times.
19 if (m_replyFailure == ReplyFailure::None) {
187 16 bool lengthOk = false;
188 const qint64 contentLength =
189
1/1
✓ Branch 12 → 13 taken 16 times.
16 reply->header(QNetworkRequest::ContentLengthHeader)
190
1/1
✓ Branch 13 → 14 taken 16 times.
16 .toLongLong(&lengthOk);
191
1/1
✓ Branch 15 → 16 taken 16 times.
16 const QByteArray tail = reply->readAll();
192
4/6
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 15 times.
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 21 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 16 times.
32 if ((lengthOk && contentLength > MaxResponseBytes) ||
193
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 16 times.
16 tail.size() > MaxResponseBytes - m_replyData.size()) {
194 ✗ m_replyFailure = ReplyFailure::TooLarge;
195 } else {
196
1/1
✓ Branch 25 → 26 taken 16 times.
16 m_replyData.append(tail);
197 }
198 16 }
199
200
1/2
✓ Branch 28 → 29 taken 19 times.
✗ Branch 28 → 41 not taken.
19 if (failureReason) {
201
3/4
✓ Branch 29 → 30 taken 16 times.
✓ Branch 29 → 32 taken 2 times.
✓ Branch 29 → 36 taken 1 time.
✗ Branch 29 → 40 not taken.
19 switch (m_replyFailure) {
202 16 case ReplyFailure::None:
203 16 failureReason->clear();
204 16 break;
205 2 case ReplyFailure::TooLarge:
206
1/1
✓ Branch 32 → 33 taken 2 times.
2 *failureReason = tr("Nextcloud Login Flow response is too large");
207 2 break;
208 1 case ReplyFailure::Timeout:
209
1/1
✓ Branch 36 → 37 taken 1 time.
1 *failureReason = tr("Nextcloud Login Flow request timed out");
210 1 break;
211 }
212 }
213
214 19 QByteArray result;
215
2/2
✓ Branch 41 → 42 taken 16 times.
✓ Branch 41 → 44 taken 3 times.
19 if (m_replyFailure == ReplyFailure::None)
216 16 result = std::move(m_replyData);
217
1/1
✓ Branch 44 → 45 taken 19 times.
19 m_replyData.clear();
218 19 m_replyFailure = ReplyFailure::None;
219 19 m_currentReply = nullptr;
220 19 return result;
221 }
222
223 13 void NextcloudAuth::onInitReply() {
224
2/2
✓ Branch 2 → 3 taken 13 times.
✓ Branch 3 → 4 taken 13 times.
13 auto *reply = qobject_cast<QNetworkReply *>(sender());
225
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 13 times.
13 if (!reply)
226 6 return;
227 13 QString boundedReplyFailure;
228
1/1
✓ Branch 6 → 7 taken 13 times.
13 const QByteArray responseData = finishReply(reply, &boundedReplyFailure);
229
1/1
✓ Branch 7 → 8 taken 13 times.
13 reply->deleteLater();
230
231
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 12 taken 11 times.
13 if (!boundedReplyFailure.isEmpty()) {
232
1/1
✓ Branch 10 → 11 taken 2 times.
2 emit loginFailed(boundedReplyFailure);
233 2 return;
234 }
235
236
3/3
✓ Branch 12 → 13 taken 11 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 31 taken 10 times.
11 if (reply->error() != QNetworkReply::NoError) {
237
4/4
✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 26 → 16 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
2 qCWarning(lcNextcloudAuth)
238
3/3
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
1 << "Login flow init failed:" << reply->errorString();
239
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
1 emit loginFailed(reply->errorString());
240 1 return;
241 }
242
243 const NextcloudLoginJson::Start start = NextcloudLoginJson::parseStart(
244
2/2
✓ Branch 31 → 32 taken 10 times.
✓ Branch 32 → 33 taken 10 times.
10 responseData, QUrl(m_originalServerUrl), &NextcloudAuth::isServerUrlAllowedForLogin);
245 10 m_pollEndpoint = start.pollEndpoint;
246 10 m_pollToken = start.pollToken;
247
2/2
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 41 taken 9 times.
10 if (start.error == NextcloudLoginJson::Error::Invalid) {
248
2/2
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
1 emit loginFailed(tr("Invalid Login Flow v2 response"));
249 1 return;
250 }
251
2/2
✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 60 taken 8 times.
9 if (start.error == NextcloudLoginJson::Error::ForeignPollEndpoint) {
252
4/4
✓ Branch 42 → 43 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
✓ Branch 55 → 44 taken 1 time.
✓ Branch 55 → 56 taken 1 time.
2 qCWarning(lcNextcloudAuth)
253
2/2
✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
1 << "Rejected cross-origin poll endpoint:" << m_pollEndpoint
254
3/3
✓ Branch 49 → 50 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 51 → 52 taken 1 time.
1 << "(expected origin:" << m_originalServerUrl << ")";
255
2/2
✓ Branch 56 → 57 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
1 emit loginFailed(tr("Security error: poll endpoint has a foreign origin"));
256 1 return;
257 }
258
2/2
✓ Branch 60 → 61 taken 1 time.
✓ Branch 60 → 82 taken 7 times.
8 if (start.error == NextcloudLoginJson::Error::ForeignLoginUrl) {
259
4/4
✓ Branch 61 → 62 taken 1 time.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 77 → 63 taken 1 time.
✓ Branch 77 → 78 taken 1 time.
2 qCWarning(lcNextcloudAuth)
260
3/3
✓ Branch 66 → 67 taken 1 time.
✓ Branch 68 → 69 taken 1 time.
✓ Branch 69 → 70 taken 1 time.
2 << "Rejected cross-origin login URL:" << start.loginUrl.toString()
261
3/3
✓ Branch 70 → 71 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 72 → 73 taken 1 time.
1 << "(expected origin:" << m_originalServerUrl << ")";
262
2/2
✓ Branch 78 → 79 taken 1 time.
✓ Branch 79 → 80 taken 1 time.
1 emit loginFailed(tr("Security error: login URL has a foreign origin"));
263 1 return;
264 }
265 7 const QUrl parsedLoginUrl = start.loginUrl;
266
267
5/5
✓ Branch 83 → 84 taken 7 times.
✓ Branch 87 → 88 taken 7 times.
✓ Branch 88 → 89 taken 7 times.
✓ Branch 92 → 85 taken 7 times.
✓ Branch 92 → 93 taken 7 times.
14 qCInfo(lcNextcloudAuth) << "Opening browser for Nextcloud login";
268
1/1
✓ Branch 93 → 94 taken 7 times.
7 QDesktopServices::openUrl(parsedLoginUrl);
269
270 // Start polling
271 7 m_pollCount = 0;
272
1/1
✓ Branch 94 → 95 taken 7 times.
7 m_pollTimer->start();
273 22 }
274
275 69 void NextcloudAuth::poll() {
276
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 24 taken 68 times.
69 if (++m_pollCount > MaxPollAttempts) {
277
1/1
✓ Branch 3 → 4 taken 1 time.
1 m_pollTimer->stop();
278 1 m_pollEndpoint.clear();
279 1 m_pollToken.clear();
280
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 8 not taken.
1 if (m_currentReply)
281
1/1
✓ Branch 7 → 8 taken 1 time.
1 m_currentReply->abort();
282
5/5
✓ Branch 8 → 9 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 19 → 10 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
2 qCWarning(lcNextcloudAuth) << "Login flow timed out after"
283
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
1 << (MaxPollAttempts * 2) << "seconds";
284
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
1 emit loginFailed(tr("Login timed out (120 seconds)"));
285 61 return;
286 }
287
288
2/2
✓ Branch 24 → 25 taken 60 times.
✓ Branch 24 → 36 taken 8 times.
68 if (m_currentReply) {
289
4/4
✓ Branch 25 → 26 taken 60 times.
✓ Branch 29 → 30 taken 60 times.
✓ Branch 34 → 27 taken 60 times.
✓ Branch 34 → 35 taken 60 times.
120 qCDebug(lcNextcloudAuth)
290
1/1
✓ Branch 30 → 31 taken 60 times.
60 << "Skipping Nextcloud login poll while previous poll is in flight";
291 60 return;
292 }
293
294
2/2
✓ Branch 36 → 37 taken 8 times.
✓ Branch 37 → 38 taken 8 times.
8 QNetworkRequest request{QUrl(m_pollEndpoint)};
295
1/1
✓ Branch 43 → 44 taken 8 times.
8 request.setHeader(QNetworkRequest::ContentTypeHeader,
296 16 QStringLiteral("application/x-www-form-urlencoded"));
297
298
1/1
✓ Branch 47 → 48 taken 8 times.
8 QUrlQuery params;
299
1/1
✓ Branch 51 → 52 taken 8 times.
16 params.addQueryItem(QStringLiteral("token"), m_pollToken);
300
301
2/2
✓ Branch 57 → 58 taken 8 times.
✓ Branch 58 → 59 taken 8 times.
8 beginReply(
302
2/2
✓ Branch 55 → 56 taken 8 times.
✓ Branch 56 → 57 taken 8 times.
16 m_nam->post(request, params.query(QUrl::FullyEncoded).toUtf8()));
303
1/1
✓ Branch 61 → 62 taken 8 times.
8 connect(m_currentReply, &QNetworkReply::finished, this, [this]() {
304
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 3 → 4 taken 6 times.
6 auto *reply = qobject_cast<QNetworkReply *>(sender());
305
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 6 times.
6 if (!reply)
306 5 return;
307 6 QString boundedReplyFailure;
308
1/1
✓ Branch 6 → 7 taken 6 times.
6 const QByteArray responseData = finishReply(reply, &boundedReplyFailure);
309
1/1
✓ Branch 7 → 8 taken 6 times.
6 reply->deleteLater();
310
311
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
6 if (m_pollToken.isEmpty())
312 ✗ return;
313
314
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 18 taken 5 times.
6 if (!boundedReplyFailure.isEmpty()) {
315
1/1
✓ Branch 13 → 14 taken 1 time.
1 m_pollTimer->stop();
316 1 m_pollEndpoint.clear();
317 1 m_pollToken.clear();
318
1/1
✓ Branch 16 → 17 taken 1 time.
1 emit loginFailed(boundedReplyFailure);
319 1 return;
320 }
321
322
1/1
✓ Branch 18 → 19 taken 5 times.
5 int status = reply->attribute(
323
1/1
✓ Branch 19 → 20 taken 5 times.
5 QNetworkRequest::HttpStatusCodeAttribute).toInt();
324
325
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 4 times.
5 if (status == 404) {
326 // Not yet authorized — keep polling
327 1 return;
328 }
329
330
3/3
✓ Branch 23 → 24 taken 4 times.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 32 taken 3 times.
4 if (reply->error() != QNetworkReply::NoError) {
331
1/1
✓ Branch 25 → 26 taken 1 time.
1 m_pollTimer->stop();
332 1 m_pollEndpoint.clear();
333 1 m_pollToken.clear();
334
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
1 emit loginFailed(reply->errorString());
335 1 return;
336 }
337
338 // Success!
339
1/1
✓ Branch 32 → 33 taken 3 times.
3 m_pollTimer->stop();
340
341 const NextcloudLoginJson::Result result =
342
2/2
✓ Branch 33 → 34 taken 3 times.
✓ Branch 34 → 35 taken 3 times.
3 NextcloudLoginJson::parseResult(responseData, QUrl(m_originalServerUrl));
343
2/2
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 43 taken 2 times.
3 if (result.error == NextcloudLoginJson::Error::Invalid) {
344 1 m_pollEndpoint.clear();
345 1 m_pollToken.clear();
346
2/2
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
1 emit loginFailed(tr("Invalid response from server"));
347 1 return;
348 }
349
2/2
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 64 taken 1 time.
2 if (result.error == NextcloudLoginJson::Error::ForeignServer) {
350
4/4
✓ Branch 44 → 45 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 57 → 46 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
2 qCWarning(lcNextcloudAuth)
351
1/1
✓ Branch 49 → 50 taken 1 time.
1 << "Rejected cross-origin server field in login success:"
352
1/1
✓ Branch 50 → 51 taken 1 time.
1 << result.server
353
3/3
✓ Branch 51 → 52 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 53 → 54 taken 1 time.
1 << "(expected origin:" << m_originalServerUrl << ")";
354 1 m_pollEndpoint.clear();
355 1 m_pollToken.clear();
356
1/1
✓ Branch 61 → 62 taken 1 time.
1 emit loginFailed(
357
1/1
✓ Branch 60 → 61 taken 1 time.
2 tr("Security error: login response has a foreign server origin"));
358 1 return;
359 }
360 1 const QString server = result.server;
361 1 const QString loginName = result.loginName;
362 1 const QString appPassword = result.appPassword;
363
364 // The login name is usually a mail address: not in the log (LOGGING.md).
365
5/5
✓ Branch 67 → 68 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 72 → 73 taken 1 time.
✓ Branch 76 → 69 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
2 qCInfo(lcNextcloudAuth) << "Login successful";
366 1 m_pollEndpoint.clear();
367 1 m_pollToken.clear();
368
1/1
✓ Branch 79 → 80 taken 1 time.
1 emit loginSuccess(server, loginName, appPassword);
369
6/6
✓ Branch 85 → 86 taken 1 time.
✓ Branch 85 → 87 taken 2 times.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 89 → 91 taken 5 times.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 93 → 96 taken 5 times.
13 });
370 8 }
371