MailJDTourDownloadDocsQualityTestsCoverage

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

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.8% 55 / 0 / 58
Functions: 100.0% 5 / 0 / 5
Branches: 73.8% 90 / 0 / 122

service/NextcloudLoginJson.cpp
Line Branch Exec Source
1 #include "NextcloudLoginJson.h"
2
3 #include <QJsonDocument>
4 #include <QJsonObject>
5
6 namespace NextcloudLoginJson {
7
8 namespace {
9 32 int effectivePort(const QUrl &url) {
10
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 6 taken 28 times.
32 if (url.port() >= 0)
11 4 return url.port();
12
1/1
✓ Branch 6 → 7 taken 28 times.
84 return url.scheme().compare(QStringLiteral("https"), Qt::CaseInsensitive) == 0
13
2/2
✓ Branch 11 → 12 taken 26 times.
✓ Branch 11 → 13 taken 2 times.
28 ? 443
14 28 : 80;
15 }
16
17 13 bool parseObject(const QByteArray &response, QJsonObject *object) {
18 13 QJsonParseError parseError;
19
1/1
✓ Branch 2 → 3 taken 13 times.
13 const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
20
4/7
✓ Branch 3 → 4 taken 13 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 13 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 13 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 13 times.
13 if (parseError.error != QJsonParseError::NoError || !document.isObject())
21 ✗ return false;
22
1/1
✓ Branch 10 → 11 taken 13 times.
13 *object = document.object();
23 13 return true;
24 13 }
25 } // namespace
26
27 19 bool isSameOrigin(const QUrl &candidate, const QUrl &expected) {
28
4/7
✓ Branch 5 → 6 taken 19 times.
✓ Branch 7 → 8 taken 19 times.
✗ Branch 7 → 30 not taken.
✓ Branch 11 → 12 taken 18 times.
✓ Branch 11 → 30 taken 1 time.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
57 return candidate.isValid() && !candidate.host().isEmpty() &&
29
4/10
✓ Branch 8 → 9 taken 19 times.
✓ Branch 9 → 10 taken 19 times.
✓ Branch 42 → 43 taken 19 times.
✗ Branch 42 → 44 not taken.
✓ Branch 44 → 45 taken 19 times.
✗ Branch 44 → 46 not taken.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
✗ Branch 72 → 73 not taken.
✗ Branch 72 → 74 not taken.
38 candidate.scheme().compare(expected.scheme(), Qt::CaseInsensitive) ==
30
2/2
✓ Branch 17 → 18 taken 16 times.
✓ Branch 17 → 30 taken 2 times.
36 0 &&
31
5/10
✓ Branch 13 → 14 taken 18 times.
✓ Branch 15 → 16 taken 18 times.
✓ Branch 38 → 39 taken 18 times.
✓ Branch 38 → 40 taken 1 time.
✓ Branch 40 → 41 taken 19 times.
✗ Branch 40 → 42 not taken.
✗ Branch 58 → 59 not taken.
✗ Branch 58 → 60 not taken.
✗ Branch 63 → 64 not taken.
✗ Branch 63 → 65 not taken.
55 candidate.host().compare(expected.host(), Qt::CaseInsensitive) == 0 &&
32
4/6
✓ Branch 18 → 19 taken 16 times.
✓ Branch 19 → 20 taken 16 times.
✓ Branch 20 → 21 taken 16 times.
✗ Branch 20 → 30 not taken.
✓ Branch 24 → 25 taken 16 times.
✗ Branch 24 → 30 not taken.
32 effectivePort(candidate) == effectivePort(expected) &&
33
11/17
✓ Branch 2 → 3 taken 19 times.
✓ Branch 3 → 4 taken 19 times.
✗ Branch 3 → 30 not taken.
✓ Branch 22 → 23 taken 16 times.
✓ Branch 26 → 27 taken 16 times.
✓ Branch 28 → 29 taken 16 times.
✗ Branch 28 → 30 not taken.
✓ Branch 32 → 33 taken 16 times.
✓ Branch 32 → 34 taken 3 times.
✓ Branch 34 → 35 taken 16 times.
✓ Branch 34 → 36 taken 3 times.
✓ Branch 36 → 37 taken 18 times.
✓ Branch 36 → 38 taken 1 time.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
73 candidate.userName().isEmpty() && candidate.password().isEmpty();
34 }
35
36 10 Start parseStart(const QByteArray &response, const QUrl &origin, bool (*loginUrlAllowed)(const QUrl &)) {
37
1/1
✓ Branch 2 → 3 taken 10 times.
10 Start start;
38
1/1
✓ Branch 3 → 4 taken 10 times.
10 QJsonObject root;
39
2/3
✓ Branch 4 → 5 taken 10 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 10 times.
10 if (!parseObject(response, &root))
40 ✗ return start;
41
2/2
✓ Branch 10 → 11 taken 10 times.
✓ Branch 11 → 12 taken 10 times.
10 const QJsonObject poll = root[QStringLiteral("poll")].toObject();
42
2/2
✓ Branch 17 → 18 taken 10 times.
✓ Branch 18 → 19 taken 10 times.
10 start.pollEndpoint = poll[QStringLiteral("endpoint")].toString();
43
2/2
✓ Branch 27 → 28 taken 10 times.
✓ Branch 28 → 29 taken 10 times.
10 start.pollToken = poll[QStringLiteral("token")].toString();
44
2/2
✓ Branch 37 → 38 taken 10 times.
✓ Branch 39 → 40 taken 10 times.
10 const QString login = root[QStringLiteral("login")].toString();
45
6/8
✓ Branch 44 → 45 taken 9 times.
✓ Branch 44 → 49 taken 1 time.
✓ Branch 46 → 47 taken 9 times.
✗ Branch 46 → 49 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 9 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 9 times.
10 if (start.pollEndpoint.isEmpty() || start.pollToken.isEmpty() || login.isEmpty())
46 1 return start;
47 // T-611/SEC-10: the poll endpoint stays on the origin (no SSRF).
48
4/4
✓ Branch 53 → 54 taken 9 times.
✓ Branch 54 → 55 taken 9 times.
✓ Branch 56 → 57 taken 1 time.
✓ Branch 56 → 58 taken 8 times.
9 if (!isSameOrigin(QUrl(start.pollEndpoint), origin)) {
49 1 start.error = Error::ForeignPollEndpoint;
50 1 return start;
51 }
52
1/1
✓ Branch 58 → 59 taken 8 times.
8 start.loginUrl = QUrl(login);
53
7/8
✓ Branch 61 → 62 taken 8 times.
✓ Branch 62 → 63 taken 8 times.
✗ Branch 62 → 65 not taken.
✓ Branch 63 → 64 taken 8 times.
✓ Branch 64 → 65 taken 1 time.
✓ Branch 64 → 66 taken 7 times.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 67 → 69 taken 7 times.
8 if (!loginUrlAllowed(start.loginUrl) || !isSameOrigin(start.loginUrl, origin)) {
54 1 start.error = Error::ForeignLoginUrl;
55 1 return start;
56 }
57 7 start.error = Error::None;
58 7 return start;
59 10 }
60
61 3 Result parseResult(const QByteArray &response, const QUrl &origin) {
62 3 Result result;
63
1/1
✓ Branch 2 → 3 taken 3 times.
3 QJsonObject object;
64
2/3
✓ Branch 3 → 4 taken 3 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3 times.
3 if (!parseObject(response, &object))
65 ✗ return result;
66
2/2
✓ Branch 9 → 10 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
3 result.server = object[QStringLiteral("server")].toString();
67
2/2
✓ Branch 20 → 21 taken 3 times.
✓ Branch 22 → 23 taken 3 times.
3 result.loginName = object[QStringLiteral("loginName")].toString();
68
2/2
✓ Branch 31 → 32 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
3 result.appPassword = object[QStringLiteral("appPassword")].toString();
69
6/8
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 45 taken 1 time.
✓ Branch 42 → 43 taken 2 times.
✗ Branch 42 → 45 not taken.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 2 times.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 2 times.
3 if (result.server.isEmpty() || result.loginName.isEmpty() || result.appPassword.isEmpty())
70 1 return result;
71 // SEC-2026-07-21-05: the app password belongs to the original server; a
72 // foreign "server" value would forward it across origins on the next sync.
73
4/4
✓ Branch 49 → 50 taken 2 times.
✓ Branch 50 → 51 taken 2 times.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 54 taken 1 time.
2 if (!isSameOrigin(QUrl(result.server), origin)) {
74 1 result.error = Error::ForeignServer;
75 1 return result;
76 }
77 1 result.error = Error::None;
78 1 return result;
79 3 }
80
81 } // namespace NextcloudLoginJson
82