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: 100.0% 2 / 0 / 2
Functions: 100.0% 2 / 0 / 2
Branches: -% 0 / 0 / 0

app/core/ContactOutbox.h
Line Branch Exec Source
1 #pragma once
2
3 #include <QObject>
4 #include <QSet>
5 #include <QString>
6 #include <QStringList>
7 #include <QTimer>
8 #include <functional>
9 #include <optional>
10
11 #include "data/ContactModels.h"
12 #include "service/DavResponse.h"
13
14 class ContactStore;
15 class CardDavClient;
16
17 // Contact write-back through a persistent outbox (Sprint 97 S97.1.4,
18 // CONTACT-OUTBOX, decision D-6) with the CalendarOutbox contract:
19 //
20 // 1. Local first: the card changes in the store at once (patched by
21 // VCardCodec, never rebuilt) and gets one coalesced outbox entry ("put"
22 // or "delete") that remembers the server ETag the change is based on.
23 // 2. flush() pushes due entries: PUT of the stored raw card with If-Match
24 // (If-None-Match for new cards, whose href is a fresh UUID), DELETE with
25 // If-Match.
26 // 3. Success adopts the pushed card as the new merge base. A PUT answer
27 // without ETag (SabreDAV changed the card while storing it) is followed
28 // by a GET that adopts the server's version (CB-13).
29 // 4. Transport errors and 5xx keep the edit and retry with backoff
30 // (30 s … 30 min); nothing is rolled back (CB-04).
31 // 5. 412: the server version is fetched and merged on property level
32 // (VCardMerge). Non-overlapping changes are pushed automatically; an
33 // overlap marks the entry "conflict" with the server version kept for
34 // resolveConflict() ("mine" / "server" / "both").
35 // 6. A move is a create in the target book and a delete in the source
36 // that waits for the create (dependsOn): a failed step retries and
37 // never leaves the card in both books for good (CB-11).
38 //
39 // The local book ("On this computer") has no server: its changes are only
40 // local. Read-only books and unreadable cards refuse writes.
41 class ContactOutbox : public QObject {
42 96 Q_OBJECT
43 public:
44 struct Endpoint {
45 QString serverUrl;
46 QString username;
47 QByteArray password;
48 };
49 using EndpointCallback = std::function<void(std::optional<Endpoint>)>;
50 struct Deps {
51 ContactStore *store = nullptr;
52 // Asynchronous (keyring); called at most once per account and flush.
53 std::function<void(QObject *owner, const QString &accountId, EndpointCallback callback)> endpoint;
54 std::function<CardDavClient *(const QString &url, const QString &user, const QString &password)> makeClient;
55 std::function<bool()> networkAllowed;
56 std::function<qint64()> now; // epoch seconds (tests inject a clock)
57 // Sprint 96: own push registration of a book ("" = none) for the
58 // Push-Dont-Notify header of PUT/DELETE.
59 std::function<QString(const QString &accountId, const QString &bookPath)> pushDontNotify;
60 };
61 explicit ContactOutbox(Deps deps, QObject *parent = nullptr);
62
63 // Each returns the card key ("<bookId>/<uid>") or an empty string with
64 // *error set (user-facing, no technical detail).
65 QString saveCard(qint64 bookId, const QString &uid, const ContactCard &edited, QString *error = nullptr);
66 QString createCard(qint64 bookId, const ContactCard &card, QString *error = nullptr);
67 // S97.5.8: an imported vCard. A 3.0/4.0 card with a UID is stored as
68 // written (lossless); a 2.1 card or one without UID is created from its
69 // parsed values. Returns the new key, "" with *error.
70 QString importRaw(qint64 bookId, const QString &raw, QString *error = nullptr);
71 QString moveCard(qint64 fromBookId, const QString &uid, qint64 toBookId, QString *error = nullptr);
72 // Replaces the raw card (undo of an edit, conflict "keep mine").
73 bool replaceRaw(qint64 bookId, const QString &uid, const QString &raw);
74 // Removes the card; *removed receives it for undo (restoreCard).
75 bool removeCard(qint64 bookId, const QString &uid, StoredCard *removed = nullptr);
76 bool restoreCard(const StoredCard &card);
77 // choice: "mine" (overwrite the server), "server" (take the server
78 // version), "both" (take the server version and keep mine as a copy).
79 bool resolveConflict(qint64 bookId, const QString &uid, const QString &choice);
80
81 // ─── Sprint 107: the conflict resolver (ContactOutboxResolve.cpp) ───
82 struct ConflictInputs {
83 bool valid = false;
84 QString base; // the last server version (a moved card: its source's)
85 QString local; // the card on this computer ("" = deleted here)
86 QString server; // the server version ("" = deleted there)
87 QString serverEtag; // identity of the shown server version (D-9)
88 QString operation; // "put" or "delete"
89 QStringList fields; // ContactOutboxEntry::conflictFields
90 qint64 movedFrom = 0; // D-22: the book the card was moved from
91 };
92 ConflictInputs conflictInputs(qint64 bookId, const QString &uid) const;
93 // S107.3.3: writes a resolution (the resolved card replaces the local one,
94 // the server version becomes the merge base, pushed against
95 // `serverEtag`). False when the conflict changed meanwhile or the card
96 // cannot be read losslessly.
97 bool resolveWith(qint64 bookId, const QString &uid, const QString &resolved, const QString &serverEtag,
98 const QString &keptSide);
99 // S107.3.6: fetches the server version again (newer → reload or merge).
100 void refreshConflict(qint64 bookId, const QString &uid);
101 // S107.3.7 (D-21): restores the version a resolution replaced.
102 bool restoreFromHistory(qint64 historyId);
103 void flush(bool ignoreBackoff = false);
104 20 void beginBatch() { ++m_batchDepth; }
105 void endBatch();
106 static qint64 backoffSeconds(int attempts);
107
108 signals:
109 // Card keys whose stored card or sync state changed (per-card updates).
110 void cardsChanged(const QStringList &keys);
111 void statusMessage(const QString &message);
112
113 private:
114 std::optional<AddressBookInfo> writableBook(qint64 bookId, QString *error) const;
115 void pushAccount(const QString &accountId, const QList<ContactOutboxEntry> &entries, const Endpoint &endpoint);
116 void push(const ContactOutboxEntry &entry, CardDavClient *client);
117 void onPutFinished(const ContactOutboxEntry &entry, const QString &body, const DavResponse &response,
118 CardDavClient *client);
119 void adoptPushed(const ContactOutboxEntry &entry, const QString &body, const QString &href, const QString &etag,
120 const QString &serverRaw);
121 void onDeleteFinished(const ContactOutboxEntry &entry, const DavResponse &response, CardDavClient *client);
122 void onServerVersion(const ContactOutboxEntry &entry, const DavResponse &response);
123 void retryLater(const ContactOutboxEntry &entry, const QString &error, bool permanent = false);
124 void markConflict(const ContactOutboxEntry &entry, const QString &serverRaw, const QString &serverEtag,
125 const QStringList &fields);
126 bool adoptServerVersion(const ContactOutboxEntry &entry, const QString &raw, const QString &etag);
127 // D-22: the source delete of a move met a changed source card.
128 void mergeMovedCard(const ContactOutboxEntry &entry, const QString &theirs, const QString &etag);
129 void recordHistory(qint64 bookId, const QString &uid, const QString &keptSide, const QString &losingText);
130 std::optional<ContactOutboxEntry> moveSourceOf(const ContactOutboxEntry &target) const;
131 void finishInFlight(const ContactOutboxEntry &entry);
132 void scheduleRetry();
133 void changed(const QStringList &keys);
134 static QString key(qint64 bookId, const QString &uid);
135
136 int m_batchDepth = 0;
137 QStringList m_batchKeys;
138 Deps m_deps;
139 QSet<QString> m_inFlight;
140 QSet<QString> m_resolving; // accounts whose credentials are being read
141 QSet<QString> m_refreshAgain; // S107.3.6: a check asked for while one runs
142 QTimer m_retryTimer;
143 };
144