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

realm / realm-core / 2213

10 Apr 2024 11:21PM UTC coverage: 91.792% (-0.8%) from 92.623%
2213

push

Evergreen

web-flow
Add missing availability checks for SecCopyErrorMessageString (#7577)

This requires iOS 11.3 and we currently target iOS 11.

94842 of 175770 branches covered (53.96%)

7 of 22 new or added lines in 2 files covered. (31.82%)

1861 existing lines in 82 files now uncovered.

242866 of 264583 relevant lines covered (91.79%)

5593111.45 hits per line

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

98.55
/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
TEST_CASE("SyncSession: management by SyncUser", "[sync][session]") {
12✔
45
    if (!EventLoop::has_implementation())
12✔
46
        return;
×
47

6✔
48
    using State = SyncUser::State;
12✔
49

6✔
50
    TestSyncManager tsm;
12✔
51
    auto& server = tsm.sync_server();
12✔
52
    const std::string realm_base_url = server.base_url();
12✔
53

6✔
54
    auto check_for_sessions = [](TestUser& user, size_t count, SyncSession::State state) {
14✔
55
        auto sessions = user.sync_manager()->get_all_sessions_for(user);
14✔
56
        CHECK(sessions.size() == count);
14!
57
        for (auto& session : sessions) {
28✔
58
            CHECK(session->state() == state);
28!
59
        }
28✔
60
    };
14✔
61

6✔
62
    SECTION("a SyncUser can properly retrieve its owned sessions") {
12✔
63
        auto user = tsm.fake_user();
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
        check_for_sessions(*user, 2, SyncSession::State::Active);
2✔
72
        auto s1 = tsm.sync_manager()->get_existing_session(session1->path());
2✔
73
        REQUIRE(s1 == session1);
2!
74
        auto s2 = tsm.sync_manager()->get_existing_session(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 = tsm.fake_user();
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
        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_for_sessions(*user, 2, SyncSession::State::Inactive);
2✔
92
    }
2✔
93

6✔
94
    SECTION("a SyncUser defers binding new sessions until it is logged in") {
12✔
95
        auto user = tsm.fake_user();
2✔
96
        user->log_out();
2✔
97
        REQUIRE(user->state() == 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
        check_for_sessions(*user, 2, SyncSession::State::Inactive);
2✔
105
        // Log the user back in via the sync manager.
1✔
106
        user->log_in();
2✔
107
        EventLoop::main().run_until([&] {
3✔
108
            return sessions_are_active(*session1, *session2);
3✔
109
        });
3✔
110
        check_for_sessions(*user, 2, SyncSession::State::Active);
2✔
111
    }
2✔
112

6✔
113
    SECTION("a SyncUser properly rebinds existing sessions upon logging back in") {
12✔
114
        auto user = tsm.fake_user();
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
        check_for_sessions(*user, 2, SyncSession::State::Active);
2✔
122
        // Log the user out.
1✔
123
        user->log_out();
2✔
124
        REQUIRE(user->state() == 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
        check_for_sessions(*user, 2, SyncSession::State::Inactive);
2✔
130
        // Log the user back in via the sync manager.
1✔
131
        user->log_in();
2✔
132
        EventLoop::main().run_until([&] {
3✔
133
            return sessions_are_active(*session1, *session2);
3✔
134
        });
3✔
135
        check_for_sessions(*user, 2, SyncSession::State::Active);
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 = tsm.fake_user();
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 = tsm.sync_manager()->get_existing_session(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 = tsm.fake_user();
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✔
UNCOV
180
        return;
×
181

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

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

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

1✔
202
    // TODO: write tests that check that a Session properly handles various types of errors reported via its callback.
1✔
203
}
2✔
204

205
TEST_CASE("SyncSession: close() API", "[sync][session]") {
4✔
206
    TestSyncManager tsm;
4✔
207
    auto user = tsm.fake_user();
4✔
208

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

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

232
TEST_CASE("SyncSession: pause()/resume() API", "[sync][session]") {
4✔
233
    TestSyncManager tsm;
4✔
234
    auto user = tsm.fake_user();
4✔
235

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

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

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

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

1✔
263
        // Pausing it again should be a no-op
1✔
264
        session->pause();
2✔
265
        REQUIRE(session->state() == SyncSession::State::Paused);
2!
266

1✔
267
        // "Logging out" the session should be a no-op.
1✔
268
        session->force_close();
2✔
269
        REQUIRE(session->state() == SyncSession::State::Paused);
2!
270
    }
2✔
271

2✔
272
    // Reviving the session via revive_if_needed() should be a no-op.
2✔
273
    session->revive_if_needed();
4✔
274
    REQUIRE(session->state() == SyncSession::State::Paused);
4!
275

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

284
TEST_CASE("SyncSession: shutdown_and_wait() API", "[sync][session]") {
2✔
285
    TestSyncManager tsm;
2✔
286
    auto user = tsm.fake_user();
2✔
287

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

306
TEST_CASE("SyncSession: internal pause_async API", "[sync][session]") {
2✔
307
    TestSyncManager tsm;
2✔
308
    auto user = tsm.fake_user("close-api-tests-user");
2✔
309

1✔
310
    auto session = sync_session(
2✔
311
        user, "/test-close-for-active", [](auto, auto) {}, SyncSessionStopPolicy::AfterChangesUploaded);
1✔
312
    EventLoop::main().run_until([&] {
3✔
313
        return sessions_are_active(*session);
3✔
314
    });
3✔
315
    REQUIRE(sessions_are_active(*session));
2!
316
    auto dbref = SyncSession::OnlyForTesting::get_db(*session);
2✔
317
    auto before = dbref.use_count();
2✔
318
    auto future = SyncSession::OnlyForTesting::pause_async(*session);
2✔
319
    future.get();
2✔
320
    auto after = dbref.use_count();
2✔
321
    // Check SessionImpl released the sync agent as result of SessionWrapper::finalize() being called.
1✔
322
    REQUIRE_NOTHROW(dbref->claim_sync_agent());
2✔
323
    // Check DBRef is released in SessionWrapper::finalize().
1✔
324
    REQUIRE(after < before);
2!
325
}
2✔
326

327
TEST_CASE("SyncSession: update_configuration()", "[sync][session]") {
4✔
328
    TestSyncManager tsm({}, {false});
4✔
329
    auto user = tsm.fake_user();
4✔
330
    auto session = sync_session(user, "/update_configuration");
4✔
331

2✔
332
    SECTION("updates reported configuration") {
4✔
333
        auto config = session->config();
2✔
334
        REQUIRE(config.client_validate_ssl);
2!
335
        config.client_validate_ssl = false;
2✔
336
        session->update_configuration(std::move(config));
2✔
337
        REQUIRE_FALSE(session->config().client_validate_ssl);
2!
338
    }
2✔
339

2✔
340
    SECTION("handles reconnects while it's trying to deactivate session") {
4✔
341
        bool wait_called = false;
2✔
342
        session->wait_for_download_completion([&](Status s) {
2✔
343
            REQUIRE(s == ErrorCodes::OperationAborted);
2!
344
            REQUIRE(session->config().client_validate_ssl);
2!
345
            REQUIRE(session->state() == SyncSession::State::Inactive);
2!
346

1✔
347
            wait_called = true;
2✔
348
            session->revive_if_needed();
2✔
349

1✔
350
            REQUIRE(session->state() != SyncSession::State::Inactive);
2!
351
        });
2✔
352

1✔
353
        auto config = session->config();
2✔
354
        config.client_validate_ssl = false;
2✔
355
        session->update_configuration(std::move(config));
2✔
356
        REQUIRE(wait_called);
2!
357
    }
2✔
358
}
4✔
359

360
TEST_CASE("sync: error handling", "[sync][session]") {
12✔
361
    TestSyncManager tsm;
12✔
362

6✔
363
    std::string on_disk_path;
12✔
364
    std::optional<SyncError> error;
12✔
365
    std::mutex mutex;
12✔
366
    auto store_sync_error = [&](auto, SyncError e) {
11✔
367
        std::lock_guard lock(mutex);
10✔
368
        error = e;
10✔
369
    };
10✔
370

6✔
371
    SECTION("reports DNS error") {
12✔
372
        tsm.sync_manager()->set_sync_route("ws://invalid.com:9090", true);
2✔
373

1✔
374
        auto user = tsm.fake_user();
2✔
375
        auto session = sync_session(user, "/test", store_sync_error);
2✔
376
        timed_wait_for(
2✔
377
            [&] {
290,493✔
378
                std::lock_guard lock(mutex);
290,493✔
379
                return error.has_value();
290,493✔
380
            },
290,493✔
381
            std::chrono::seconds(35)); // this sometimes needs to wait for a 30s dns timeout
2✔
382
        REQUIRE(error);
2!
383
        CHECK(error->status.code() == ErrorCodes::SyncConnectFailed);
2!
384
        // May end with either (authoritative) or (non-authoritative)
1✔
385
        CHECK_THAT(error->status.reason(), Catch::Matchers::StartsWith("Failed to connect to sync: Host not found"));
2✔
386
    }
2✔
387

6✔
388
    // requires test resource files and a server implementation
6✔
389
#if !(defined(SWIFT_PACKAGE) || REALM_MOBILE)
12✔
390
    SECTION("reports TLS error as handshake failed") {
12✔
391
        TestSyncManager ssl_sync_manager({}, {StartImmediately{true}, EnableSSL{true}});
2✔
392
        auto user = ssl_sync_manager.fake_user();
2✔
393
        auto session = sync_session(user, "/test", store_sync_error);
2✔
394
        timed_wait_for(
2✔
395
            [&] {
26,131✔
396
                std::lock_guard lock(mutex);
26,131✔
397
                return error.has_value();
26,131✔
398
            },
26,131✔
399
            std::chrono::seconds(35));
2✔
400
        REQUIRE(error);
2!
401
        CHECK(error->status.code() == ErrorCodes::TlsHandshakeFailed);
2!
402
#if REALM_HAVE_SECURE_TRANSPORT
1✔
403
        CHECK(error->status.reason() ==
1!
404
              "TLS handshake failed: SecureTransport error: invalid certificate chain (-9807)");
1✔
405
#else
406
        // The exact error code seems to vary, so only check the message
1✔
407
        CHECK_THAT(error->status.reason(),
1✔
408
                   Catch::Matchers::StartsWith("TLS handshake failed: OpenSSL error: certificate verify failed"));
1✔
409
#endif
1✔
410
    }
2✔
411
#endif // !defined(SWIFT_PACKAGE) && !REALM_MOBILE
12✔
412

6✔
413
    using ProtocolError = realm::sync::ProtocolError;
12✔
414
    using ProtocolErrorInfo = realm::sync::ProtocolErrorInfo;
12✔
415

6✔
416
    SECTION("Doesn't treat unknown system errors as being fatal") {
12✔
417
        auto user = tsm.fake_user();
2✔
418
        auto session = sync_session(user, "/test", store_sync_error);
2✔
419
        EventLoop::main().run_until([&] {
3✔
420
            return sessions_are_active(*session);
3✔
421
        });
3✔
422

1✔
423
        sync::SessionErrorInfo err{Status{ErrorCodes::UnknownError, "unknown error"}, true};
2✔
424
        err.server_requests_action = ProtocolErrorInfo::Action::Transient;
2✔
425
        SyncSession::OnlyForTesting::handle_error(*session, std::move(err));
2✔
426
        CHECK(!sessions_are_inactive(*session));
2!
427
        // Error is non-fatal so it's not reported to the error handler
1✔
428
        std::lock_guard lock(mutex);
2✔
429
        REQUIRE_FALSE(error);
2!
430
    }
2✔
431

6✔
432
    SECTION("Properly handles a client reset error") {
12✔
433
        OfflineAppSession oas;
6✔
434
        auto user = oas.make_user();
6✔
435

3✔
436
        auto session = sync_session(user, "/test", store_sync_error);
6✔
437
        std::string on_disk_path = session->path();
6✔
438
        EventLoop::main().run_until([&] {
9✔
439
            return sessions_are_active(*session);
9✔
440
        });
9✔
441

3✔
442
        auto code = GENERATE(ProtocolError::bad_client_file_ident, ProtocolError::bad_server_version,
6✔
443
                             ProtocolError::diverging_histories);
6✔
444

3✔
445
        sync::SessionErrorInfo initial_error{sync::protocol_error_to_status(code, "Something bad happened"), true};
6✔
446
        initial_error.server_requests_action = ProtocolErrorInfo::Action::ClientReset;
6✔
447
        std::time_t just_before_raw = std::time(nullptr);
6✔
448
        SyncSession::OnlyForTesting::handle_error(*session, std::move(initial_error));
6✔
449
        REQUIRE(session->state() == SyncSession::State::Inactive);
6!
450
        std::time_t just_after_raw = std::time(nullptr);
6✔
451
        auto just_before = util::localtime(just_before_raw);
6✔
452
        auto just_after = util::localtime(just_after_raw);
6✔
453
        // At this point error should be populated.
3✔
454
        REQUIRE(error);
6!
455
        CHECK(error->is_client_reset_requested());
6!
456
        CHECK(error->server_requests_action == ProtocolErrorInfo::Action::ClientReset);
6!
457
        // The original file path should be present.
3✔
458
        CHECK(error->user_info[SyncError::c_original_file_path_key] == on_disk_path);
6!
459
        // The path to the recovery file should be present, and should contain all necessary components.
3✔
460
        std::string recovery_path = error->user_info[SyncError::c_recovery_file_path_key];
6✔
461
        auto idx = recovery_path.find("recovered_realm");
6✔
462
        CHECK(idx != std::string::npos);
6!
463
        idx = recovery_path.find(oas.app()->config().base_file_path);
6✔
464
        CHECK(idx != std::string::npos);
6!
465
        idx = recovery_path.find(oas.app()->app_id());
6✔
466
        CHECK(idx != std::string::npos);
6!
467
        if (just_before.tm_year == just_after.tm_year) {
6✔
468
            idx = recovery_path.find(util::format_local_time(just_after_raw, "%Y"));
6✔
469
            CHECK(idx != std::string::npos);
6!
470
        }
6✔
471
        if (just_before.tm_mon == just_after.tm_mon) {
6✔
472
            idx = recovery_path.find(util::format_local_time(just_after_raw, "%m"));
6✔
473
            CHECK(idx != std::string::npos);
6!
474
        }
6✔
475
        if (just_before.tm_yday == just_after.tm_yday) {
6✔
476
            idx = recovery_path.find(util::format_local_time(just_after_raw, "%d"));
6✔
477
            CHECK(idx != std::string::npos);
6!
478
        }
6✔
479
    }
6✔
480
}
12✔
481

482
TEST_CASE("sync: stop policy behavior", "[sync][session]") {
12✔
483
    if (!EventLoop::has_implementation())
12✔
UNCOV
484
        return;
×
485

6✔
486
    // Server is initially stopped so we can control when the session exits the dying state.
6✔
487
    TestSyncManager tsm({}, {false});
12✔
488
    auto& server = tsm.sync_server();
12✔
489
    auto sync_manager = tsm.sync_manager();
12✔
490
    auto schema = Schema{
12✔
491
        {"object",
12✔
492
         {
12✔
493
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
12✔
494
             {"value", PropertyType::Int},
12✔
495
         }},
12✔
496
    };
12✔
497

6✔
498
    std::atomic<bool> error_handler_invoked(false);
12✔
499
    Realm::Config config;
12✔
500
    auto user = tsm.fake_user();
12✔
501

6✔
502
    auto create_session = [&](SyncSessionStopPolicy stop_policy) {
12✔
503
        auto session = sync_session(
12✔
504
            user, "/test-dying-state",
12✔
505
            [&](auto, auto) {
6✔
UNCOV
506
                error_handler_invoked = true;
×
UNCOV
507
            },
×
508
            stop_policy, nullptr, schema, &config);
12✔
509
        EventLoop::main().run_until([&] {
18✔
510
            return sessions_are_active(*session);
18✔
511
        });
18✔
512

6✔
513
        // Add an object so there's something to upload
6✔
514
        auto r = Realm::get_shared_realm(config);
12✔
515
        TableRef table = ObjectStore::table_for_object_type(r->read_group(), "object");
12✔
516
        r->begin_transaction();
12✔
517
        table->create_object_with_primary_key(0);
12✔
518
        r->commit_transaction();
12✔
519

6✔
520
        return session;
12✔
521
    };
12✔
522

6✔
523
    SECTION("Immediately") {
12✔
524
        SECTION("transitions directly to Inactive even with the server stopped") {
2✔
525
            auto session = create_session(SyncSessionStopPolicy::Immediately);
2✔
526
            session->close();
2✔
527
            REQUIRE(sessions_are_inactive(*session));
2!
528
        }
2✔
529
    }
2✔
530

6✔
531
    SECTION("AfterChangesUploaded") {
12✔
532
        auto session = create_session(SyncSessionStopPolicy::AfterChangesUploaded);
8✔
533
        // Now close the session, causing the state to transition to Dying.
4✔
534
        // (it should remain stuck there until we start the server)
4✔
535
        session->close();
8✔
536
        REQUIRE(session->state() == SyncSession::State::Dying);
8!
537

4✔
538
        SECTION("transitions to Inactive once the server is started") {
8✔
539
            server.start();
2✔
540
            EventLoop::main().run_until([&] {
15,706✔
541
                return sessions_are_inactive(*session);
15,706✔
542
            });
15,706✔
543
        }
2✔
544

4✔
545
        SECTION("transitions back to Active if the session is revived") {
8✔
546
            std::shared_ptr<SyncSession> session2;
2✔
547
            {
2✔
548
                auto realm = Realm::get_shared_realm(config);
2✔
549
                session2 = user->sync_manager()->get_existing_session(config.path);
2✔
550
            }
2✔
551
            REQUIRE(session->state() == SyncSession::State::Active);
2!
552
            REQUIRE(session2 == session);
2!
553
        }
2✔
554

4✔
555
        SECTION("transitions to Inactive if a fatal error occurs") {
8✔
556
            sync::SessionErrorInfo err{Status{ErrorCodes::SyncProtocolInvariantFailed, "Not a real error message"},
2✔
557
                                       sync::IsFatal{true}};
2✔
558
            err.server_requests_action = realm::sync::ProtocolErrorInfo::Action::ProtocolViolation;
2✔
559
            SyncSession::OnlyForTesting::handle_error(*session, std::move(err));
2✔
560
            CHECK(sessions_are_inactive(*session));
2!
561
            // The session shouldn't report fatal errors when in the dying state.
1✔
562
            CHECK(!error_handler_invoked);
2!
563
        }
2✔
564

4✔
565
        SECTION("ignores non-fatal errors and does not transition to Inactive") {
8✔
566
            // Fire a simulated *non-fatal* error.
1✔
567
            sync::SessionErrorInfo err{Status{ErrorCodes::ConnectionClosed, "Not a real error message"},
2✔
568
                                       sync::IsFatal{false}};
2✔
569
            err.server_requests_action = realm::sync::ProtocolErrorInfo::Action::Transient;
2✔
570
            SyncSession::OnlyForTesting::handle_error(*session, std::move(err));
2✔
571
            REQUIRE(session->state() == SyncSession::State::Dying);
2!
572
            CHECK(!error_handler_invoked);
2!
573
        }
2✔
574
    }
8✔
575

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

1✔
580
        auto config = session->config();
2✔
581
        config.stop_policy = SyncSessionStopPolicy::Immediately;
2✔
582
        session->update_configuration(std::move(config));
2✔
583

1✔
584
        session->close();
2✔
585
        REQUIRE(sessions_are_inactive(*session));
2!
586
    }
2✔
587
}
12✔
588

589
TEST_CASE("session restart", "[sync][session]") {
2✔
590
    if (!EventLoop::has_implementation())
2✔
UNCOV
591
        return;
×
592

1✔
593
    TestSyncManager tsm({}, {false});
2✔
594
    auto& server = tsm.sync_server();
2✔
595
    Realm::Config config;
2✔
596
    auto schema = Schema{
2✔
597
        {"object",
2✔
598
         {
2✔
599
             {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
600
             {"value", PropertyType::Int},
2✔
601
         }},
2✔
602
    };
2✔
603

1✔
604
    auto user = tsm.fake_user();
2✔
605
    auto session = sync_session(
2✔
606
        user, "/test-restart", [&](auto, auto) {}, SyncSessionStopPolicy::AfterChangesUploaded, nullptr, schema,
1✔
607
        &config);
2✔
608

1✔
609
    EventLoop::main().run_until([&] {
3✔
610
        return sessions_are_active(*session);
3✔
611
    });
3✔
612

1✔
613
    server.start();
2✔
614

1✔
615
    // Add an object so there's something to upload
1✔
616
    auto realm = Realm::get_shared_realm(config);
2✔
617
    TableRef table = ObjectStore::table_for_object_type(realm->read_group(), "object");
2✔
618
    realm->begin_transaction();
2✔
619
    table->create_object_with_primary_key(0);
2✔
620
    realm->commit_transaction();
2✔
621

1✔
622
    // Close the current session and start a new one
1✔
623
    // The stop policy is ignored when closing the current session
1✔
624
    session->restart_session();
2✔
625

1✔
626
    REQUIRE(session->state() == SyncSession::State::Active);
2!
627
    REQUIRE(!wait_for_upload(*realm));
2!
628
}
2✔
629

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

1✔
634
    // Disable file-related functionality and metadata functionality for testing purposes.
1✔
635
    TestSyncManager tsm;
2✔
636
    auto user = tsm.fake_user();
2✔
637
    ;
2✔
638
    // Create a synced Realm containing a class with two properties.
1✔
639
    {
2✔
640
        SyncTestFile config1(user, "schema-version-test");
2✔
641
        config1.schema_version = 1;
2✔
642
        config1.schema = Schema{
2✔
643
            {"object",
2✔
644
             {{"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
645
              {"property1", PropertyType::Int},
2✔
646
              {"property2", PropertyType::Int}}},
2✔
647
        };
2✔
648

1✔
649
        auto realm1 = Realm::get_shared_realm(config1);
2✔
650
        wait_for_upload(*realm1);
2✔
651
    }
2✔
652

1✔
653
    // Download the existing Realm into a second local file without specifying a schema,
1✔
654
    // mirroring how `openAsync` works.
1✔
655
    SyncTestFile config2(user, "schema-version-test");
2✔
656
    config2.schema_version = 1;
2✔
657
    {
2✔
658
        auto realm2 = Realm::get_shared_realm(config2);
2✔
659
        wait_for_download(*realm2);
2✔
660
    }
2✔
661

1✔
662
    // Open the just-downloaded Realm while specifying a schema that contains a class with
1✔
663
    // only a single property. This should not result in us trying to remove `property2`,
1✔
664
    // and will throw an exception if it does.
1✔
665
    {
2✔
666
        SyncTestFile config3(user, "schema-version-test");
2✔
667
        config3.path = config2.path;
2✔
668
        config3.schema_version = 1;
2✔
669
        config3.schema = Schema{
2✔
670
            {"object", {{"_id", PropertyType::Int, Property::IsPrimary{true}}, {"property1", PropertyType::Int}}},
2✔
671
        };
2✔
672

1✔
673
        auto realm3 = Realm::get_shared_realm(config3);
2✔
674
    }
2✔
675
}
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