• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 1658

10 Sep 2023 11:58PM UTC coverage: 91.222% (-0.04%) from 91.263%
1658

push

Evergreen

GitHub
Merge pull request #6938 from realm/tg/tls-error-reporting

95840 of 175760 branches covered (0.0%)

142 of 146 new or added lines in 7 files covered. (97.26%)

290 existing lines in 22 files now uncovered.

233460 of 255926 relevant lines covered (91.22%)

6997123.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.5
/test/object-store/sync/session/session.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2017 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <util/event_loop.hpp>
20
#include <util/test_utils.hpp>
21
#include <util/sync/session_util.hpp>
22

23
#include <realm/object-store/feature_checks.hpp>
24
#include <realm/object-store/object_schema.hpp>
25
#include <realm/object-store/object_store.hpp>
26
#include <realm/object-store/property.hpp>
27
#include <realm/object-store/schema.hpp>
28

29
#include <realm/util/time.hpp>
30
#include <realm/util/scope_exit.hpp>
31

32
#include <catch2/catch_all.hpp>
33

34
#include <atomic>
35
#include <chrono>
36
#include <fstream>
37
#ifndef _WIN32
38
#include <unistd.h>
39
#endif
40

41
using namespace realm;
42
using namespace realm::util;
43

44
static const std::string dummy_auth_url = "https://realm.example.org";
45
static const std::string dummy_device_id = "123400000000000000000000";
46

47
static std::shared_ptr<SyncUser> get_user(const std::shared_ptr<app::App>& app)
48
{
58✔
49
    return app->sync_manager()->get_user("user_id", ENCODE_FAKE_JWT("fake_refresh_token"),
58✔
50
                                         ENCODE_FAKE_JWT("fake_access_token"), dummy_auth_url, dummy_device_id);
58✔
51
}
58✔
52

53
TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") {
12✔
54
    if (!EventLoop::has_implementation())
12✔
UNCOV
55
        return;
×
56

6✔
57
    TestSyncManager init_sync_manager;
12✔
58
    auto& server = init_sync_manager.sync_server();
12✔
59
    auto app = init_sync_manager.app();
12✔
60
    const std::string realm_base_url = server.base_url();
12✔
61

6✔
62
    SECTION("a SyncUser can properly retrieve its owned sessions") {
12✔
63
        auto user = get_user(app);
2✔
64
        auto session1 = sync_session(user, "/test1a-1");
2✔
65
        auto session2 = sync_session(user, "/test1a-2");
2✔
66
        EventLoop::main().run_until([&] {
3✔
67
            return sessions_are_active(*session1, *session2);
3✔
68
        });
3✔
69

1✔
70
        // Check the sessions on the SyncUser.
1✔
71
        REQUIRE(user->all_sessions().size() == 2);
2!
72
        auto s1 = user->session_for_on_disk_path(session1->path());
2✔
73
        REQUIRE(s1 == session1);
2!
74
        auto s2 = user->session_for_on_disk_path(session2->path());
2✔
75
        REQUIRE(s2 == session2);
2!
76
    }
2✔
77

6✔
78
    SECTION("a SyncUser properly unbinds its sessions upon logging out") {
12✔
79
        auto user = get_user(app);
2✔
80
        auto session1 = sync_session(user, "/test1b-1");
2✔
81
        auto session2 = sync_session(user, "/test1b-2");
2✔
82
        EventLoop::main().run_until([&] {
3✔
83
            return sessions_are_active(*session1, *session2);
3✔
84
        });
3✔
85

1✔
86
        // Log the user out.
1✔
87
        user->log_out();
2✔
88
        // The sessions should log themselves out.
1✔
89
        EventLoop::main().run_until([&] {
3✔
90
            return sessions_are_inactive(*session1, *session2);
3✔
91
        });
3✔
92
        CHECK(user->all_sessions().size() == 0);
2!
93
    }
2✔
94

6✔
95
    SECTION("a SyncUser defers binding new sessions until it is logged in") {
12✔
96
        auto user = get_user(app);
2✔
97
        user->log_out();
2✔
98
        REQUIRE(user->state() == SyncUser::State::LoggedOut);
2!
99
        auto session1 = sync_session(user, "/test1c-1");
2✔
100
        auto session2 = sync_session(user, "/test1c-2");
2✔
101
        // Run the runloop many iterations to see if the sessions spuriously bind.
1✔
102
        spin_runloop();
2✔
103
        REQUIRE(session1->state() == SyncSession::State::Inactive);
2!
104
        REQUIRE(session2->state() == SyncSession::State::Inactive);
2!
105
        REQUIRE(user->all_sessions().size() == 0);
2!
106
        // Log the user back in via the sync manager.
1✔
107
        user = get_user(app);
2✔
108
        EventLoop::main().run_until([&] {
3✔
109
            return sessions_are_active(*session1, *session2);
3✔
110
        });
3✔
111
        REQUIRE(user->all_sessions().size() == 2);
2!
112
    }
2✔
113

6✔
114
    SECTION("a SyncUser properly rebinds existing sessions upon logging back in") {
12✔
115
        auto user = get_user(app);
2✔
116
        auto session1 = sync_session(user, "/test1d-1");
2✔
117
        auto session2 = sync_session(user, "/test1d-2");
2✔
118
        // Make sure the sessions are bound.
1✔
119
        EventLoop::main().run_until([&] {
3✔
120
            return sessions_are_active(*session1, *session2);
3✔
121
        });
3✔
122
        REQUIRE(user->all_sessions().size() == 2);
2!
123
        // Log the user out.
1✔
124
        user->log_out();
2✔
125
        REQUIRE(user->state() == SyncUser::State::LoggedOut);
2!
126
        // Run the runloop many iterations to see if the sessions spuriously rebind.
1✔
127
        spin_runloop();
2✔
128
        REQUIRE(session1->state() == SyncSession::State::Inactive);
2!
129
        REQUIRE(session2->state() == SyncSession::State::Inactive);
2!
130
        REQUIRE(user->all_sessions().size() == 0);
2!
131
        // Log the user back in via the sync manager.
1✔
132
        user = get_user(app);
2✔
133
        EventLoop::main().run_until([&] {
3✔
134
            return sessions_are_active(*session1, *session2);
3✔
135
        });
3✔
136
        REQUIRE(user->all_sessions().size() == 2);
2!
137
    }
2✔
138

6✔
139
    SECTION("sessions that were destroyed can be properly recreated when requested again") {
12✔
140
        const std::string path = "/test1e";
2✔
141
        std::weak_ptr<SyncSession> weak_session;
2✔
142
        std::string on_disk_path;
2✔
143
        util::Optional<SyncConfig> config;
2✔
144
        auto user = get_user(app);
2✔
145
        {
2✔
146
            // Create the session within a nested scope, so we can control its lifetime.
1✔
147
            auto session = sync_session(
2✔
148
                user, path, [](auto, auto) {}, SyncSessionStopPolicy::Immediately, &on_disk_path);
1✔
149
            weak_session = session;
2✔
150
            config = session->config();
2✔
151
            REQUIRE(on_disk_path.size() > 0);
2!
152
            REQUIRE(weak_session.lock());
2!
153
        }
2✔
154
        // Wait for the session to die. It may not happen immediately if a progress or error handler
1✔
155
        // is called on a background thread and keeps the session alive past the scope of the above block.
1✔
156
        EventLoop::main().run_until([&] {
3✔
157
            return weak_session.expired();
3✔
158
        });
3✔
159

1✔
160
        // The next time we request it, it'll be created anew.
1✔
161
        // The call to `get_session()` should result in `SyncUser::register_session()` being called.
1✔
162
        auto session = sync_session(
2✔
163
            user, path, [](auto, auto) {}, SyncSessionStopPolicy::Immediately, &on_disk_path);
1✔
164
        CHECK(session);
2!
165
        session = user->session_for_on_disk_path(on_disk_path);
2✔
166
        CHECK(session);
2!
167
    }
2✔
168

6✔
169
    SECTION("a user can create multiple sessions for the same URL") {
12✔
170
        auto user = get_user(app);
2✔
171
        // Note that this should put the sessions at different paths.
1✔
172
        auto session1 = sync_session(user, "/test");
2✔
173
        auto session2 = sync_session(user, "/test");
2✔
174
        REQUIRE(session1 != session2);
2!
175
        REQUIRE(session1->path() != session2->path());
2!
176
    }
2✔
177
}
12✔
178

179
TEST_CASE("sync: log-in", "[sync][session]") {
2✔
180
    if (!EventLoop::has_implementation())
2✔
UNCOV
181
        return;
×
182

1✔
183
    // Disable file-related functionality and metadata functionality for testing purposes.
1✔
184
    TestSyncManager init_sync_manager;
2✔
185
    auto app = init_sync_manager.app();
2✔
186
    auto user = get_user(app);
2✔
187

1✔
188
    SECTION("Can log in") {
2✔
189
        std::atomic<int> error_count(0);
2✔
190
        auto session = sync_session(user, "/test", [&](auto, auto) {
1✔
UNCOV
191
            ++error_count;
×
UNCOV
192
        });
×
193

1✔
194
        std::atomic<bool> download_did_complete(false);
2✔
195
        session->wait_for_download_completion([&](auto) {
2✔
196
            download_did_complete = true;
2✔
197
        });
2✔
198
        EventLoop::main().run_until([&] {
10,830✔
199
            return download_did_complete.load() || error_count > 0;
10,830✔
200
        });
10,830✔
201
        CHECK(error_count == 0);
2!
202
    }
2✔
203

1✔
204
    // TODO: write a test that logs out a Realm with multiple sessions, then logs it back in?
1✔
205
    // TODO: write tests that check that a Session properly handles various types of errors reported via its callback.
1✔
206
}
2✔
207

208
TEST_CASE("SyncSession: close() API", "[sync][session]") {
4✔
209
    TestSyncManager init_sync_manager;
4✔
210
    auto app = init_sync_manager.app();
4✔
211
    auto user = get_user(app);
4✔
212

2✔
213
    SECTION("Behaves properly when called on session in the 'active' or 'inactive' state") {
4✔
214
        auto session = sync_session(user, "/test-close-for-active");
2✔
215
        EventLoop::main().run_until([&] {
3✔
216
            return sessions_are_active(*session);
3✔
217
        });
3✔
218
        REQUIRE(sessions_are_active(*session));
2!
219
        session->close();
2✔
220
        EventLoop::main().run_until([&] {
102✔
221
            return sessions_are_inactive(*session);
102✔
222
        });
102✔
223
        REQUIRE(sessions_are_inactive(*session));
2!
224
        // Try closing the session again. This should be a no-op.
1✔
225
        session->close();
2✔
226
        REQUIRE(sessions_are_inactive(*session));
2!
227
    }
2✔
228

2✔
229
    SECTION("Close session after it was detached from the SyncManager") {
4✔
230
        auto session = sync_session(user, "/test-close-after-detach");
2✔
231
        session->detach_from_sync_manager();
2✔
232
        REQUIRE_NOTHROW(session->close());
2✔
233
    }
2✔
234
}
4✔
235

236
TEST_CASE("SyncSession: pause()/resume() API", "[sync][session]") {
4✔
237
    TestSyncManager init_sync_manager;
4✔
238
    auto app = init_sync_manager.app();
4✔
239
    auto user = get_user(app);
4✔
240

2✔
241
    auto session = sync_session(user, "/test-close-for-active");
4✔
242
    EventLoop::main().run_until([&] {
6✔
243
        return sessions_are_active(*session);
6✔
244
    });
6✔
245
    REQUIRE(sessions_are_active(*session));
4!
246

2✔
247
    SECTION("making the session inactive and then pausing it should end up in the paused state") {
4✔
248
        session->force_close();
2✔
249
        EventLoop::main().run_until([&] {
3✔
250
            return sessions_are_inactive(*session);
3✔
251
        });
3✔
252
        REQUIRE(sessions_are_inactive(*session));
2!
253

1✔
254
        session->pause();
2✔
255
        EventLoop::main().run_until([&] {
3✔
256
            return session->state() == SyncSession::State::Paused;
3✔
257
        });
3✔
258
        REQUIRE(session->state() == SyncSession::State::Paused);
2!
259
    }
2✔
260

2✔
261
    SECTION("pausing from the active state should end up in the paused state") {
4✔
262
        session->pause();
2✔
263
        EventLoop::main().run_until([&] {
3✔
264
            return session->state() == SyncSession::State::Paused;
3✔
265
        });
3✔
266
        REQUIRE(session->state() == SyncSession::State::Paused);
2!
267

1✔
268
        // Pausing it again should be a no-op
1✔
269
        session->pause();
2✔
270
        REQUIRE(session->state() == SyncSession::State::Paused);
2!
271

1✔
272
        // "Logging out" the session should be a no-op.
1✔
273
        session->force_close();
2✔
274
        REQUIRE(session->state() == SyncSession::State::Paused);
2!
275
    }
2✔
276

2✔
277
    // Reviving the session via revive_if_needed() should be a no-op.
2✔
278
    session->revive_if_needed();
4✔
279
    REQUIRE(session->state() == SyncSession::State::Paused);
4!
280

2✔
281
    // Only resume() can revive a paused session.
2✔
282
    session->resume();
4✔
283
    EventLoop::main().run_until([&] {
6✔
284
        return sessions_are_active(*session);
6✔
285
    });
6✔
286
    REQUIRE(sessions_are_active(*session));
4!
287
}
4✔
288

289
TEST_CASE("SyncSession: shutdown_and_wait() API", "[sync][session]") {
2✔
290
    TestSyncManager init_sync_manager;
2✔
291
    auto app = init_sync_manager.app();
2✔
292
    auto user = get_user(app);
2✔
293

1✔
294
    SECTION("Behaves properly when called on session in the 'active' or 'inactive' state") {
2✔
295
        auto session = sync_session(user, "/test-close-for-active");
2✔
296
        EventLoop::main().run_until([&] {
3✔
297
            return sessions_are_active(*session);
3✔
298
        });
3✔
299
        REQUIRE(sessions_are_active(*session));
2!
300
        session->shutdown_and_wait();
2✔
301
        session->close();
2✔
302
        EventLoop::main().run_until([&] {
3✔
303
            return sessions_are_inactive(*session);
3✔
304
        });
3✔
305
        REQUIRE(sessions_are_inactive(*session));
2!
306
        // Try closing the session again. This should be a no-op.
1✔
307
        session->close();
2✔
308
        REQUIRE(sessions_are_inactive(*session));
2!
309
    }
2✔
310
}
2✔
311

312
TEST_CASE("SyncSession: update_configuration()", "[sync][session]") {
4✔
313
    TestSyncManager init_sync_manager({}, {false});
4✔
314
    auto app = init_sync_manager.app();
4✔
315
    auto user = get_user(app);
4✔
316
    auto session = sync_session(user, "/update_configuration");
4✔
317

2✔
318
    SECTION("updates reported configuration") {
4✔
319
        auto config = session->config();
2✔
320
        REQUIRE(config.client_validate_ssl);
2!
321
        config.client_validate_ssl = false;
2✔
322
        session->update_configuration(std::move(config));
2✔
323
        REQUIRE_FALSE(session->config().client_validate_ssl);
2!
324
    }
2✔
325

2✔
326
    SECTION("handles reconnects while it's trying to deactivate session") {
4✔
327
        bool wait_called = false;
2✔
328
        session->wait_for_download_completion([&](Status s) {
2✔
329
            REQUIRE(s == ErrorCodes::OperationAborted);
2!
330
            REQUIRE(session->config().client_validate_ssl);
2!
331
            REQUIRE(session->state() == SyncSession::State::Inactive);
2!
332

1✔
333
            wait_called = true;
2✔
334
            session->revive_if_needed();
2✔
335

1✔
336
            REQUIRE(session->state() != SyncSession::State::Inactive);
2!
337
        });
2✔
338

1✔
339
        auto config = session->config();
2✔
340
        config.client_validate_ssl = false;
2✔
341
        session->update_configuration(std::move(config));
2✔
342
        REQUIRE(wait_called);
2!
343
    }
2✔
344
}
4✔
345

346
TEST_CASE("sync: error handling", "[sync][session]") {
12✔
347
    TestSyncManager init_sync_manager;
12✔
348
    auto app = init_sync_manager.app();
12✔
349

6✔
350
    std::string on_disk_path;
12✔
351
    std::optional<SyncError> error;
12✔
352
    std::mutex mutex;
12✔
353
    auto store_sync_error = [&](auto, SyncError e) {
11✔
354
        std::lock_guard lock(mutex);
10✔
355
        error = e;
10✔
356
    };
10✔
357

6✔
358
    SECTION("reports DNS error") {
12✔
359
        app->sync_manager()->set_sync_route("ws://invalid.com:9090");
2✔
360

1✔
361
        auto user = get_user(app);
2✔
362
        auto session = sync_session(user, "/test", store_sync_error);
2✔
363
        timed_wait_for(
2✔
364
            [&] {
192,487✔
365
                std::lock_guard lock(mutex);
192,487✔
366
                return error.has_value();
192,487✔
367
            },
192,487✔
368
            std::chrono::seconds(35)); // this sometimes needs to wait for a 30s dns timeout
2✔
369
        REQUIRE(error);
2!
370
        CHECK(error->status.code() == ErrorCodes::SyncConnectFailed);
2!
371
        // May end with either (authoritative) or (non-authoritative)
1✔
372
        CHECK_THAT(error->status.reason(), Catch::Matchers::StartsWith("Failed to connect to sync: Host not found"));
2✔
373
    }
2✔
374

6✔
375
#ifndef SWIFT_PACKAGE // requires test resource files
12✔
376
    SECTION("reports TLS error as handshake failed") {
12✔
377
        TestSyncManager ssl_sync_manager({}, {StartImmediately{true}, EnableSSL{true}});
2✔
378
        auto app = ssl_sync_manager.app();
2✔
379

1✔
380
        auto user = get_user(app);
2✔
381
        auto session = sync_session(user, "/test", store_sync_error);
2✔
382
        timed_wait_for([&] {
52,145✔
383
            std::lock_guard lock(mutex);
52,145✔
384
            return error.has_value();
52,145✔
385
        });
52,145✔
386
        REQUIRE(error);
2!
387
        CHECK(error->status.code() == ErrorCodes::TlsHandshakeFailed);
2!
388
#if REALM_HAVE_SECURE_TRANSPORT
1✔
389
        CHECK(error->status.reason() ==
1!
390
              "TLS handshake failed: SecureTransport error: invalid certificate chain (-9807)");
1✔
391
#else
392
        // The exact error code seems to vary, so only check the message
1✔
393
        CHECK_THAT(error->status.reason(),
1✔
394
                   Catch::Matchers::StartsWith("TLS handshake failed: OpenSSL error: certificate verify failed"));
1✔
395
#endif
1✔
396
    }
2✔
397
#endif
12✔
398

6✔
399
    using ProtocolError = realm::sync::ProtocolError;
12✔
400
    using ProtocolErrorInfo = realm::sync::ProtocolErrorInfo;
12✔
401

6✔
402
    SECTION("Doesn't treat unknown system errors as being fatal") {
12✔
403
        auto user = get_user(app);
2✔
404
        auto session = sync_session(user, "/test", store_sync_error);
2✔
405
        EventLoop::main().run_until([&] {
3✔
406
            return sessions_are_active(*session);
3✔
407
        });
3✔
408

1✔
409
        sync::SessionErrorInfo err{Status{ErrorCodes::UnknownError, "unknown error"}, true};
2✔
410
        err.server_requests_action = ProtocolErrorInfo::Action::Transient;
2✔
411
        SyncSession::OnlyForTesting::handle_error(*session, std::move(err));
2✔
412
        CHECK(!sessions_are_inactive(*session));
2!
413
        // Error is non-fatal so it's not reported to the error handler
1✔
414
        std::lock_guard lock(mutex);
2✔
415
        REQUIRE_FALSE(error);
2!
416
    }
2✔
417

6✔
418
    SECTION("Properly handles a client reset error") {
12✔
419
        auto user = get_user(app);
6✔
420
        auto session = sync_session(user, "/test", store_sync_error);
6✔
421
        std::string on_disk_path = session->path();
6✔
422
        EventLoop::main().run_until([&] {
9✔
423
            return sessions_are_active(*session);
9✔
424
        });
9✔
425

3✔
426
        auto code = GENERATE(ProtocolError::bad_client_file_ident, ProtocolError::bad_server_version,
6✔
427
                             ProtocolError::diverging_histories);
6✔
428

3✔
429
        sync::SessionErrorInfo initial_error{sync::protocol_error_to_status(code, "Something bad happened"), true};
6✔
430
        initial_error.server_requests_action = ProtocolErrorInfo::Action::ClientReset;
6✔
431
        std::time_t just_before_raw = std::time(nullptr);
6✔
432
        SyncSession::OnlyForTesting::handle_error(*session, std::move(initial_error));
6✔
433
        REQUIRE(session->state() == SyncSession::State::Inactive);
6!
434
        std::time_t just_after_raw = std::time(nullptr);
6✔
435
        auto just_before = util::localtime(just_before_raw);
6✔
436
        auto just_after = util::localtime(just_after_raw);
6✔
437
        // At this point error should be populated.
3✔
438
        REQUIRE(error);
6!
439
        CHECK(error->is_client_reset_requested());
6!
440
        CHECK(error->server_requests_action == ProtocolErrorInfo::Action::ClientReset);
6!
441
        // The original file path should be present.
3✔
442
        CHECK(error->user_info[SyncError::c_original_file_path_key] == on_disk_path);
6!
443
        // The path to the recovery file should be present, and should contain all necessary components.
3✔
444
        std::string recovery_path = error->user_info[SyncError::c_recovery_file_path_key];
6✔
445
        auto idx = recovery_path.find("recovered_realm");
6✔
446
        CHECK(idx != std::string::npos);
6!
447
        idx = recovery_path.find(app->sync_manager()->recovery_directory_path());
6✔
448
        CHECK(idx != std::string::npos);
6!
449
        if (just_before.tm_year == just_after.tm_year) {
6✔
450
            idx = recovery_path.find(util::format_local_time(just_after_raw, "%Y"));
6✔
451
            CHECK(idx != std::string::npos);
6!
452
        }
6✔
453
        if (just_before.tm_mon == just_after.tm_mon) {
6✔
454
            idx = recovery_path.find(util::format_local_time(just_after_raw, "%m"));
6✔
455
            CHECK(idx != std::string::npos);
6!
456
        }
6✔
457
        if (just_before.tm_yday == just_after.tm_yday) {
6✔
458
            idx = recovery_path.find(util::format_local_time(just_after_raw, "%d"));
6✔
459
            CHECK(idx != std::string::npos);
6!
460
        }
6✔
461
    }
6✔
462
}
12✔
463

464
TEST_CASE("sync: stop policy behavior", "[sync][session]") {
12✔
465
    const std::string dummy_auth_url = "https://realm.example.org";
12✔
466
    if (!EventLoop::has_implementation())
12✔
UNCOV
467
        return;
×
468

6✔
469
    // Server is initially stopped so we can control when the session exits the dying state.
6✔
470
    TestSyncManager init_sync_manager({}, {false});
12✔
471
    auto& server = init_sync_manager.sync_server();
12✔
472
    auto sync_manager = init_sync_manager.app()->sync_manager();
12✔
473
    auto schema = Schema{
12✔
474
        {"object",
12✔
475
         {
12✔
476
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
12✔
477
             {"value", PropertyType::Int},
12✔
478
         }},
12✔
479
    };
12✔
480

6✔
481
    std::atomic<bool> error_handler_invoked(false);
12✔
482
    Realm::Config config;
12✔
483
    auto user = get_user(init_sync_manager.app());
12✔
484

6✔
485
    auto create_session = [&](SyncSessionStopPolicy stop_policy) {
12✔
486
        auto session = sync_session(
12✔
487
            user, "/test-dying-state",
12✔
488
            [&](auto, auto) {
6✔
UNCOV
489
                error_handler_invoked = true;
×
UNCOV
490
            },
×
491
            stop_policy, nullptr, schema, &config);
12✔
492
        EventLoop::main().run_until([&] {
18✔
493
            return sessions_are_active(*session);
18✔
494
        });
18✔
495

6✔
496
        // Add an object so there's something to upload
6✔
497
        auto r = Realm::get_shared_realm(config);
12✔
498
        TableRef table = ObjectStore::table_for_object_type(r->read_group(), "object");
12✔
499
        r->begin_transaction();
12✔
500
        table->create_object_with_primary_key(0);
12✔
501
        r->commit_transaction();
12✔
502

6✔
503
        return session;
12✔
504
    };
12✔
505

6✔
506
    SECTION("Immediately") {
12✔
507
        SECTION("transitions directly to Inactive even with the server stopped") {
2✔
508
            auto session = create_session(SyncSessionStopPolicy::Immediately);
2✔
509
            session->close();
2✔
510
            REQUIRE(sessions_are_inactive(*session));
2!
511
        }
2✔
512
    }
2✔
513

6✔
514
    SECTION("AfterChangesUploaded") {
12✔
515
        auto session = create_session(SyncSessionStopPolicy::AfterChangesUploaded);
8✔
516
        // Now close the session, causing the state to transition to Dying.
4✔
517
        // (it should remain stuck there until we start the server)
4✔
518
        session->close();
8✔
519
        REQUIRE(session->state() == SyncSession::State::Dying);
8!
520

4✔
521
        SECTION("transitions to Inactive once the server is started") {
8✔
522
            server.start();
2✔
523
            EventLoop::main().run_until([&] {
12,587✔
524
                return sessions_are_inactive(*session);
12,587✔
525
            });
12,587✔
526
        }
2✔
527

4✔
528
        SECTION("transitions back to Active if the session is revived") {
8✔
529
            std::shared_ptr<SyncSession> session2;
2✔
530
            {
2✔
531
                auto realm = Realm::get_shared_realm(config);
2✔
532
                session2 = user->sync_manager()->get_existing_session(config.path);
2✔
533
            }
2✔
534
            REQUIRE(session->state() == SyncSession::State::Active);
2!
535
            REQUIRE(session2 == session);
2!
536
        }
2✔
537

4✔
538
        SECTION("transitions to Inactive if a fatal error occurs") {
8✔
539
            sync::SessionErrorInfo err{Status{ErrorCodes::SyncProtocolInvariantFailed, "Not a real error message"},
2✔
540
                                       sync::IsFatal{true}};
2✔
541
            err.server_requests_action = realm::sync::ProtocolErrorInfo::Action::ProtocolViolation;
2✔
542
            SyncSession::OnlyForTesting::handle_error(*session, std::move(err));
2✔
543
            CHECK(sessions_are_inactive(*session));
2!
544
            // The session shouldn't report fatal errors when in the dying state.
1✔
545
            CHECK(!error_handler_invoked);
2!
546
        }
2✔
547

4✔
548
        SECTION("ignores non-fatal errors and does not transition to Inactive") {
8✔
549
            // Fire a simulated *non-fatal* error.
1✔
550
            sync::SessionErrorInfo err{Status{ErrorCodes::ConnectionClosed, "Not a real error message"},
2✔
551
                                       sync::IsFatal{false}};
2✔
552
            err.server_requests_action = realm::sync::ProtocolErrorInfo::Action::Transient;
2✔
553
            SyncSession::OnlyForTesting::handle_error(*session, std::move(err));
2✔
554
            REQUIRE(session->state() == SyncSession::State::Dying);
2!
555
            CHECK(!error_handler_invoked);
2!
556
        }
2✔
557
    }
8✔
558

6✔
559
    SECTION("can change to Immediately after opening the session") {
12✔
560
        auto session = create_session(SyncSessionStopPolicy::AfterChangesUploaded);
2✔
561
        REQUIRE(session->state() == SyncSession::State::Active);
2!
562

1✔
563
        auto config = session->config();
2✔
564
        config.stop_policy = SyncSessionStopPolicy::Immediately;
2✔
565
        session->update_configuration(std::move(config));
2✔
566

1✔
567
        session->close();
2✔
568
        REQUIRE(sessions_are_inactive(*session));
2!
569
    }
2✔
570
}
12✔
571

572
TEST_CASE("session restart", "[sync][session]") {
2✔
573
    if (!EventLoop::has_implementation())
2✔
UNCOV
574
        return;
×
575

1✔
576
    TestSyncManager init_sync_manager({}, {false});
2✔
577
    auto& server = init_sync_manager.sync_server();
2✔
578
    auto app = init_sync_manager.app();
2✔
579
    Realm::Config config;
2✔
580
    auto schema = Schema{
2✔
581
        {"object",
2✔
582
         {
2✔
583
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
584
             {"value", PropertyType::Int},
2✔
585
         }},
2✔
586
    };
2✔
587

1✔
588
    auto user = get_user(app);
2✔
589
    auto session = sync_session(
2✔
590
        user, "/test-restart", [&](auto, auto) {}, SyncSessionStopPolicy::AfterChangesUploaded, nullptr, schema,
1✔
591
        &config);
2✔
592

1✔
593
    EventLoop::main().run_until([&] {
3✔
594
        return sessions_are_active(*session);
3✔
595
    });
3✔
596

1✔
597
    server.start();
2✔
598

1✔
599
    // Add an object so there's something to upload
1✔
600
    auto realm = Realm::get_shared_realm(config);
2✔
601
    TableRef table = ObjectStore::table_for_object_type(realm->read_group(), "object");
2✔
602
    realm->begin_transaction();
2✔
603
    table->create_object_with_primary_key(0);
2✔
604
    realm->commit_transaction();
2✔
605

1✔
606
    // Close the current session and start a new one
1✔
607
    // The stop policy is ignored when closing the current session
1✔
608
    session->restart_session();
2✔
609

1✔
610
    REQUIRE(session->state() == SyncSession::State::Active);
2!
611
    REQUIRE(!wait_for_upload(*realm));
2!
612
}
2✔
613

614
TEST_CASE("sync: non-synced metadata table doesn't result in non-additive schema changes", "[sync][session]") {
2✔
615
    if (!EventLoop::has_implementation())
2✔
UNCOV
616
        return;
×
617

1✔
618
    // Disable file-related functionality and metadata functionality for testing purposes.
1✔
619
    TestSyncManager init_sync_manager;
2✔
620

1✔
621
    // Create a synced Realm containing a class with two properties.
1✔
622
    {
2✔
623
        SyncTestFile config1(init_sync_manager.app(), "schema-version-test");
2✔
624
        config1.schema_version = 1;
2✔
625
        config1.schema = Schema{
2✔
626
            {"object",
2✔
627
             {{"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
628
              {"property1", PropertyType::Int},
2✔
629
              {"property2", PropertyType::Int}}},
2✔
630
        };
2✔
631

1✔
632
        auto realm1 = Realm::get_shared_realm(config1);
2✔
633
        wait_for_upload(*realm1);
2✔
634
    }
2✔
635

1✔
636
    // Download the existing Realm into a second local file without specifying a schema,
1✔
637
    // mirroring how `openAsync` works.
1✔
638
    SyncTestFile config2(init_sync_manager.app(), "schema-version-test");
2✔
639
    config2.schema_version = 1;
2✔
640
    {
2✔
641
        auto realm2 = Realm::get_shared_realm(config2);
2✔
642
        wait_for_download(*realm2);
2✔
643
    }
2✔
644

1✔
645
    // Open the just-downloaded Realm while specifying a schema that contains a class with
1✔
646
    // only a single property. This should not result in us trying to remove `property2`,
1✔
647
    // and will throw an exception if it does.
1✔
648
    {
2✔
649
        SyncTestFile config3(init_sync_manager.app(), "schema-version-test");
2✔
650
        config3.path = config2.path;
2✔
651
        config3.schema_version = 1;
2✔
652
        config3.schema = Schema{
2✔
653
            {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"property1", PropertyType::Int}}},
2✔
654
        };
2✔
655

1✔
656
        auto realm3 = Realm::get_shared_realm(config3);
2✔
657
    }
2✔
658
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc