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

app/core/CalendarSyncCoordinator.h
Line Branch Exec Source
1 #pragma once
2
3 #include "service/CalDavXml.h"
4
5 #include <QObject>
6 #include <QHash>
7 #include <QSet>
8 #include <QStringList>
9 #include <QTimer>
10 #include <QPointer>
11 #include "data/DavCredentials.h"
12 #include <functional>
13 #include <memory>
14 #include <optional>
15
16 #include "data/CalendarModels.h"
17 #include "CalendarAccountSync.h"
18
19 class CalendarOutbox;
20 class CalendarSyncApply;
21 class DavSyncScheduler;
22 class CalendarStore;
23 class CalendarSyncStatus;
24 class CalendarSubscriptions;
25 struct DavCollectionChanges;
26 class CalDavClient;
27
28 // Session-owned background CalDAV sync (D2): ports the Widgets MainWindow
29 // sync loop (account/config scan, credential resolution, per-account
30 // CalDavClient wiring, store transactions) into the application core so the
31 // Quick frontend gets the same freshness. Owns its own CalendarStore write
32 // connection; SQLite WAL keeps it safe beside readers and the Widgets
33 // process. The Quick projections listen to storeChanged() and reload.
34 //
35 // Event write-back (Sprint 93 S93.1.6) goes through CalendarOutbox: the
36 // store changes at once, the DAV resource is queued persistently and pushed
37 // with retry/backoff; 412 merges or marks a conflict; nothing is rolled back.
38 // Sync never overwrites a resource that still has a queued local change.
39 //
40 // Task write-back (D6) keeps the Widgets contract: local-first, best-effort
41 // push, rollback on failure while that operation is still the latest write
42 // for the complete identity.
43 //
44 // Credentials are resolved through an injectable resolver (defaults to
45 // asynchronous DavCredentials lookup for sync) so tests can stub the keychain.
46 class CalendarFreeBusy;
47
48 class CalendarSyncCoordinator : public QObject {
49 6 Q_OBJECT
50 public:
51 using CredentialResolver =
52 std::function<QByteArray(const QString &accountId, const QString &serverUrl,
53 const QString &username)>;
54 using AsyncCredentialResolver = std::function<void(QObject *owner,
55 const QString &accountId, const QString &serverUrl, const QString &username,
56 DavCredentials::PasswordCallback callback)>;
57 // clientFactory for tests: returns an unstarted CalDavClient; the
58 // coordinator connects its store handlers and starts discovery.
59 using ClientFactory =
60 std::function<CalDavClient *(const QString &serverUrl, const QString &username,
61 const QString &password)>;
62
63 explicit CalendarSyncCoordinator(const QString &calendarDbPath, QObject *parent = nullptr);
64 ~CalendarSyncCoordinator() override;
65
66 bool open();
67 // Reads caldav/syncIntervalMin (0 disables) and starts the periodic
68 // timer plus the initial delayed sync, mirroring the Widgets setup.
69 void startBackgroundSync();
70 void stopBackgroundSync();
71 // Offline profiles (--offline, disabled network) must not touch the
72 // keyring or DAV servers. Unset means network is always allowed.
73 828 void setNetworkGate(std::function<bool()> gate) { m_networkGate = std::move(gate); }
74 // Manual "Sync now" of every account (queued behind running batches).
75 Q_INVOKABLE void triggerSync();
76 // S106-055: the DAV account was removed — its calendars, their events,
77 // tasks and queued writes (cascade) and its sync status leave the store.
78 // They used to stay writable targets whose writes queued forever.
79 void forgetAccount(const QString &accountId);
80 // Sprint 96 S96.3.1: targeted read requests (push, recovery) and a
81 // full run of one account; both go through the scheduler.
82 void requestCollections(const QString &accountId, const QStringList &paths, unsigned reasons);
83 void requestAccount(const QString &accountId, unsigned reasons);
84 1361 DavSyncScheduler *scheduler() const { return m_scheduler; }
85 // Sprint 96 S96.3.7: own registration URL per collection for
86 // Push-Dont-Notify on outbox writes (set by the DAV-Push coordinator).
87 851 void setPushDontNotifyProvider(std::function<QString(const QString &, const QString &)> provider) {
88 851 m_pushDontNotify = std::move(provider);
89 851 }
90 // Accounts with at least one selected calendar.
91 static QStringList syncAccountIds();
92 // The configured account (nullopt when missing or ambiguous).
93 static std::optional<CalendarSyncAccount> syncConfiguration(const QString &accountId);
94
95 // Durable local-first task writes. Success reports local persistence;
96 // transport retries and merge conflicts retain the local edit.
97 void beginTaskBatch();
98 void endTaskBatch();
99 bool saveTask(const CalendarTask &task, bool wasNew);
100 // Destination PUT is acknowledged before the source DELETE is sent.
101 bool moveTask(const CalendarTask &source, const CalendarTask &target);
102 bool removeTask(const QString &uid, const QString &accountId,
103 const QString &calendarPath);
104 // D5 / Sprint 93: event write-back through the outbox. `wasNew` is kept
105 // for the call sites; the outbox derives create/update from the stored
106 // server ETag.
107 bool saveEvent(const CalendarEvent &event, bool wasNew);
108 bool removeEvent(const QString &uid, const QString &accountId,
109 const QString &calendarPath);
110 // Sprint 93 S93.5.8: a color chosen in MailJD also goes to the server
111 // (APPLE:calendar-color), so other clients show it. Best effort: the
112 // local color stays in any case (discovery never replaces a stored
113 // color); read-only collections and offline accounts are skipped.
114 void pushCalendarColor(const QString &accountId, const QString &calendarPath, const QString &color);
115 static QString serverColorKey(const QString &accountId, const QString &calendarPath);
116 // Undo: replace every row of a resource with `rows` (empty = remove).
117 bool restoreEventSeries(const QString &uid, const QString &accountId,
118 const QString &calendarPath, const QList<CalendarEvent> &rows);
119 // F-52 (SPRINT-104): occurrence-scoped invitation writes — merge
120 // RECURRENCE-ID rows into the stored series, or cancel exactly one
121 // occurrence (exception row + EXDATE on the master).
122 bool applyOccurrenceUpdate(const QString &uid, const QString &accountId,
123 const QString &calendarPath,
124 const QList<CalendarEvent> &exceptionRows);
125 bool applyOccurrenceCancel(const QString &uid, const QString &accountId,
126 const QString &calendarPath,
127 const QDateTime &recurrenceId);
128 // choice: "mine", "server" or "both" (see CalendarOutbox).
129 bool resolveEventConflict(const QString &uid, const QString &accountId,
130 const QString &calendarPath, const QString &choice);
131 // "Retry now": pushes queued changes without waiting for the backoff.
132 Q_INVOKABLE void retryPendingWrites();
133 std::optional<CalendarOutboxEntry> pendingWrite(const QString &uid, const QString &accountId,
134 const QString &calendarPath) const;
135 // Test seam: epoch-seconds clock of the outbox backoff.
136 void setClock(std::function<qint64()> clock);
137 // Calendars usable as write targets, ordered by display name.
138 QList<CalendarInfo> calendars() const;
139 // Read access for editor lookups (exception resolution, prefill).
140 42721 CalendarStore *store() const { return m_store; }
141 // Sprint 107: the resolver writes resolutions through the outbox.
142 1192 CalendarOutbox *outbox() const { return m_outbox; }
143 // Single-record lookups on the coordinator's store connection (editor
144 // prefill, toggles, save merges).
145 std::optional<CalendarTask> taskFor(const QString &uid, const QString &accountId,
146 const QString &calendarPath) const;
147 std::optional<CalendarEvent> eventFor(const QString &uid, const QString &accountId,
148 const QString &calendarPath) const;
149
150 void setCredentialResolver(CredentialResolver resolver);
151 3 void setAsyncCredentialResolver(AsyncCredentialResolver resolver) { m_asyncCredentialResolver = std::move(resolver); }
152 119 void setClientFactory(ClientFactory factory) { m_clientFactory = std::move(factory); }
153
154 static int configuredIntervalMinutes();
155 // Sprint 93 S93.6.3: per-account last success / last error of the
156 // background sync (settings, sidebar and calendar banner).
157 3348 CalendarSyncStatus *syncStatus() const { return m_status; }
158 // Sprint 93 S93.5.4: ICS URL subscriptions (refreshed with every sync run).
159 795 CalendarSubscriptions *subscriptions() const { return m_subscriptions; }
160 // Sprint 108 S108.8.5 (D-25): attendees' busy times between from and to
161 // through the account's schedule outbox; `done` runs once, with one
162 // entry per attendee (known = false: no information).
163 void requestFreeBusy(const QString &accountId, const QString &organizer, const QStringList &attendees,
164 const QDateTime &from, const QDateTime &to,
165 std::function<void(const QList<CalDavXml::FreeBusy> &)> done);
166 CalendarFreeBusy *freeBusy() const { return m_freeBusy; }
167 4189 bool syncRunning() const { return !m_runs.isEmpty(); }
168 448 bool accountRunning(const QString &accountId) const { return m_runs.contains(accountId); }
169
170 signals:
171 // Emitted after a successful events/tasks transaction and after calendar
172 // discovery upserts; consumers reload their projections.
173 void storeChanged();
174 void statusMessage(const QString &message);
175 void syncStateChanged(bool running);
176 // Sprint 96: per-account results and discovery (push offers).
177 void accountSyncFinished(const CalendarSyncResult &result);
178 void collectionsDiscovered(const QString &accountId, const QList<CalendarInfo> &calendars);
179
180 private:
181 struct AccountEndpoint {
182 QString accountId;
183 QString serverUrl;
184 QString username;
185 QByteArray password;
186 };
187 bool startAccountSync(const CalendarSyncRequest &request);
188 void adoptServerColor(const QString &accountId, const QString &calendarPath, const QString &serverColor);
189 bool readOnlyCalendar(const QString &accountId, const QString &calendarPath) const;
190 std::optional<AccountEndpoint> endpointForAccount(const QString &accountId) const;
191 CalDavClient *makeClient(const QString &serverUrl, const QString &username,
192 const QString &password);
193 void deleteMovedTaskSource(const CalendarTask &source,
194 const CalendarTask &target,
195 const QStringList &identity, quint64 revision);
196
197 CalendarOutbox *m_outbox = nullptr;
198 CalendarSyncStatus *m_status = nullptr;
199 CalendarSubscriptions *m_subscriptions = nullptr;
200 CalendarFreeBusy *m_freeBusy = nullptr;
201 std::function<qint64()> m_clock;
202 quint64 m_nextWriteRevision = 0;
203 QHash<QStringList, quint64> m_writeRevisions;
204 CalendarStore *m_store = nullptr;
205 QTimer m_initialSyncTimer;
206 DavSyncScheduler *m_scheduler = nullptr;
207 QHash<QString, QPointer<CalendarAccountSync>> m_runs;
208 std::function<QString(const QString &, const QString &)> m_pushDontNotify;
209 AsyncCredentialResolver m_asyncCredentialResolver;
210 std::function<bool()> m_networkGate;
211 CredentialResolver m_credentialResolver;
212 ClientFactory m_clientFactory;
213 // S93.6.2 / S96.3.3: collections (calendarSelectionKey) whose server
214 // rejected sync-collection (session); other collections keep the report.
215 QSet<QString> m_queryOnlyCollections;
216 // S96.3.0: every read result goes through one atomic apply contract.
217 std::unique_ptr<CalendarSyncApply> m_apply;
218
219 };
220