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

realm / realm-core / 1691

20 Sep 2023 01:57AM UTC coverage: 91.217% (+0.05%) from 91.168%
1691

push

Evergreen

web-flow
Merge pull request #6837 from realm/tg/user-provider

Fix handling of users with multiple identities

95990 of 175908 branches covered (0.0%)

799 of 830 new or added lines in 24 files covered. (96.27%)

44 existing lines in 15 files now uncovered.

233732 of 256237 relevant lines covered (91.22%)

6741306.52 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_device_id = "123400000000000000000000";
45

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

463
TEST_CASE("sync: stop policy behavior", "[sync][session]") {
12✔
464
    if (!EventLoop::has_implementation())
12✔
UNCOV
465
        return;
×
466

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

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

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

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

6✔
501
        return session;
12✔
502
    };
12✔
503

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
595
    server.start();
2✔
596

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

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

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

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

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

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

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

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

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

1✔
654
        auto realm3 = Realm::get_shared_realm(config3);
2✔
655
    }
2✔
656
}
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

© 2025 Coveralls, Inc