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

realm / realm-core / 2476

05 Jul 2024 01:18PM UTC coverage: 90.974% (-0.02%) from 90.995%
2476

push

Evergreen

web-flow
New changelog section to prepare for vNext (#7866)

102302 of 180462 branches covered (56.69%)

215189 of 236538 relevant lines covered (90.97%)

5815746.14 hits per line

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

98.28
/test/object-store/sync/flx_sync.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2021 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
#if REALM_ENABLE_AUTH_TESTS
20

21
#include <catch2/catch_all.hpp>
22

23
#include <util/test_file.hpp>
24
#include <util/crypt_key.hpp>
25
#include <util/sync/flx_sync_harness.hpp>
26
#include <util/sync/sync_test_utils.hpp>
27

28
#include <realm/object_id.hpp>
29
#include <realm/query_expression.hpp>
30

31
#include <realm/object-store/binding_context.hpp>
32
#include <realm/object-store/impl/object_accessor_impl.hpp>
33
#include <realm/object-store/impl/realm_coordinator.hpp>
34
#include <realm/object-store/schema.hpp>
35
#include <realm/object-store/sync/generic_network_transport.hpp>
36
#include <realm/object-store/sync/mongo_client.hpp>
37
#include <realm/object-store/sync/mongo_database.hpp>
38
#include <realm/object-store/sync/mongo_collection.hpp>
39
#include <realm/object-store/sync/async_open_task.hpp>
40
#include <realm/util/bson/bson.hpp>
41
#include <realm/object-store/sync/sync_session.hpp>
42

43
#include <realm/sync/client_base.hpp>
44
#include <realm/sync/config.hpp>
45
#include <realm/sync/noinst/client_history_impl.hpp>
46
#include <realm/sync/noinst/client_reset.hpp>
47
#include <realm/sync/noinst/client_reset_operation.hpp>
48
#include <realm/sync/noinst/pending_bootstrap_store.hpp>
49
#include <realm/sync/noinst/server/access_token.hpp>
50
#include <realm/sync/protocol.hpp>
51
#include <realm/sync/subscriptions.hpp>
52

53
#include <realm/util/future.hpp>
54
#include <realm/util/logger.hpp>
55

56
#include <catch2/catch_all.hpp>
57

58
#include <filesystem>
59
#include <iostream>
60
#include <stdexcept>
61

62
using namespace std::string_literals;
63

64
namespace realm {
65

66
class TestHelper {
67
public:
68
    static DBRef& get_db(SharedRealm const& shared_realm)
69
    {
70
        return Realm::Internal::get_db(*shared_realm);
71
    }
72
};
73

74
} // namespace realm
75

76
namespace realm::app {
77

78
namespace {
79
const Schema g_minimal_schema{
80
    {"TopLevel",
81
     {
82
         {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
83
     }},
84
};
85

86
const Schema g_large_array_schema{
87
    ObjectSchema("TopLevel",
88
                 {
89
                     {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
90
                     {"queryable_int_field", PropertyType::Int | PropertyType::Nullable},
91
                     {"list_of_strings", PropertyType::Array | PropertyType::String},
92
                 }),
93
};
94

95
const Schema g_simple_embedded_obj_schema{
96
    {"TopLevel",
97
     {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
98
      {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
99
      {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "TopLevel_embedded_obj"}}},
100
    {"TopLevel_embedded_obj",
101
     ObjectSchema::ObjectType::Embedded,
102
     {
103
         {"str_field", PropertyType::String | PropertyType::Nullable},
104
     }},
105
};
106

107
// Populates a FLXSyncTestHarness with the g_large_array_schema with objects that are large enough that
108
// they are guaranteed to fill multiple bootstrap download messages. Currently this means generating 5
109
// objects each with 1024 array entries of 1024 bytes each.
110
//
111
// Returns a list of the _id values for the objects created.
112
std::vector<ObjectId> fill_large_array_schema(FLXSyncTestHarness& harness)
113
{
14✔
114
    std::vector<ObjectId> ret;
14✔
115
    REQUIRE(harness.schema() == g_large_array_schema);
14!
116
    harness.load_initial_data([&](SharedRealm realm) {
14✔
117
        CppContext c(realm);
14✔
118
        for (int i = 0; i < 5; ++i) {
84✔
119
            auto id = ObjectId::gen();
70✔
120
            auto obj = Object::create(c, realm, "TopLevel",
70✔
121
                                      std::any(AnyDict{{"_id", id},
70✔
122
                                                       {"list_of_strings", AnyVector{}},
70✔
123
                                                       {"queryable_int_field", static_cast<int64_t>(i * 5)}}));
70✔
124
            List str_list(obj, realm->schema().find("TopLevel")->property_for_name("list_of_strings"));
70✔
125
            for (int j = 0; j < 1024; ++j) {
71,750✔
126
                str_list.add(c, std::any(std::string(1024, 'a' + (j % 26))));
71,680✔
127
            }
71,680✔
128

129
            ret.push_back(id);
70✔
130
        }
70✔
131
    });
14✔
132
    return ret;
14✔
133
}
14✔
134

135
} // namespace
136

137
TEST_CASE("flx: connect to FLX-enabled app", "[sync][flx][baas]") {
2✔
138
    FLXSyncTestHarness harness("basic_flx_connect");
2✔
139

140
    auto foo_obj_id = ObjectId::gen();
2✔
141
    auto bar_obj_id = ObjectId::gen();
2✔
142
    harness.load_initial_data([&](SharedRealm realm) {
2✔
143
        CppContext c(realm);
2✔
144
        Object::create(c, realm, "TopLevel",
2✔
145
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
146
                                        {"queryable_str_field", "foo"s},
2✔
147
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
148
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
149
        Object::create(c, realm, "TopLevel",
2✔
150
                       std::any(AnyDict{{"_id", bar_obj_id},
2✔
151
                                        {"queryable_str_field", "bar"s},
2✔
152
                                        {"queryable_int_field", static_cast<int64_t>(10)},
2✔
153
                                        {"non_queryable_field", "non queryable 2"s}}));
2✔
154
    });
2✔
155

156

157
    harness.do_with_new_realm([&](SharedRealm realm) {
2✔
158
        {
2✔
159
            auto empty_subs = realm->get_latest_subscription_set();
2✔
160
            CHECK(empty_subs.size() == 0);
2!
161
            CHECK(empty_subs.version() == 0);
2!
162
            empty_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
163
        }
2✔
164

165
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
166
        auto col_key = table->get_column_key("queryable_str_field");
2✔
167
        Query query_foo(table);
2✔
168
        query_foo.equal(col_key, "foo");
2✔
169
        {
2✔
170
            auto new_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
171
            new_subs.insert_or_assign(query_foo);
2✔
172
            auto subs = new_subs.commit();
2✔
173
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
174
        }
2✔
175

176
        {
2✔
177
            wait_for_advance(*realm);
2✔
178
            Results results(realm, table);
2✔
179
            CHECK(results.size() == 1);
2!
180
            auto obj = results.get<Obj>(0);
2✔
181
            CHECK(obj.is_valid());
2!
182
            CHECK(obj.get<ObjectId>("_id") == foo_obj_id);
2!
183
        }
2✔
184

185
        {
2✔
186
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
187
            Query new_query_bar(table);
2✔
188
            new_query_bar.equal(col_key, "bar");
2✔
189
            mut_subs.insert_or_assign(new_query_bar);
2✔
190
            auto subs = mut_subs.commit();
2✔
191
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
192
        }
2✔
193

194
        {
2✔
195
            wait_for_advance(*realm);
2✔
196
            Results results(realm, Query(table));
2✔
197
            CHECK(results.size() == 2);
2!
198
        }
2✔
199

200
        {
2✔
201
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
202
            CHECK(mut_subs.erase(query_foo));
2!
203
            Query new_query_bar(table);
2✔
204
            new_query_bar.equal(col_key, "bar");
2✔
205
            mut_subs.insert_or_assign(new_query_bar);
2✔
206
            auto subs = mut_subs.commit();
2✔
207
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
208
        }
2✔
209

210
        {
2✔
211
            wait_for_advance(*realm);
2✔
212
            Results results(realm, Query(table));
2✔
213
            CHECK(results.size() == 1);
2!
214
            auto obj = results.get<Obj>(0);
2✔
215
            CHECK(obj.is_valid());
2!
216
            CHECK(obj.get<ObjectId>("_id") == bar_obj_id);
2!
217
        }
2✔
218

219
        {
2✔
220
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
221
            mut_subs.clear();
2✔
222
            auto subs = mut_subs.commit();
2✔
223
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
224
        }
2✔
225

226
        {
2✔
227
            wait_for_advance(*realm);
2✔
228
            Results results(realm, table);
2✔
229
            CHECK(results.size() == 0);
2!
230
        }
2✔
231
    });
2✔
232
}
2✔
233

234
TEST_CASE("flx: test commands work", "[sync][flx][test command][baas]") {
2✔
235
    FLXSyncTestHarness harness("test_commands");
2✔
236
    harness.do_with_new_realm([&](const SharedRealm& realm) {
2✔
237
        wait_for_upload(*realm);
2✔
238
        nlohmann::json command_request = {
2✔
239
            {"command", "PAUSE_ROUTER_SESSION"},
2✔
240
        };
2✔
241
        auto resp_body =
2✔
242
            SyncSession::OnlyForTesting::send_test_command(*realm->sync_session(), command_request.dump()).get();
2✔
243
        REQUIRE(resp_body == "{}");
2!
244

245
        auto bad_status =
2✔
246
            SyncSession::OnlyForTesting::send_test_command(*realm->sync_session(), "foobar: }").get_no_throw();
2✔
247
        REQUIRE(bad_status.get_status() == ErrorCodes::LogicError);
2!
248
        REQUIRE_THAT(bad_status.get_status().reason(),
2✔
249
                     Catch::Matchers::ContainsSubstring("Invalid json input to send_test_command"));
2✔
250

251
        bad_status =
2✔
252
            SyncSession::OnlyForTesting::send_test_command(*realm->sync_session(), "{\"cmd\": \"\"}").get_no_throw();
2✔
253
        REQUIRE_FALSE(bad_status.is_ok());
2!
254
        REQUIRE(bad_status.get_status() == ErrorCodes::LogicError);
2!
255
        REQUIRE(bad_status.get_status().reason() ==
2!
256
                "Must supply command name in \"command\" field of test command json object");
2✔
257
    });
2✔
258
}
2✔
259

260

261
static auto make_error_handler()
262
{
40✔
263
    auto [error_promise, error_future] = util::make_promise_future<SyncError>();
40✔
264
    auto shared_promise = std::make_shared<decltype(error_promise)>(std::move(error_promise));
40✔
265
    auto fn = [error_promise = std::move(shared_promise)](std::shared_ptr<SyncSession>, SyncError err) {
40✔
266
        error_promise->emplace_value(std::move(err));
36✔
267
    };
36✔
268
    return std::make_pair(std::move(error_future), std::move(fn));
40✔
269
}
40✔
270

271
static auto make_client_reset_handler()
272
{
20✔
273
    auto [reset_promise, reset_future] = util::make_promise_future<ClientResyncMode>();
20✔
274
    auto shared_promise = std::make_shared<decltype(reset_promise)>(std::move(reset_promise));
20✔
275
    auto fn = [reset_promise = std::move(shared_promise)](SharedRealm, ThreadSafeReference, bool did_recover) {
20✔
276
        reset_promise->emplace_value(did_recover ? ClientResyncMode::Recover : ClientResyncMode::DiscardLocal);
20✔
277
    };
20✔
278
    return std::make_pair(std::move(reset_future), std::move(fn));
20✔
279
}
20✔
280

281

282
TEST_CASE("app: error handling integration test", "[sync][flx][baas]") {
18✔
283
    static std::optional<FLXSyncTestHarness> harness{"error_handling"};
18✔
284
    create_user_and_log_in(harness->app());
18✔
285
    SyncTestFile config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{});
18✔
286
    auto&& [error_future, error_handler] = make_error_handler();
18✔
287
    config.sync_config->error_handler = std::move(error_handler);
18✔
288
    config.sync_config->client_resync_mode = ClientResyncMode::Manual;
18✔
289

290
    SECTION("Resuming while waiting for session to auto-resume") {
18✔
291
        enum class TestState { InitialSuspend, InitialResume, SecondBind, SecondSuspend, SecondResume, Done };
2✔
292
        TestingStateMachine<TestState> state(TestState::InitialSuspend);
2✔
293
        config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession>,
2✔
294
                                                            const SyncClientHookData& data) {
39✔
295
            std::optional<TestState> wait_for;
39✔
296
            auto event = data.event;
39✔
297
            state.transition_with([&](TestState state) -> std::optional<TestState> {
39✔
298
                if (state == TestState::InitialSuspend && event == SyncClientHookEvent::SessionSuspended) {
39✔
299
                    // If we're getting suspended for the first time, notify the test thread that we're
300
                    // ready to be resumed.
301
                    wait_for = TestState::SecondBind;
2✔
302
                    return TestState::InitialResume;
2✔
303
                }
2✔
304
                else if (state == TestState::SecondBind && data.event == SyncClientHookEvent::BindMessageSent) {
37✔
305
                    return TestState::SecondSuspend;
2✔
306
                }
2✔
307
                else if (state == TestState::SecondSuspend && event == SyncClientHookEvent::SessionSuspended) {
35✔
308
                    wait_for = TestState::Done;
2✔
309
                    return TestState::SecondResume;
2✔
310
                }
2✔
311
                return std::nullopt;
33✔
312
            });
39✔
313
            if (wait_for) {
39✔
314
                state.wait_for(*wait_for);
4✔
315
            }
4✔
316
            return SyncClientHookAction::NoAction;
39✔
317
        };
39✔
318
        auto r = Realm::get_shared_realm(config);
2✔
319
        wait_for_upload(*r);
2✔
320
        nlohmann::json error_body = {
2✔
321
            {"tryAgain", true},           {"message", "fake error"},
2✔
322
            {"shouldClientReset", false}, {"isRecoveryModeDisabled", false},
2✔
323
            {"action", "Transient"},      {"backoffIntervalSec", 900},
2✔
324
            {"backoffMaxDelaySec", 900},  {"backoffMultiplier", 1},
2✔
325
        };
2✔
326
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
327
                                       {"args", nlohmann::json{{"errorCode", 229}, {"errorBody", error_body}}}};
2✔
328

329
        // First we trigger a retryable transient error that should cause the client to try to resume the
330
        // session in 5 minutes.
331
        auto test_cmd_res =
2✔
332
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
333
                .get();
2✔
334
        REQUIRE(test_cmd_res == "{}");
2!
335

336
        // Wait for the
337
        state.wait_for(TestState::InitialResume);
2✔
338

339
        // Once we're suspended, immediately tell the sync client to resume the session. This should cancel the
340
        // timer that would have auto-resumed the session.
341
        r->sync_session()->handle_reconnect();
2✔
342
        state.transition_with([&](TestState cur_state) {
2✔
343
            REQUIRE(cur_state == TestState::InitialResume);
2!
344
            return TestState::SecondBind;
2✔
345
        });
2✔
346
        state.wait_for(TestState::SecondSuspend);
2✔
347

348
        // Once we're connected again trigger another retryable transient error. Before RCORE-1770 the timer
349
        // to auto-resume the session would have still been active here and we would crash when trying to start
350
        // a second timer to auto-resume after this error.
351
        test_cmd_res =
2✔
352
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
353
                .get();
2✔
354
        REQUIRE(test_cmd_res == "{}");
2!
355
        state.wait_for(TestState::SecondResume);
2✔
356

357
        // Finally resume the session again which should cancel the second timer and the session should auto-resume
358
        // normally without crashing.
359
        r->sync_session()->handle_reconnect();
2✔
360
        state.transition_with([&](TestState cur_state) {
2✔
361
            REQUIRE(cur_state == TestState::SecondResume);
2!
362
            return TestState::Done;
2✔
363
        });
2✔
364
        wait_for_download(*r);
2✔
365
    }
2✔
366

367
    SECTION("handles unknown errors gracefully") {
18✔
368
        auto r = Realm::get_shared_realm(config);
2✔
369
        wait_for_download(*r);
2✔
370
        nlohmann::json error_body = {
2✔
371
            {"tryAgain", false},         {"message", "fake error"},
2✔
372
            {"shouldClientReset", true}, {"isRecoveryModeDisabled", false},
2✔
373
            {"action", "ClientReset"},
2✔
374
        };
2✔
375
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
376
                                       {"args", nlohmann::json{{"errorCode", 299}, {"errorBody", error_body}}}};
2✔
377
        auto test_cmd_res =
2✔
378
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
379
                .get();
2✔
380
        REQUIRE(test_cmd_res == "{}");
2!
381
        auto error = wait_for_future(std::move(error_future)).get();
2✔
382
        REQUIRE(error.status == ErrorCodes::UnknownError);
2!
383
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset);
2!
384
        REQUIRE(error.is_fatal);
2!
385
        REQUIRE_THAT(error.status.reason(),
2✔
386
                     Catch::Matchers::ContainsSubstring("Unknown sync protocol error code 299"));
2✔
387
        REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error"));
2✔
388
    }
2✔
389

390
    SECTION("unknown errors without actions are application bugs") {
18✔
391
        auto r = Realm::get_shared_realm(config);
2✔
392
        wait_for_download(*r);
2✔
393
        nlohmann::json error_body = {
2✔
394
            {"tryAgain", false},
2✔
395
            {"message", "fake error"},
2✔
396
            {"shouldClientReset", false},
2✔
397
            {"isRecoveryModeDisabled", false},
2✔
398
        };
2✔
399
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
400
                                       {"args", nlohmann::json{{"errorCode", 299}, {"errorBody", error_body}}}};
2✔
401
        auto test_cmd_res =
2✔
402
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
403
                .get();
2✔
404
        REQUIRE(test_cmd_res == "{}");
2!
405
        auto error = wait_for_future(std::move(error_future)).get();
2✔
406
        REQUIRE(error.status == ErrorCodes::UnknownError);
2!
407
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
408
        REQUIRE(error.is_fatal);
2!
409
        REQUIRE_THAT(error.status.reason(),
2✔
410
                     Catch::Matchers::ContainsSubstring("Unknown sync protocol error code 299"));
2✔
411
        REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error"));
2✔
412
    }
2✔
413

414
    SECTION("handles unknown actions gracefully") {
18✔
415
        auto r = Realm::get_shared_realm(config);
2✔
416
        wait_for_download(*r);
2✔
417
        nlohmann::json error_body = {
2✔
418
            {"tryAgain", false},
2✔
419
            {"message", "fake error"},
2✔
420
            {"shouldClientReset", true},
2✔
421
            {"isRecoveryModeDisabled", false},
2✔
422
            {"action", "FakeActionThatWillNeverExist"},
2✔
423
        };
2✔
424
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
425
                                       {"args", nlohmann::json{{"errorCode", 201}, {"errorBody", error_body}}}};
2✔
426
        auto test_cmd_res =
2✔
427
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
428
                .get();
2✔
429
        REQUIRE(test_cmd_res == "{}");
2!
430
        auto error = wait_for_future(std::move(error_future)).get();
2✔
431
        REQUIRE(error.status == ErrorCodes::RuntimeError);
2!
432
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
433
        REQUIRE(error.is_fatal);
2!
434
        REQUIRE_THAT(error.status.reason(), !Catch::Matchers::ContainsSubstring("Unknown sync protocol error code"));
2✔
435
        REQUIRE_THAT(error.status.reason(), Catch::Matchers::ContainsSubstring("fake error"));
2✔
436
    }
2✔
437

438

439
    SECTION("unknown connection-level errors are still errors") {
18✔
440
        auto r = Realm::get_shared_realm(config);
2✔
441
        wait_for_download(*r);
2✔
442
        nlohmann::json error_body = {{"tryAgain", false},
2✔
443
                                     {"message", "fake error"},
2✔
444
                                     {"shouldClientReset", false},
2✔
445
                                     {"isRecoveryModeDisabled", false},
2✔
446
                                     {"action", "ApplicationBug"}};
2✔
447
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
2✔
448
                                       {"args", nlohmann::json{{"errorCode", 199}, {"errorBody", error_body}}}};
2✔
449
        auto test_cmd_res =
2✔
450
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
2✔
451
                .get();
2✔
452
        REQUIRE(test_cmd_res == "{}");
2!
453
        auto error = wait_for_future(std::move(error_future)).get();
2✔
454
        REQUIRE(error.status == ErrorCodes::SyncProtocolInvariantFailed);
2!
455
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ProtocolViolation);
2!
456
        REQUIRE(error.is_fatal);
2!
457
    }
2✔
458

459
    SECTION("client reset errors") {
18✔
460
        auto r = Realm::get_shared_realm(config);
6✔
461
        wait_for_download(*r);
6✔
462
        nlohmann::json error_body = {{"tryAgain", false},
6✔
463
                                     {"message", "fake error"},
6✔
464
                                     {"shouldClientReset", true},
6✔
465
                                     {"isRecoveryModeDisabled", false},
6✔
466
                                     {"action", "ClientReset"}};
6✔
467
        auto code = GENERATE(sync::ProtocolError::bad_client_file_ident, sync::ProtocolError::bad_server_version,
6✔
468
                             sync::ProtocolError::diverging_histories);
6✔
469
        nlohmann::json test_command = {{"command", "ECHO_ERROR"},
6✔
470
                                       {"args", nlohmann::json{{"errorCode", code}, {"errorBody", error_body}}}};
6✔
471
        auto test_cmd_res =
6✔
472
            wait_for_future(SyncSession::OnlyForTesting::send_test_command(*r->sync_session(), test_command.dump()))
6✔
473
                .get();
6✔
474
        REQUIRE(test_cmd_res == "{}");
6!
475
        auto error = wait_for_future(std::move(error_future)).get();
6✔
476
        REQUIRE(error.status == ErrorCodes::SyncClientResetRequired);
6!
477
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset);
6!
478
        REQUIRE(error.is_client_reset_requested());
6!
479
        REQUIRE(error.is_fatal);
6!
480
    }
6✔
481

482

483
    SECTION("teardown") {
18✔
484
        harness.reset();
2✔
485
    }
2✔
486
}
18✔
487

488

489
TEST_CASE("flx: client reset", "[sync][flx][client reset][baas]") {
46✔
490
    std::vector<ObjectSchema> schema{
46✔
491
        {"TopLevel",
46✔
492
         {
46✔
493
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
46✔
494
             {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
46✔
495
             {"queryable_int_field", PropertyType::Int | PropertyType::Nullable},
46✔
496
             {"non_queryable_field", PropertyType::String | PropertyType::Nullable},
46✔
497
             {"list_of_ints_field", PropertyType::Int | PropertyType::Array},
46✔
498
             {"sum_of_list_field", PropertyType::Int},
46✔
499
             {"any_mixed", PropertyType::Mixed | PropertyType::Nullable},
46✔
500
             {"dictionary_mixed", PropertyType::Dictionary | PropertyType::Mixed | PropertyType::Nullable},
46✔
501
         }},
46✔
502
        {"TopLevel2",
46✔
503
         {
46✔
504
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
46✔
505
             {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
46✔
506
         }},
46✔
507
    };
46✔
508

509
    // some of these tests make additive schema changes which is only allowed in dev mode
510
    constexpr bool dev_mode = true;
46✔
511
    FLXSyncTestHarness harness("flx_client_reset",
46✔
512
                               {schema, {"queryable_str_field", "queryable_int_field"}, {}, dev_mode});
46✔
513

514
    auto add_object = [](SharedRealm realm, std::string str_field, int64_t int_field,
46✔
515
                         ObjectId oid = ObjectId::gen()) {
122✔
516
        CppContext c(realm);
122✔
517
        realm->begin_transaction();
122✔
518

519
        int64_t r1 = random_int();
122✔
520
        int64_t r2 = random_int();
122✔
521
        int64_t r3 = random_int();
122✔
522
        int64_t sum = uint64_t(r1) + r2 + r3;
122✔
523

524
        Object::create(c, realm, "TopLevel",
122✔
525
                       std::any(AnyDict{{"_id", oid},
122✔
526
                                        {"queryable_str_field", str_field},
122✔
527
                                        {"queryable_int_field", int_field},
122✔
528
                                        {"non_queryable_field", "non queryable 1"s},
122✔
529
                                        {"list_of_ints_field", std::vector<std::any>{r1, r2, r3}},
122✔
530
                                        {"sum_of_list_field", sum}}));
122✔
531
        realm->commit_transaction();
122✔
532
    };
122✔
533

534
    auto subscribe_to_and_add_objects = [&](SharedRealm realm, size_t num_objects) {
46✔
535
        auto table = realm->read_group().get_table("class_TopLevel");
40✔
536
        auto id_col = table->get_primary_key_column();
40✔
537
        auto sub_set = realm->get_latest_subscription_set();
40✔
538
        for (size_t i = 0; i < num_objects; ++i) {
130✔
539
            auto oid = ObjectId::gen();
90✔
540
            auto mut_sub = sub_set.make_mutable_copy();
90✔
541
            mut_sub.clear();
90✔
542
            mut_sub.insert_or_assign(Query(table).equal(id_col, oid));
90✔
543
            sub_set = mut_sub.commit();
90✔
544
            add_object(realm, util::format("added _id='%1'", oid), 0, oid);
90✔
545
        }
90✔
546
    };
40✔
547

548
    auto add_subscription_for_new_object = [&](SharedRealm realm, std::string str_field,
46✔
549
                                               int64_t int_field) -> sync::SubscriptionSet {
46✔
550
        auto table = realm->read_group().get_table("class_TopLevel");
28✔
551
        auto queryable_str_field = table->get_column_key("queryable_str_field");
28✔
552
        auto sub_set = realm->get_latest_subscription_set().make_mutable_copy();
28✔
553
        sub_set.insert_or_assign(Query(table).equal(queryable_str_field, StringData(str_field)));
28✔
554
        auto resulting_set = sub_set.commit();
28✔
555
        add_object(realm, str_field, int_field);
28✔
556
        return resulting_set;
28✔
557
    };
28✔
558

559
    auto add_invalid_subscription = [](SharedRealm realm) -> sync::SubscriptionSet {
46✔
560
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
561
        auto queryable_str_field = table->get_column_key("non_queryable_field");
2✔
562
        auto sub_set = realm->get_latest_subscription_set().make_mutable_copy();
2✔
563
        sub_set.insert_or_assign(Query(table).equal(queryable_str_field, "foo"));
2✔
564
        auto resulting_set = sub_set.commit();
2✔
565
        return resulting_set;
2✔
566
    };
2✔
567

568
    auto count_queries_with_str = [](sync::SubscriptionSet subs, std::string_view str) {
46✔
569
        size_t count = 0;
8✔
570
        for (auto sub : subs) {
14✔
571
            if (sub.query_string.find(str) != std::string::npos) {
14✔
572
                ++count;
6✔
573
            }
6✔
574
        }
14✔
575
        return count;
8✔
576
    };
8✔
577
    create_user_and_log_in(harness.app());
46✔
578
    auto user1 = harness.app()->current_user();
46✔
579
    create_user_and_log_in(harness.app());
46✔
580
    auto user2 = harness.app()->current_user();
46✔
581
    SyncTestFile config_local(user1, harness.schema(), SyncConfig::FLXSyncEnabled{});
46✔
582
    config_local.path += ".local";
46✔
583
    SyncTestFile config_remote(user2, harness.schema(), SyncConfig::FLXSyncEnabled{});
46✔
584
    config_remote.path += ".remote";
46✔
585
    const std::string str_field_value = "foo";
46✔
586
    const int64_t local_added_int = 100;
46✔
587
    const int64_t local_added_int2 = 150;
46✔
588
    const int64_t remote_added_int = 200;
46✔
589
    size_t before_reset_count = 0;
46✔
590
    size_t after_reset_count = 0;
46✔
591
    config_local.sync_config->notify_before_client_reset = [&before_reset_count](SharedRealm) {
46✔
592
        ++before_reset_count;
30✔
593
    };
30✔
594
    config_local.sync_config->notify_after_client_reset = [&after_reset_count](SharedRealm, ThreadSafeReference,
46✔
595
                                                                               bool) {
46✔
596
        ++after_reset_count;
×
597
    };
×
598

599
    config_local.sync_config->on_sync_client_event_hook = [](std::weak_ptr<SyncSession> weak_session,
46✔
600
                                                             const SyncClientHookData& data) {
2,621✔
601
        // To prevent the upload cursors from becoming out of sync when the local realm assumes
602
        // the client file ident of the fresh realm, UPLOAD messages are not allowed during the
603
        // fresh realm download so the server's upload cursor versions start at 0 when the
604
        // local realm resumes after the client reset.
605
        if (data.event == SyncClientHookEvent::UploadMessageSent) {
2,621✔
606
            // If this is an UPLOAD message event, check to see if the fresh realm is being downloaded
607
            if (auto session = weak_session.lock()) {
585✔
608
                // Check for a "fresh" path to determine if this is a client reset fresh download session
609
                if (_impl::client_reset::is_fresh_path(session->path())) {
577✔
610
                    FAIL("UPLOAD messages are not allowed during client reset fresh realm download");
×
611
                }
×
612
            }
577✔
613
        }
585✔
614
        return SyncClientHookAction::NoAction;
2,621✔
615
    };
2,621✔
616

617
    SECTION("Recover: offline writes and subscription (single subscription)") {
46✔
618
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
619
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
620
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
621
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
622
        test_reset
2✔
623
            ->populate_initial_object([&](SharedRealm realm) {
2✔
624
                auto pk_of_added_object = ObjectId::gen();
2✔
625
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
626
                auto table = realm->read_group().get_table(ObjectStore::table_name_for_object_type("TopLevel"));
2✔
627
                REALM_ASSERT(table);
2✔
628
                mut_subs.insert_or_assign(Query(table));
2✔
629
                mut_subs.commit();
2✔
630

631
                realm->begin_transaction();
2✔
632
                CppContext c(realm);
2✔
633
                int64_t r1 = random_int();
2✔
634
                int64_t r2 = random_int();
2✔
635
                int64_t r3 = random_int();
2✔
636
                int64_t sum = uint64_t(r1) + r2 + r3;
2✔
637

638
                Object::create(c, realm, "TopLevel",
2✔
639
                               std::any(AnyDict{{"_id"s, pk_of_added_object},
2✔
640
                                                {"queryable_str_field"s, "initial value"s},
2✔
641
                                                {"list_of_ints_field", std::vector<std::any>{r1, r2, r3}},
2✔
642
                                                {"sum_of_list_field", sum}}));
2✔
643

644
                realm->commit_transaction();
2✔
645
                wait_for_upload(*realm);
2✔
646
                return pk_of_added_object;
2✔
647
            })
2✔
648
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
649
                add_object(local_realm, str_field_value, local_added_int);
2✔
650
            })
2✔
651
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
652
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
653
                sync::SubscriptionSet::State actual =
2✔
654
                    remote_realm->get_latest_subscription_set()
2✔
655
                        .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
656
                        .get();
2✔
657
                REQUIRE(actual == sync::SubscriptionSet::State::Complete);
2!
658
            })
2✔
659
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) {
2✔
660
                wait_for_advance(*local_realm);
2✔
661
                ClientResyncMode mode = client_reset_future.get();
2✔
662
                REQUIRE(mode == ClientResyncMode::Recover);
2!
663
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
664
                auto str_col = table->get_column_key("queryable_str_field");
2✔
665
                auto int_col = table->get_column_key("queryable_int_field");
2✔
666
                auto tv = table->where().equal(str_col, StringData(str_field_value)).find_all();
2✔
667
                tv.sort(int_col);
2✔
668
                // the object we created while offline was recovered, and the remote object was downloaded
669
                REQUIRE(tv.size() == 2);
2!
670
                CHECK(tv.get_object(0).get<Int>(int_col) == local_added_int);
2!
671
                CHECK(tv.get_object(1).get<Int>(int_col) == remote_added_int);
2!
672
            })
2✔
673
            ->run();
2✔
674
    }
2✔
675

676
    SECTION("Recover: subscription and offline writes after client reset failure") {
46✔
677
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
678
        auto&& [error_future, error_handler] = make_error_handler();
2✔
679
        config_local.sync_config->error_handler = error_handler;
2✔
680

681
        std::string fresh_path = realm::_impl::client_reset::get_fresh_path_for(config_local.path);
2✔
682
        // create a non-empty directory that we'll fail to delete
683
        util::make_dir(fresh_path);
2✔
684
        util::File(util::File::resolve("file", fresh_path), util::File::mode_Write);
2✔
685

686
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
687
        test_reset
2✔
688
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
689
                auto mut_sub = local_realm->get_latest_subscription_set().make_mutable_copy();
2✔
690
                auto table = local_realm->read_group().get_table("class_TopLevel2");
2✔
691
                mut_sub.insert_or_assign(Query(table));
2✔
692
                mut_sub.commit();
2✔
693

694
                CppContext c(local_realm);
2✔
695
                local_realm->begin_transaction();
2✔
696
                Object::create(c, local_realm, "TopLevel2",
2✔
697
                               std::any(AnyDict{{"_id"s, ObjectId::gen()}, {"queryable_str_field"s, "foo"s}}));
2✔
698
                local_realm->commit_transaction();
2✔
699
            })
2✔
700
            ->on_post_reset([](SharedRealm local_realm) {
2✔
701
                // Verify offline subscription was not removed.
702
                auto subs = local_realm->get_latest_subscription_set();
2✔
703
                auto table = local_realm->read_group().get_table("class_TopLevel2");
2✔
704
                REQUIRE(subs.find(Query(table)));
2!
705
            })
2✔
706
            ->run();
2✔
707

708
        // Remove the folder preventing the completion of a client reset.
709
        util::try_remove_dir_recursive(fresh_path);
2✔
710

711
        RealmConfig config_copy = config_local;
2✔
712
        config_copy.sync_config = std::make_shared<SyncConfig>(*config_copy.sync_config);
2✔
713
        config_copy.sync_config->error_handler = nullptr;
2✔
714
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
715
        config_copy.sync_config->notify_after_client_reset = reset_handler;
2✔
716

717
        // Attempt to open the realm again.
718
        // This time the client reset succeeds and the offline subscription and writes are recovered.
719
        auto realm = Realm::get_shared_realm(config_copy);
2✔
720
        ClientResyncMode mode = reset_future.get();
2✔
721
        REQUIRE(mode == ClientResyncMode::Recover);
2!
722

723
        auto table = realm->read_group().get_table("class_TopLevel2");
2✔
724
        auto str_col = table->get_column_key("queryable_str_field");
2✔
725
        REQUIRE(table->size() == 1);
2!
726
        REQUIRE(table->get_object(0).get<String>(str_col) == "foo");
2!
727
    }
2✔
728

729
    SECTION("Recover: offline writes and subscriptions (multiple subscriptions)") {
46✔
730
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
731
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
732
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
733
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
734
        test_reset
2✔
735
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
736
                add_subscription_for_new_object(local_realm, str_field_value, local_added_int);
2✔
737
            })
2✔
738
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
739
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
740
                sync::SubscriptionSet::State actual =
2✔
741
                    remote_realm->get_latest_subscription_set()
2✔
742
                        .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
743
                        .get();
2✔
744
                REQUIRE(actual == sync::SubscriptionSet::State::Complete);
2!
745
            })
2✔
746
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) {
2✔
747
                ClientResyncMode mode = client_reset_future.get();
2✔
748
                REQUIRE(mode == ClientResyncMode::Recover);
2!
749
                auto subs = local_realm->get_latest_subscription_set();
2✔
750
                subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
751
                subs.refresh();
2✔
752
                // make sure that the subscription for "foo" survived the reset
753
                size_t count_of_foo = count_queries_with_str(subs, util::format("\"%1\"", str_field_value));
2✔
754
                REQUIRE(subs.state() == sync::SubscriptionSet::State::Complete);
2!
755
                REQUIRE(count_of_foo == 1);
2!
756
                local_realm->refresh();
2✔
757
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
758
                auto str_col = table->get_column_key("queryable_str_field");
2✔
759
                auto int_col = table->get_column_key("queryable_int_field");
2✔
760
                auto tv = table->where().equal(str_col, StringData(str_field_value)).find_all();
2✔
761
                tv.sort(int_col);
2✔
762
                // the object we created while offline was recovered, and the remote object was downloaded
763
                REQUIRE(tv.size() == 2);
2!
764
                CHECK(tv.get_object(0).get<Int>(int_col) == local_added_int);
2!
765
                CHECK(tv.get_object(1).get<Int>(int_col) == remote_added_int);
2!
766
            })
2✔
767
            ->run();
2✔
768
    }
2✔
769

770
    SECTION("Recover: offline writes interleaved with subscriptions and empty writes") {
46✔
771
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
772
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
773
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
774
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
775
        test_reset
2✔
776
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
777
                // The sequence of events bellow generates five changesets:
778
                //  1. create sub1 => empty changeset
779
                //  2. create local_added_int object
780
                //  3. create empty changeset
781
                //  4. create sub2 => empty changeset
782
                //  5. create local_added_int2 object
783
                //
784
                // Before sending 'sub2' to the server, an UPLOAD message is sent first.
785
                // The upload message contains changeset 2. (local_added_int) with the cursor
786
                // of changeset 3. (empty changeset).
787

788
                add_subscription_for_new_object(local_realm, str_field_value, local_added_int);
2✔
789
                // Commit empty changeset.
790
                local_realm->begin_transaction();
2✔
791
                local_realm->commit_transaction();
2✔
792
                add_subscription_for_new_object(local_realm, str_field_value, local_added_int2);
2✔
793
            })
2✔
794
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
795
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
796
                sync::SubscriptionSet::State actual =
2✔
797
                    remote_realm->get_latest_subscription_set()
2✔
798
                        .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
799
                        .get();
2✔
800
                REQUIRE(actual == sync::SubscriptionSet::State::Complete);
2!
801
            })
2✔
802
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) {
2✔
803
                ClientResyncMode mode = client_reset_future.get();
2✔
804
                REQUIRE(mode == ClientResyncMode::Recover);
2!
805
                auto subs = local_realm->get_latest_subscription_set();
2✔
806
                subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
807
                // make sure that the subscription for "foo" survived the reset
808
                size_t count_of_foo = count_queries_with_str(subs, util::format("\"%1\"", str_field_value));
2✔
809
                subs.refresh();
2✔
810
                REQUIRE(subs.state() == sync::SubscriptionSet::State::Complete);
2!
811
                REQUIRE(count_of_foo == 1);
2!
812
                local_realm->refresh();
2✔
813
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
814
                auto str_col = table->get_column_key("queryable_str_field");
2✔
815
                auto int_col = table->get_column_key("queryable_int_field");
2✔
816
                auto tv = table->where().equal(str_col, StringData(str_field_value)).find_all();
2✔
817
                tv.sort(int_col);
2✔
818
                // the objects we created while offline was recovered, and the remote object was downloaded
819
                REQUIRE(tv.size() == 3);
2!
820
                CHECK(tv.get_object(0).get<Int>(int_col) == local_added_int);
2!
821
                CHECK(tv.get_object(1).get<Int>(int_col) == local_added_int2);
2!
822
                CHECK(tv.get_object(2).get<Int>(int_col) == remote_added_int);
2!
823
            })
2✔
824
            ->run();
2✔
825
    }
2✔
826

827
    auto validate_integrity_of_arrays = [](TableRef table) -> size_t {
46✔
828
        auto sum_col = table->get_column_key("sum_of_list_field");
16✔
829
        auto array_col = table->get_column_key("list_of_ints_field");
16✔
830
        auto query = table->column<Lst<Int>>(array_col).sum() == table->column<Int>(sum_col) &&
16✔
831
                     table->column<Lst<Int>>(array_col).size() > 0;
16✔
832
        return query.count();
16✔
833
    };
16✔
834

835
    SECTION("Recover: offline writes with associated subscriptions in the correct order") {
46✔
836
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
837
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
838
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
839
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
840
        constexpr size_t num_objects_added = 20;
2✔
841
        constexpr size_t num_objects_added_by_harness = 1; // BaasFLXClientReset.run()
2✔
842
        constexpr size_t num_objects_added_by_remote = 1;  // make_remote_changes()
2✔
843
        test_reset
2✔
844
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
845
                subscribe_to_and_add_objects(local_realm, num_objects_added);
2✔
846
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
847
                REQUIRE(table->size() == num_objects_added + num_objects_added_by_harness);
2!
848
                size_t count_of_valid_array_data = validate_integrity_of_arrays(table);
2✔
849
                REQUIRE(count_of_valid_array_data == num_objects_added);
2!
850
            })
2✔
851
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
852
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
853
                sync::SubscriptionSet::State actual =
2✔
854
                    remote_realm->get_latest_subscription_set()
2✔
855
                        .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
856
                        .get();
2✔
857
                REQUIRE(actual == sync::SubscriptionSet::State::Complete);
2!
858
            })
2✔
859
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) {
2✔
860
                ClientResyncMode mode = client_reset_future.get();
2✔
861
                REQUIRE(mode == ClientResyncMode::Recover);
2!
862
                local_realm->refresh();
2✔
863
                auto latest_subs = local_realm->get_latest_subscription_set();
2✔
864
                auto state = latest_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
865
                REQUIRE(state == sync::SubscriptionSet::State::Complete);
2!
866
                local_realm->refresh();
2✔
867
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
868
                if (table->size() != 1) {
2✔
869
                    table->to_json(std::cout);
×
870
                }
×
871
                REQUIRE(table->size() == 1);
2!
872
                auto mut_sub = latest_subs.make_mutable_copy();
2✔
873
                mut_sub.clear();
2✔
874
                mut_sub.insert_or_assign(Query(table));
2✔
875
                latest_subs = mut_sub.commit();
2✔
876
                latest_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
877
                local_realm->refresh();
2✔
878
                REQUIRE(table->size() ==
2!
879
                        num_objects_added + num_objects_added_by_harness + num_objects_added_by_remote);
2✔
880
                size_t count_of_valid_array_data = validate_integrity_of_arrays(table);
2✔
881
                REQUIRE(count_of_valid_array_data == num_objects_added + num_objects_added_by_remote);
2!
882
            })
2✔
883
            ->run();
2✔
884
    }
2✔
885

886
    SECTION("Recover: incompatible property changes are rejected") {
46✔
887
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
888
        auto&& [error_future, err_handler] = make_error_handler();
2✔
889
        config_local.sync_config->error_handler = err_handler;
2✔
890
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
891
        constexpr size_t num_objects_added_before = 2;
2✔
892
        constexpr size_t num_objects_added_after = 2;
2✔
893
        constexpr size_t num_objects_added_by_harness = 1; // BaasFLXClientReset.run()
2✔
894
        constexpr std::string_view added_property_name = "new_property";
2✔
895
        test_reset
2✔
896
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
897
                subscribe_to_and_add_objects(local_realm, num_objects_added_before);
2✔
898
                Schema local_update = schema;
2✔
899
                Schema::iterator it = local_update.find("TopLevel");
2✔
900
                REQUIRE(it != local_update.end());
2!
901
                it->persisted_properties.push_back(
2✔
902
                    {std::string(added_property_name), PropertyType::Float | PropertyType::Nullable});
2✔
903
                local_realm->update_schema(local_update);
2✔
904
                subscribe_to_and_add_objects(local_realm, num_objects_added_after);
2✔
905
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
906
                REQUIRE(table->size() ==
2!
907
                        num_objects_added_before + num_objects_added_after + num_objects_added_by_harness);
2✔
908
                size_t count_of_valid_array_data = validate_integrity_of_arrays(table);
2✔
909
                REQUIRE(count_of_valid_array_data == num_objects_added_before + num_objects_added_after);
2!
910
            })
2✔
911
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
912
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
913
                Schema remote_update = schema;
2✔
914
                Schema::iterator it = remote_update.find("TopLevel");
2✔
915
                REQUIRE(it != remote_update.end());
2!
916
                it->persisted_properties.push_back(
2✔
917
                    {std::string(added_property_name), PropertyType::UUID | PropertyType::Nullable});
2✔
918
                remote_realm->update_schema(remote_update);
2✔
919
                sync::SubscriptionSet::State actual =
2✔
920
                    remote_realm->get_latest_subscription_set()
2✔
921
                        .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
922
                        .get();
2✔
923
                REQUIRE(actual == sync::SubscriptionSet::State::Complete);
2!
924
            })
2✔
925
            ->on_post_reset([&, err_future = std::move(error_future)](SharedRealm local_realm) mutable {
2✔
926
                auto sync_error = wait_for_future(std::move(err_future)).get();
2✔
927
                REQUIRE(before_reset_count == 1);
2!
928
                REQUIRE(after_reset_count == 0);
2!
929
                REQUIRE(sync_error.status == ErrorCodes::AutoClientResetFailed);
2!
930
                REQUIRE(sync_error.status.reason().find(
2!
931
                            "SyncClientResetRequired: Bad client file identifier (IDENT)") != std::string::npos);
2✔
932
                REQUIRE(sync_error.is_client_reset_requested());
2!
933
                local_realm->refresh();
2✔
934
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
935
                // since schema validation happens in the first recovery commit, that whole commit is rolled back
936
                // and the final state here is "pre reset"
937
                REQUIRE(table->size() ==
2!
938
                        num_objects_added_before + num_objects_added_by_harness + num_objects_added_after);
2✔
939
                size_t count_of_valid_array_data = validate_integrity_of_arrays(table);
2✔
940
                REQUIRE(count_of_valid_array_data == num_objects_added_before + num_objects_added_after);
2!
941
            })
2✔
942
            ->run();
2✔
943
    }
2✔
944

945
    SECTION("unsuccessful replay of local changes") {
46✔
946
        constexpr size_t num_objects_added_before = 2;
4✔
947
        constexpr size_t num_objects_added_after = 2;
4✔
948
        constexpr size_t num_objects_added_by_harness = 1; // BaasFLXClientReset.run()
4✔
949
        constexpr std::string_view added_property_name = "new_property";
4✔
950
        auto&& [error_future, err_handler] = make_error_handler();
4✔
951
        config_local.sync_config->error_handler = err_handler;
4✔
952

953
        // The local changes here are a bit contrived because removing a column is disallowed
954
        // at the object store layer for sync'd Realms. The only reason a recovery should fail in production
955
        // during the apply stage is due to programmer error or external factors such as out of disk space.
956
        // Any schema discrepancies are caught by the initial diff, so the way to make a recovery fail here is
957
        // to add and remove a column at the core level such that the schema diff passes, but instructions are
958
        // generated which will fail when applied.
959
        auto make_local_changes_that_will_fail = [&](SharedRealm local_realm) {
4✔
960
            subscribe_to_and_add_objects(local_realm, num_objects_added_before);
4✔
961
            auto table = local_realm->read_group().get_table("class_TopLevel");
4✔
962
            REQUIRE(table->size() == num_objects_added_before + num_objects_added_by_harness);
4!
963
            size_t count_of_valid_array_data = validate_integrity_of_arrays(table);
4✔
964
            REQUIRE(count_of_valid_array_data == num_objects_added_before);
4!
965
            local_realm->begin_transaction();
4✔
966
            ColKey added = table->add_column(type_Int, added_property_name);
4✔
967
            table->remove_column(added);
4✔
968
            local_realm->commit_transaction();
4✔
969
            subscribe_to_and_add_objects(local_realm, num_objects_added_after); // these are lost!
4✔
970
        };
4✔
971

972
        VersionID expected_version;
4✔
973

974
        auto store_pre_reset_state = [&](SharedRealm local_realm) {
4✔
975
            expected_version = local_realm->read_transaction_version();
4✔
976
        };
4✔
977

978
        auto verify_post_reset_state = [&, err_future = std::move(error_future)](SharedRealm local_realm) {
4✔
979
            auto sync_error = err_future.get();
4✔
980
            REQUIRE(before_reset_count == 1);
4!
981
            REQUIRE(after_reset_count == 0);
4!
982
            REQUIRE(sync_error.status == ErrorCodes::AutoClientResetFailed);
4!
983
            REQUIRE(sync_error.is_client_reset_requested());
4!
984

985
            // All changes should have been rolled back when recovery hit remove_column(),
986
            // leaving the Realm in the pre-reset state
987
            local_realm->refresh();
4✔
988
            auto table = local_realm->read_group().get_table("class_TopLevel");
4✔
989
            ColKey added = table->get_column_key(added_property_name);
4✔
990
            REQUIRE(!added);
4!
991
            const size_t expected_added_objects = num_objects_added_before + num_objects_added_after;
4✔
992
            REQUIRE(table->size() == num_objects_added_by_harness + expected_added_objects);
4!
993
            size_t count_of_valid_array_data = validate_integrity_of_arrays(table);
4✔
994
            REQUIRE(count_of_valid_array_data == expected_added_objects);
4!
995

996
            // The attempted client reset should have been recorded so that we
997
            // don't attempt it again
998
            REQUIRE(local_realm->read_transaction_version().version == expected_version.version + 1);
4!
999
        };
4✔
1000

1001
        SECTION("Recover: unsuccessful recovery leads to a manual reset") {
4✔
1002
            config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1003
            auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1004
            test_reset->make_local_changes(make_local_changes_that_will_fail)
2✔
1005
                ->on_post_local_changes(store_pre_reset_state)
2✔
1006
                ->on_post_reset(std::move(verify_post_reset_state))
2✔
1007
                ->run();
2✔
1008
            RealmConfig config_copy = config_local;
2✔
1009
            auto&& [error_future2, err_handler2] = make_error_handler();
2✔
1010
            config_copy.sync_config->error_handler = err_handler2;
2✔
1011
            auto realm_post_reset = Realm::get_shared_realm(config_copy);
2✔
1012
            auto sync_error = wait_for_future(std::move(error_future2)).get();
2✔
1013
            REQUIRE(before_reset_count == 2);
2!
1014
            REQUIRE(after_reset_count == 0);
2!
1015
            REQUIRE(sync_error.status == ErrorCodes::AutoClientResetFailed);
2!
1016
            REQUIRE(sync_error.is_client_reset_requested());
2!
1017
        }
2✔
1018

1019
        SECTION("RecoverOrDiscard: unsuccessful reapply leads to discard") {
4✔
1020
            config_local.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
1021
            auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1022
            test_reset->make_local_changes(make_local_changes_that_will_fail)
2✔
1023
                ->on_post_local_changes(store_pre_reset_state)
2✔
1024
                ->on_post_reset(std::move(verify_post_reset_state))
2✔
1025
                ->run();
2✔
1026

1027
            RealmConfig config_copy = config_local;
2✔
1028
            auto&& [client_reset_future, reset_handler] = make_client_reset_handler();
2✔
1029
            config_copy.sync_config->error_handler = [](std::shared_ptr<SyncSession>, SyncError err) {
2✔
1030
                REALM_ASSERT_EX(!err.is_fatal, err.status);
×
1031
                CHECK(err.server_requests_action == sync::ProtocolErrorInfo::Action::Transient);
×
1032
            };
×
1033
            config_copy.sync_config->notify_after_client_reset = reset_handler;
2✔
1034
            auto realm_post_reset = Realm::get_shared_realm(config_copy);
2✔
1035
            ClientResyncMode mode = wait_for_future(std::move(client_reset_future)).get();
2✔
1036
            REQUIRE(mode == ClientResyncMode::DiscardLocal);
2!
1037
            realm_post_reset->refresh();
2✔
1038
            auto table = realm_post_reset->read_group().get_table("class_TopLevel");
2✔
1039
            ColKey added = table->get_column_key(added_property_name);
2✔
1040
            REQUIRE(!added);                                        // reverted local changes
2!
1041
            REQUIRE(table->size() == num_objects_added_by_harness); // discarded all offline local changes
2!
1042
        }
2✔
1043
    }
4✔
1044

1045
    SECTION("DiscardLocal: offline writes and subscriptions are lost") {
46✔
1046
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1047
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
1048
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
1049
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1050
        test_reset
2✔
1051
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
1052
                add_subscription_for_new_object(local_realm, str_field_value, local_added_int);
2✔
1053
            })
2✔
1054
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
1055
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
1056
            })
2✔
1057
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) mutable {
2✔
1058
                ClientResyncMode mode = wait_for_future(std::move(client_reset_future)).get();
2✔
1059
                REQUIRE(mode == ClientResyncMode::DiscardLocal);
2!
1060
                auto subs = local_realm->get_latest_subscription_set();
2✔
1061
                wait_for_future(subs.get_state_change_notification(sync::SubscriptionSet::State::Complete)).get();
2✔
1062
                local_realm->refresh();
2✔
1063
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
1064
                auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
1065
                auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
1066
                auto tv = table->where().equal(queryable_str_field, StringData(str_field_value)).find_all();
2✔
1067
                // the object we created while offline was discarded, and the remote object was not downloaded
1068
                REQUIRE(tv.size() == 0);
2!
1069
                size_t count_of_foo = count_queries_with_str(subs, util::format("\"%1\"", str_field_value));
2✔
1070
                // make sure that the subscription for "foo" did not survive the reset
1071
                REQUIRE(count_of_foo == 0);
2!
1072
                REQUIRE(subs.state() == sync::SubscriptionSet::State::Complete);
2!
1073

1074
                // adding data and subscriptions to a reset Realm works as normal
1075
                add_subscription_for_new_object(local_realm, str_field_value, local_added_int);
2✔
1076
                auto latest_subs = local_realm->get_latest_subscription_set();
2✔
1077
                REQUIRE(latest_subs.version() > subs.version());
2!
1078
                wait_for_future(latest_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete))
2✔
1079
                    .get();
2✔
1080
                local_realm->refresh();
2✔
1081
                count_of_foo = count_queries_with_str(latest_subs, util::format("\"%1\"", str_field_value));
2✔
1082
                REQUIRE(count_of_foo == 1);
2!
1083
                tv = table->where().equal(queryable_str_field, StringData(str_field_value)).find_all();
2✔
1084
                REQUIRE(tv.size() == 2);
2!
1085
                tv.sort(queryable_int_field);
2✔
1086
                REQUIRE(tv.get_object(0).get<int64_t>(queryable_int_field) == local_added_int);
2!
1087
                REQUIRE(tv.get_object(1).get<int64_t>(queryable_int_field) == remote_added_int);
2!
1088
            })
2✔
1089
            ->run();
2✔
1090
    }
2✔
1091

1092
    SECTION("DiscardLocal: an invalid subscription made while offline becomes superseded") {
46✔
1093
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1094
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
1095
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
1096
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1097
        std::unique_ptr<sync::SubscriptionSet> invalid_sub;
2✔
1098
        test_reset
2✔
1099
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
1100
                invalid_sub = std::make_unique<sync::SubscriptionSet>(add_invalid_subscription(local_realm));
2✔
1101
                add_subscription_for_new_object(local_realm, str_field_value, local_added_int);
2✔
1102
            })
2✔
1103
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
1104
                add_subscription_for_new_object(remote_realm, str_field_value, remote_added_int);
2✔
1105
            })
2✔
1106
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) {
2✔
1107
                local_realm->refresh();
2✔
1108
                sync::SubscriptionSet::State actual =
2✔
1109
                    invalid_sub->get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
1110
                REQUIRE(actual == sync::SubscriptionSet::State::Superseded);
2!
1111
                ClientResyncMode mode = client_reset_future.get();
2✔
1112
                REQUIRE(mode == ClientResyncMode::DiscardLocal);
2!
1113
            })
2✔
1114
            ->run();
2✔
1115
    }
2✔
1116

1117
    SECTION("DiscardLocal: an error is produced if a previously successful query becomes invalid due to "
46✔
1118
            "server changes across a reset") {
2✔
1119
        // Disable dev mode so non-queryable fields are not automatically added as queryable
1120
        const AppSession& app_session = harness.session().app_session();
2✔
1121
        app_session.admin_api.set_development_mode_to(app_session.server_app_id, false);
2✔
1122
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1123
        auto&& [error_future, err_handler] = make_error_handler();
2✔
1124
        config_local.sync_config->error_handler = err_handler;
2✔
1125
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1126
        test_reset
2✔
1127
            ->setup([&](SharedRealm realm) {
2✔
1128
                if (realm->sync_session()->path() == config_local.path) {
2✔
1129
                    auto added_sub = add_subscription_for_new_object(realm, str_field_value, 0);
2✔
1130
                    added_sub.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
1131
                }
2✔
1132
            })
2✔
1133
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
1134
                add_object(local_realm, str_field_value, local_added_int);
2✔
1135
                // Make "queryable_str_field" not a valid query field.
1136
                // Pre-reset, the Realm had a successful query on it, but now when the client comes back online
1137
                // and tries to reset, the fresh Realm download will fail with a query error.
1138
                const AppSession& app_session = harness.session().app_session();
2✔
1139
                auto baas_sync_service = app_session.admin_api.get_sync_service(app_session.server_app_id);
2✔
1140
                auto baas_sync_config =
2✔
1141
                    app_session.admin_api.get_config(app_session.server_app_id, baas_sync_service);
2✔
1142
                REQUIRE(baas_sync_config.queryable_field_names->is_array());
2!
1143
                auto it = baas_sync_config.queryable_field_names->begin();
2✔
1144
                for (; it != baas_sync_config.queryable_field_names->end(); ++it) {
2✔
1145
                    if (*it == "queryable_str_field") {
2✔
1146
                        break;
2✔
1147
                    }
2✔
1148
                }
2✔
1149
                REQUIRE(it != baas_sync_config.queryable_field_names->end());
2!
1150
                baas_sync_config.queryable_field_names->erase(it);
2✔
1151
                app_session.admin_api.enable_sync(app_session.server_app_id, baas_sync_service.id, baas_sync_config);
2✔
1152
            })
2✔
1153
            ->on_post_reset([&, err_future = std::move(error_future)](SharedRealm) mutable {
2✔
1154
                auto sync_error = wait_for_future(std::move(err_future)).get();
2✔
1155
                INFO(sync_error.status);
2✔
1156
                CHECK(sync_error.status == ErrorCodes::AutoClientResetFailed);
2!
1157
                REQUIRE(sync_error.status.reason().find(
2!
1158
                            "SyncClientResetRequired: Bad client file identifier (IDENT)") != std::string::npos);
2✔
1159
            })
2✔
1160
            ->run();
2✔
1161
    }
2✔
1162

1163
    SECTION("DiscardLocal: completion callbacks fire after client reset even when there is no data to download") {
46✔
1164
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1165
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
1166
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
1167
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1168
        test_reset
2✔
1169
            ->on_post_local_changes([&](SharedRealm realm) {
2✔
1170
                wait_for_upload(*realm);
2✔
1171
                wait_for_download(*realm);
2✔
1172
            })
2✔
1173
            ->run();
2✔
1174
    }
2✔
1175

1176
    SECTION("DiscardLocal: open realm after client reset failure") {
46✔
1177
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1178
        auto&& [error_future, error_handler] = make_error_handler();
2✔
1179
        config_local.sync_config->error_handler = error_handler;
2✔
1180

1181
        std::string fresh_path = realm::_impl::client_reset::get_fresh_path_for(config_local.path);
2✔
1182
        // create a non-empty directory that we'll fail to delete
1183
        util::make_dir(fresh_path);
2✔
1184
        util::File(util::File::resolve("file", fresh_path), util::File::mode_Write);
2✔
1185

1186
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1187
        test_reset->run();
2✔
1188

1189
        // Client reset fails due to sync client not being able to create the fresh realm.
1190
        auto sync_error = wait_for_future(std::move(error_future)).get();
2✔
1191
        REQUIRE(sync_error.status == ErrorCodes::AutoClientResetFailed);
2!
1192

1193
        // Open the realm again. This should not crash.
1194
        auto&& [err_future, err_handler] = make_error_handler();
2✔
1195
        config_local.sync_config->error_handler = std::move(err_handler);
2✔
1196

1197
        auto realm_post_reset = Realm::get_shared_realm(config_local);
2✔
1198
        sync_error = wait_for_future(std::move(err_future)).get();
2✔
1199
        REQUIRE(sync_error.status == ErrorCodes::AutoClientResetFailed);
2!
1200
    }
2✔
1201

1202
    enum class ResetMode { NoReset, InitiateClientReset };
46✔
1203
    auto seed_realm = [&harness, &subscribe_to_and_add_objects](RealmConfig config, ResetMode reset_mode) {
46✔
1204
        config.sync_config->error_handler = [path = config.path](std::shared_ptr<SyncSession>, SyncError err) {
26✔
1205
            // ignore spurious failures on this instance
1206
            util::format(std::cout, "spurious error while seeding a Realm at '%1': %2\n", path, err.status);
×
1207
        };
×
1208
        SharedRealm realm = Realm::get_shared_realm(config);
26✔
1209
        subscribe_to_and_add_objects(realm, 1);
26✔
1210
        auto subs = realm->get_latest_subscription_set();
26✔
1211
        auto result = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
26✔
1212
        CHECK(result == sync::SubscriptionSet::State::Complete);
26!
1213
        if (reset_mode == ResetMode::InitiateClientReset) {
26✔
1214
            reset_utils::trigger_client_reset(harness.session().app_session(), realm);
18✔
1215
        }
18✔
1216
        realm->close();
26✔
1217
    };
26✔
1218

1219
    auto setup_reset_handlers_for_schema_validation =
46✔
1220
        [&before_reset_count, &after_reset_count](RealmConfig& config, Schema expected_schema) {
46✔
1221
            auto& sync_config = *config.sync_config;
14✔
1222
            sync_config.error_handler = [](std::shared_ptr<SyncSession>, SyncError err) {
14✔
1223
                FAIL(err.status);
×
1224
            };
×
1225
            sync_config.notify_before_client_reset = [&before_reset_count,
14✔
1226
                                                      expected = expected_schema](SharedRealm frozen_before) {
14✔
1227
                ++before_reset_count;
14✔
1228
                REQUIRE(frozen_before->schema().size() > 0);
14!
1229
                REQUIRE(frozen_before->schema_version() != ObjectStore::NotVersioned);
14!
1230
                REQUIRE(frozen_before->schema() == expected);
14!
1231
            };
14✔
1232

1233
            auto [promise, future] = util::make_promise_future<void>();
14✔
1234
            sync_config.notify_after_client_reset =
14✔
1235
                [&after_reset_count, promise = util::CopyablePromiseHolder<void>(std::move(promise)), expected_schema,
14✔
1236
                 reset_mode = config.sync_config->client_resync_mode, has_schema = config.schema.has_value()](
14✔
1237
                    SharedRealm frozen_before, ThreadSafeReference after_ref, bool did_recover) mutable {
14✔
1238
                    ++after_reset_count;
14✔
1239
                    REQUIRE(frozen_before->schema().size() > 0);
14!
1240
                    REQUIRE(frozen_before->schema_version() != ObjectStore::NotVersioned);
14!
1241
                    REQUIRE(frozen_before->schema() == expected_schema);
14!
1242
                    SharedRealm after = Realm::get_shared_realm(std::move(after_ref), util::Scheduler::make_dummy());
14✔
1243
                    if (!has_schema) {
14✔
1244
                        after->set_schema_subset(expected_schema);
4✔
1245
                    }
4✔
1246
                    REQUIRE(after);
14!
1247
                    REQUIRE(after->schema() == expected_schema);
14!
1248
                    // the above check is sufficient unless operator==() is changed to not care about ordering
1249
                    // so future proof that by explicitly checking the order of properties here as well
1250
                    REQUIRE(after->schema().size() == frozen_before->schema().size());
14!
1251
                    auto after_it = after->schema().find("TopLevel");
14✔
1252
                    auto before_it = frozen_before->schema().find("TopLevel");
14✔
1253
                    REQUIRE(after_it != after->schema().end());
14!
1254
                    REQUIRE(before_it != frozen_before->schema().end());
14!
1255
                    REQUIRE(after_it->name == before_it->name);
14!
1256
                    REQUIRE(after_it->persisted_properties.size() == before_it->persisted_properties.size());
14!
1257
                    REQUIRE(after_it->persisted_properties[1].name == "queryable_int_field");
14!
1258
                    REQUIRE(after_it->persisted_properties[2].name == "queryable_str_field");
14!
1259
                    REQUIRE(before_it->persisted_properties[1].name == "queryable_int_field");
14!
1260
                    REQUIRE(before_it->persisted_properties[2].name == "queryable_str_field");
14!
1261
                    REQUIRE(did_recover == (reset_mode == ClientResyncMode::Recover));
14!
1262
                    promise.get_promise().emplace_value();
14✔
1263
                };
14✔
1264
            return std::move(future); // move is not redundant here because of how destructing works
14✔
1265
        };
14✔
1266

1267
    SECTION("Recover: schema indexes match in before and after states") {
46✔
1268
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1269
        // reorder a property such that it does not match the on disk property order
1270
        std::vector<ObjectSchema> local_schema = schema;
2✔
1271
        std::swap(local_schema[0].persisted_properties[1], local_schema[0].persisted_properties[2]);
2✔
1272
        local_schema[0].persisted_properties.push_back(
2✔
1273
            {"queryable_oid_field", PropertyType::ObjectId | PropertyType::Nullable});
2✔
1274
        config_local.schema = local_schema;
2✔
1275
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1276
        auto future = setup_reset_handlers_for_schema_validation(config_local, local_schema);
2✔
1277
        SharedRealm realm = Realm::get_shared_realm(config_local);
2✔
1278
        future.get();
2✔
1279
        CHECK(before_reset_count == 1);
2!
1280
        CHECK(after_reset_count == 1);
2!
1281
    }
2✔
1282

1283
    SECTION("Adding a local property matching a server addition is allowed") {
46✔
1284
        auto mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
4✔
1285
        config_local.sync_config->client_resync_mode = mode;
4✔
1286
        CHECK_NOTHROW(seed_realm(config_local, ResetMode::InitiateClientReset));
4✔
1287
        std::vector<ObjectSchema> changed_schema = schema;
4✔
1288
        changed_schema[0].persisted_properties.push_back(
4✔
1289
            {"queryable_oid_field", PropertyType::ObjectId | PropertyType::Nullable});
4✔
1290
        // In a separate Realm, make the property addition.
1291
        // Since this is dev mode, it will be added to the server's schema.
1292
        config_remote.schema = changed_schema;
4✔
1293
        CHECK_NOTHROW(seed_realm(config_remote, ResetMode::NoReset));
4✔
1294
        std::swap(changed_schema[0].persisted_properties[1], changed_schema[0].persisted_properties[2]);
4✔
1295
        config_local.schema = changed_schema;
4✔
1296
        auto future = setup_reset_handlers_for_schema_validation(config_local, changed_schema);
4✔
1297
        successfully_async_open_realm(config_local);
4✔
1298
        CHECK_NOTHROW(future.get());
4✔
1299
        CHECK(before_reset_count == 1);
4!
1300
        CHECK(after_reset_count == 1);
4!
1301
    }
4✔
1302

1303
    SECTION("Adding a local property matching a server addition inside the before reset callback is allowed") {
46✔
1304
        auto mode = GENERATE(ClientResyncMode::DiscardLocal, ClientResyncMode::Recover);
4✔
1305
        config_local.sync_config->client_resync_mode = mode;
4✔
1306
        seed_realm(config_local, ResetMode::InitiateClientReset);
4✔
1307
        std::vector<ObjectSchema> changed_schema = schema;
4✔
1308
        changed_schema[0].persisted_properties.push_back(
4✔
1309
            {"queryable_oid_field", PropertyType::ObjectId | PropertyType::Nullable});
4✔
1310
        // In a separate Realm, make the property addition.
1311
        // Since this is dev mode, it will be added to the server's schema.
1312
        config_remote.schema = changed_schema;
4✔
1313
        seed_realm(config_remote, ResetMode::NoReset);
4✔
1314
        std::swap(changed_schema[0].persisted_properties[1], changed_schema[0].persisted_properties[2]);
4✔
1315
        config_local.schema.reset();
4✔
1316
        config_local.sync_config->freeze_before_reset_realm = false;
4✔
1317
        auto future = setup_reset_handlers_for_schema_validation(config_local, changed_schema);
4✔
1318

1319
        auto notify_before = std::move(config_local.sync_config->notify_before_client_reset);
4✔
1320
        config_local.sync_config->notify_before_client_reset = [=](std::shared_ptr<Realm> realm) {
4✔
1321
            realm->update_schema(changed_schema);
4✔
1322
            notify_before(realm);
4✔
1323
        };
4✔
1324

1325
        auto notify_after = std::move(config_local.sync_config->notify_after_client_reset);
4✔
1326
        config_local.sync_config->notify_after_client_reset = [=](std::shared_ptr<Realm> before,
4✔
1327
                                                                  ThreadSafeReference after, bool did_recover) {
4✔
1328
            before->set_schema_subset(changed_schema);
4✔
1329
            notify_after(before, std::move(after), did_recover);
4✔
1330
        };
4✔
1331

1332
        successfully_async_open_realm(config_local);
4✔
1333
        future.get();
4✔
1334
        CHECK(before_reset_count == 1);
4!
1335
        CHECK(after_reset_count == 1);
4!
1336
    }
4✔
1337

1338
    auto make_additive_changes = [](std::vector<ObjectSchema> schema) {
46✔
1339
        schema[0].persisted_properties.push_back(
6✔
1340
            {"added_oid_field", PropertyType::ObjectId | PropertyType::Nullable});
6✔
1341
        std::swap(schema[0].persisted_properties[1], schema[0].persisted_properties[2]);
6✔
1342
        schema.push_back({"AddedClass",
6✔
1343
                          {
6✔
1344
                              {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
6✔
1345
                              {"str_field", PropertyType::String | PropertyType::Nullable},
6✔
1346
                          }});
6✔
1347
        return schema;
6✔
1348
    };
6✔
1349
    SECTION("Recover: additive schema changes are recovered in dev mode") {
46✔
1350
        const AppSession& app_session = harness.session().app_session();
2✔
1351
        app_session.admin_api.set_development_mode_to(app_session.server_app_id, true);
2✔
1352
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1353
        std::vector<ObjectSchema> changed_schema = make_additive_changes(schema);
2✔
1354
        config_local.schema = changed_schema;
2✔
1355
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1356
        ThreadSafeReference ref_async;
2✔
1357
        auto future = setup_reset_handlers_for_schema_validation(config_local, changed_schema);
2✔
1358
        {
2✔
1359
            auto realm = successfully_async_open_realm(config_local);
2✔
1360
            future.get();
2✔
1361
            CHECK(before_reset_count == 1);
2!
1362
            CHECK(after_reset_count == 1);
2!
1363

1364
            // make changes to the newly added property
1365
            realm->begin_transaction();
2✔
1366
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
1367
            ColKey new_col = table->get_column_key("added_oid_field");
2✔
1368
            REQUIRE(new_col);
2!
1369
            for (auto it = table->begin(); it != table->end(); ++it) {
4✔
1370
                it->set(new_col, ObjectId::gen());
2✔
1371
            }
2✔
1372
            realm->commit_transaction();
2✔
1373
            // subscribe to the new Class and add an object
1374
            auto new_table = realm->read_group().get_table("class_AddedClass");
2✔
1375
            auto sub_set = realm->get_latest_subscription_set();
2✔
1376
            auto mut_sub = sub_set.make_mutable_copy();
2✔
1377
            mut_sub.insert_or_assign(Query(new_table));
2✔
1378
            mut_sub.commit();
2✔
1379
            realm->begin_transaction();
2✔
1380
            REQUIRE(new_table);
2!
1381
            new_table->create_object_with_primary_key(ObjectId::gen());
2✔
1382
            realm->commit_transaction();
2✔
1383
            auto result = realm->get_latest_subscription_set()
2✔
1384
                              .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
1385
                              .get();
2✔
1386
            CHECK(result == sync::SubscriptionSet::State::Complete);
2!
1387
            realm->sync_session()->shutdown_and_wait();
2✔
1388
            realm->close();
2✔
1389
        }
2✔
1390
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(config_local.path));
2!
1391
        {
2✔
1392
            // ensure that an additional schema change after the successful reset is also accepted by the server
1393
            changed_schema[0].persisted_properties.push_back(
2✔
1394
                {"added_oid_field_second", PropertyType::ObjectId | PropertyType::Nullable});
2✔
1395
            changed_schema.push_back({"AddedClassSecond",
2✔
1396
                                      {
2✔
1397
                                          {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1398
                                          {"str_field_2", PropertyType::String | PropertyType::Nullable},
2✔
1399
                                      }});
2✔
1400
            config_local.schema = changed_schema;
2✔
1401
            auto realm = Realm::get_shared_realm(config_local);
2✔
1402
            auto table = realm->read_group().get_table("class_AddedClassSecond");
2✔
1403
            ColKey new_col = table->get_column_key("str_field_2");
2✔
1404
            REQUIRE(new_col);
2!
1405
            auto new_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
1406
            new_subs.insert_or_assign(Query(table).equal(new_col, "hello"));
2✔
1407
            auto subs = new_subs.commit();
2✔
1408
            realm->begin_transaction();
2✔
1409
            table->create_object_with_primary_key(Mixed{ObjectId::gen()}, {{new_col, "hello"}});
2✔
1410
            realm->commit_transaction();
2✔
1411
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
1412
            wait_for_advance(*realm);
2✔
1413
            REQUIRE(table->size() == 1);
2!
1414
        }
2✔
1415
    }
2✔
1416

1417
    SECTION("DiscardLocal: additive schema changes not allowed") {
46✔
1418
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1419
        std::vector<ObjectSchema> changed_schema = make_additive_changes(schema);
2✔
1420
        config_local.schema = changed_schema;
2✔
1421
        config_local.sync_config->client_resync_mode = ClientResyncMode::DiscardLocal;
2✔
1422
        auto&& [error_future, err_handler] = make_error_handler();
2✔
1423
        config_local.sync_config->error_handler = err_handler;
2✔
1424
        auto status = async_open_realm(config_local);
2✔
1425
        REQUIRE_FALSE(status.is_ok());
2!
1426
        REQUIRE_THAT(status.get_status().reason(),
2✔
1427
                     Catch::Matchers::ContainsSubstring(
2✔
1428
                         "'Client reset cannot recover when classes have been removed: {AddedClass}'"));
2✔
1429
        error_future.get();
2✔
1430
        CHECK(before_reset_count == 1);
2!
1431
        CHECK(after_reset_count == 0);
2!
1432
    }
2✔
1433

1434
    SECTION("Recover: incompatible schema changes on async open are an error") {
46✔
1435
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1436
        std::vector<ObjectSchema> changed_schema = schema;
2✔
1437
        changed_schema[0].persisted_properties[0].type = PropertyType::UUID; // incompatible type change
2✔
1438
        config_local.schema = changed_schema;
2✔
1439
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1440
        auto&& [error_future, err_handler] = make_error_handler();
2✔
1441
        config_local.sync_config->error_handler = err_handler;
2✔
1442
        auto status = async_open_realm(config_local);
2✔
1443
        REQUIRE_FALSE(status.is_ok());
2!
1444
        REQUIRE_THAT(
2✔
1445
            status.get_status().reason(),
2✔
1446
            Catch::Matchers::ContainsSubstring(
2✔
1447
                "'The following changes cannot be made in additive-only schema mode:\n"
2✔
1448
                "- Property 'TopLevel._id' has been changed from 'object id' to 'uuid'.\nIf your app is running in "
2✔
1449
                "development mode, you can delete the realm and restart the app to update your schema.'"));
2✔
1450
        error_future.get();
2✔
1451
        CHECK(before_reset_count == 0); // we didn't even get this far because opening the frozen realm fails
2!
1452
        CHECK(after_reset_count == 0);
2!
1453
    }
2✔
1454

1455
    SECTION("Recover: additive schema changes without dev mode produce an error after client reset") {
46✔
1456
        const AppSession& app_session = harness.session().app_session();
2✔
1457
        app_session.admin_api.set_development_mode_to(app_session.server_app_id, true);
2✔
1458
        seed_realm(config_local, ResetMode::InitiateClientReset);
2✔
1459
        // Disable dev mode so that schema changes are not allowed
1460
        app_session.admin_api.set_development_mode_to(app_session.server_app_id, false);
2✔
1461
        auto cleanup = util::make_scope_exit([&]() noexcept {
2✔
1462
            const AppSession& app_session = harness.session().app_session();
2✔
1463
            app_session.admin_api.set_development_mode_to(app_session.server_app_id, true);
2✔
1464
        });
2✔
1465

1466
        std::vector<ObjectSchema> changed_schema = make_additive_changes(schema);
2✔
1467
        config_local.schema = changed_schema;
2✔
1468
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1469
        (void)setup_reset_handlers_for_schema_validation(config_local, changed_schema);
2✔
1470
        auto&& [error_future, err_handler] = make_error_handler();
2✔
1471
        config_local.sync_config->error_handler = err_handler;
2✔
1472
        auto realm = successfully_async_open_realm(config_local);
2✔
1473
        // make changes to the new property
1474
        realm->begin_transaction();
2✔
1475
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
1476
        ColKey new_col = table->get_column_key("added_oid_field");
2✔
1477
        REQUIRE(new_col);
2!
1478
        for (auto it = table->begin(); it != table->end(); ++it) {
4✔
1479
            it->set(new_col, ObjectId::gen());
2✔
1480
        }
2✔
1481
        realm->commit_transaction();
2✔
1482
        auto err = error_future.get();
2✔
1483
        std::string property_err = "Invalid schema change (UPLOAD): non-breaking schema change: adding "
2✔
1484
                                   "\"ObjectID\" column at field \"added_oid_field\" in schema \"TopLevel\", "
2✔
1485
                                   "schema changes from clients are restricted when developer mode is disabled";
2✔
1486
        std::string class_err = "Invalid schema change (UPLOAD): non-breaking schema change: adding schema "
2✔
1487
                                "for Realm table \"AddedClass\", schema changes from clients are restricted when "
2✔
1488
                                "developer mode is disabled";
2✔
1489
        REQUIRE_THAT(err.status.reason(), Catch::Matchers::ContainsSubstring(property_err) ||
2✔
1490
                                              Catch::Matchers::ContainsSubstring(class_err));
2✔
1491
        CHECK(before_reset_count == 1);
2!
1492
        CHECK(after_reset_count == 1);
2!
1493
    }
2✔
1494

1495
    SECTION("Recover: inserts in collections in mixed - collections cleared remotely") {
46✔
1496
        config_local.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
1497
        auto&& [reset_future, reset_handler] = make_client_reset_handler();
2✔
1498
        config_local.sync_config->notify_after_client_reset = reset_handler;
2✔
1499
        auto test_reset = reset_utils::make_baas_flx_client_reset(config_local, config_remote, harness.session());
2✔
1500
        test_reset
2✔
1501
            ->populate_initial_object([&](SharedRealm realm) {
2✔
1502
                subscribe_to_all_and_bootstrap(*realm);
2✔
1503
                auto pk_of_added_object = ObjectId::gen();
2✔
1504
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
1505

1506
                realm->begin_transaction();
2✔
1507
                CppContext c(realm);
2✔
1508
                auto obj = Object::create(c, realm, "TopLevel",
2✔
1509
                                          std::any(AnyDict{{"_id"s, pk_of_added_object},
2✔
1510
                                                           {"queryable_str_field"s, "initial value"s},
2✔
1511
                                                           {"sum_of_list_field"s, int64_t(42)}}));
2✔
1512
                auto col_any = table->get_column_key("any_mixed");
2✔
1513
                obj.get_obj().set_collection(col_any, CollectionType::List);
2✔
1514
                auto list = obj.get_obj().get_list_ptr<Mixed>(col_any);
2✔
1515
                list->add(1);
2✔
1516
                auto dict = obj.get_obj().get_dictionary("dictionary_mixed");
2✔
1517
                dict.insert("key", 42);
2✔
1518
                realm->commit_transaction();
2✔
1519
                wait_for_upload(*realm);
2✔
1520
                return pk_of_added_object;
2✔
1521
            })
2✔
1522
            ->make_local_changes([&](SharedRealm local_realm) {
2✔
1523
                local_realm->begin_transaction();
2✔
1524
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
1525
                auto col_any = table->get_column_key("any_mixed");
2✔
1526
                auto obj = table->get_object(0);
2✔
1527
                auto list = obj.get_list_ptr<Mixed>(col_any);
2✔
1528
                list->add(2);
2✔
1529
                auto dict = obj.get_dictionary("dictionary_mixed");
2✔
1530
                dict.insert("key2", "value");
2✔
1531
                local_realm->commit_transaction();
2✔
1532
            })
2✔
1533
            ->make_remote_changes([&](SharedRealm remote_realm) {
2✔
1534
                remote_realm->begin_transaction();
2✔
1535
                auto table = remote_realm->read_group().get_table("class_TopLevel");
2✔
1536
                auto col_any = table->get_column_key("any_mixed");
2✔
1537
                auto obj = table->get_object(0);
2✔
1538
                auto list = obj.get_list_ptr<Mixed>(col_any);
2✔
1539
                list->clear();
2✔
1540
                auto dict = obj.get_dictionary("dictionary_mixed");
2✔
1541
                dict.clear();
2✔
1542
                remote_realm->commit_transaction();
2✔
1543
            })
2✔
1544
            ->on_post_reset([&, client_reset_future = std::move(reset_future)](SharedRealm local_realm) {
2✔
1545
                wait_for_advance(*local_realm);
2✔
1546
                ClientResyncMode mode = client_reset_future.get();
2✔
1547
                REQUIRE(mode == ClientResyncMode::Recover);
2!
1548
                auto table = local_realm->read_group().get_table("class_TopLevel");
2✔
1549
                auto obj = table->get_object(0);
2✔
1550
                auto col_any = table->get_column_key("any_mixed");
2✔
1551
                auto list = obj.get_list_ptr<Mixed>(col_any);
2✔
1552
                CHECK(list->size() == 1);
2!
1553
                CHECK(list->get_any(0).get_int() == 2);
2!
1554
                auto dict = obj.get_dictionary("dictionary_mixed");
2✔
1555
                CHECK(dict.size() == 1);
2!
1556
                CHECK(dict.get("key2").get_string() == "value");
2!
1557
            })
2✔
1558
            ->run();
2✔
1559
    }
2✔
1560
}
46✔
1561

1562
TEST_CASE("flx: creating an object on a class with no subscription throws", "[sync][flx][subscription][baas]") {
2✔
1563
    FLXSyncTestHarness harness("flx_bad_query", {g_simple_embedded_obj_schema, {"queryable_str_field"}});
2✔
1564
    harness.do_with_new_user([&](auto user) {
2✔
1565
        SyncTestFile config(user, harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
1566
        auto [error_promise, error_future] = util::make_promise_future<SyncError>();
2✔
1567
        auto shared_promise = std::make_shared<decltype(error_promise)>(std::move(error_promise));
2✔
1568
        config.sync_config->error_handler = [error_promise = std::move(shared_promise)](std::shared_ptr<SyncSession>,
2✔
1569
                                                                                        SyncError err) {
2✔
1570
            CHECK(err.server_requests_action == sync::ProtocolErrorInfo::Action::Transient);
×
1571
            error_promise->emplace_value(std::move(err));
×
1572
        };
×
1573

1574
        auto realm = Realm::get_shared_realm(config);
2✔
1575
        CppContext c(realm);
2✔
1576
        realm->begin_transaction();
2✔
1577
        REQUIRE_THROWS_AS(
2✔
1578
            Object::create(c, realm, "TopLevel",
2✔
1579
                           std::any(AnyDict{{"_id", ObjectId::gen()}, {"queryable_str_field", "foo"s}})),
2✔
1580
            NoSubscriptionForWrite);
2✔
1581
        realm->cancel_transaction();
2✔
1582

1583
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
1584

1585
        REQUIRE(table->is_empty());
2!
1586
        auto col_key = table->get_column_key("queryable_str_field");
2✔
1587
        {
2✔
1588
            auto new_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
1589
            new_subs.insert_or_assign(Query(table).equal(col_key, "foo"));
2✔
1590
            auto subs = new_subs.commit();
2✔
1591
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
1592
        }
2✔
1593

1594
        realm->begin_transaction();
2✔
1595
        auto obj = Object::create(c, realm, "TopLevel",
2✔
1596
                                  std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
1597
                                                   {"queryable_str_field", "foo"s},
2✔
1598
                                                   {"embedded_obj", AnyDict{{"str_field", "bar"s}}}}));
2✔
1599
        realm->commit_transaction();
2✔
1600

1601
        realm->begin_transaction();
2✔
1602
        auto embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
1603
        embedded_obj.set_property_value(c, "str_field", std::any{"baz"s});
2✔
1604
        realm->commit_transaction();
2✔
1605

1606
        wait_for_upload(*realm);
2✔
1607
        wait_for_download(*realm);
2✔
1608
    });
2✔
1609
}
2✔
1610

1611
TEST_CASE("flx: uploading an object that is out-of-view results in compensating write",
1612
          "[sync][flx][compensating write][baas]") {
16✔
1613
    static std::optional<FLXSyncTestHarness> harness;
16✔
1614
    if (!harness) {
16✔
1615
        Schema schema{{"TopLevel",
2✔
1616
                       {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1617
                        {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
1618
                        {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "TopLevel_embedded_obj"}}},
2✔
1619
                      {"TopLevel_embedded_obj",
2✔
1620
                       ObjectSchema::ObjectType::Embedded,
2✔
1621
                       {{"str_field", PropertyType::String | PropertyType::Nullable}}},
2✔
1622
                      {"Int PK",
2✔
1623
                       {
2✔
1624
                           {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1625
                           {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
1626
                       }},
2✔
1627
                      {"String PK",
2✔
1628
                       {
2✔
1629
                           {"_id", PropertyType::String, Property::IsPrimary{true}},
2✔
1630
                           {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
1631
                       }},
2✔
1632
                      {"UUID PK",
2✔
1633
                       {
2✔
1634
                           {"_id", PropertyType::UUID, Property::IsPrimary{true}},
2✔
1635
                           {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
1636
                       }}};
2✔
1637

1638
        AppCreateConfig::ServiceRole role;
2✔
1639
        role.name = "compensating_write_perms";
2✔
1640

1641
        AppCreateConfig::ServiceRoleDocumentFilters doc_filters;
2✔
1642
        doc_filters.read = true;
2✔
1643
        doc_filters.write = {{"queryable_str_field", {{"$in", nlohmann::json::array({"foo", "bar"})}}}};
2✔
1644
        role.document_filters = doc_filters;
2✔
1645

1646
        role.insert_filter = true;
2✔
1647
        role.delete_filter = true;
2✔
1648
        role.read = true;
2✔
1649
        role.write = true;
2✔
1650
        FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field"}, {role}};
2✔
1651
        harness.emplace("flx_bad_query", server_schema);
2✔
1652
    }
2✔
1653

1654
    create_user_and_log_in(harness->app());
16✔
1655
    auto user = harness->app()->current_user();
16✔
1656

1657
    auto make_error_handler = [] {
16✔
1658
        auto [error_promise, error_future] = util::make_promise_future<SyncError>();
16✔
1659
        auto shared_promise = std::make_shared<decltype(error_promise)>(std::move(error_promise));
16✔
1660
        auto fn = [error_promise = std::move(shared_promise)](std::shared_ptr<SyncSession>, SyncError err) mutable {
16✔
1661
            if (!error_promise) {
14✔
1662
                util::format(std::cerr,
×
1663
                             "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n",
×
1664
                             err.status);
×
1665
                abort();
×
1666
            }
×
1667
            error_promise->emplace_value(std::move(err));
14✔
1668
            error_promise.reset();
14✔
1669
        };
14✔
1670

1671
        return std::make_pair(std::move(error_future), std::move(fn));
16✔
1672
    };
16✔
1673

1674
    auto validate_sync_error = [&](const SyncError& sync_error, Mixed expected_pk, const char* expected_object_name,
16✔
1675
                                   const std::string& error_msg_fragment) {
16✔
1676
        CHECK(sync_error.status == ErrorCodes::SyncCompensatingWrite);
14!
1677
        CHECK(!sync_error.is_client_reset_requested());
14!
1678
        CHECK(sync_error.compensating_writes_info.size() == 1);
14!
1679
        CHECK(sync_error.server_requests_action == sync::ProtocolErrorInfo::Action::Warning);
14!
1680
        auto write_info = sync_error.compensating_writes_info[0];
14✔
1681
        CHECK(write_info.primary_key == expected_pk);
14!
1682
        CHECK(write_info.object_name == expected_object_name);
14!
1683
        CHECK_THAT(write_info.reason, Catch::Matchers::ContainsSubstring(error_msg_fragment));
14✔
1684
    };
14✔
1685

1686
    SyncTestFile config(user, harness->schema(), SyncConfig::FLXSyncEnabled{});
16✔
1687
    auto&& [error_future, err_handler] = make_error_handler();
16✔
1688
    config.sync_config->error_handler = err_handler;
16✔
1689
    auto realm = Realm::get_shared_realm(config);
16✔
1690
    auto table = realm->read_group().get_table("class_TopLevel");
16✔
1691

1692
    auto create_subscription = [&](StringData table_name, auto make_query) {
16✔
1693
        auto table = realm->read_group().get_table(table_name);
14✔
1694
        auto queryable_str_field = table->get_column_key("queryable_str_field");
14✔
1695
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
14✔
1696
        new_query.insert_or_assign(make_query(Query(table), queryable_str_field));
14✔
1697
        new_query.commit();
14✔
1698
    };
14✔
1699

1700
    SECTION("compensating write because of permission violation") {
16✔
1701
        create_subscription("class_TopLevel", [](auto q, auto col) {
2✔
1702
            return q.equal(col, "bizz");
2✔
1703
        });
2✔
1704

1705
        CppContext c(realm);
2✔
1706
        realm->begin_transaction();
2✔
1707
        auto invalid_obj = ObjectId::gen();
2✔
1708
        Object::create(c, realm, "TopLevel",
2✔
1709
                       std::any(AnyDict{{"_id", invalid_obj}, {"queryable_str_field", "bizz"s}}));
2✔
1710
        realm->commit_transaction();
2✔
1711

1712
        validate_sync_error(
2✔
1713
            std::move(error_future).get(), invalid_obj, "TopLevel",
2✔
1714
            util::format("write to ObjectID(\"%1\") in table \"TopLevel\" not allowed", invalid_obj.to_string()));
2✔
1715

1716
        wait_for_advance(*realm);
2✔
1717

1718
        auto top_level_table = realm->read_group().get_table("class_TopLevel");
2✔
1719
        REQUIRE(top_level_table->is_empty());
2!
1720
    }
2✔
1721

1722
    SECTION("compensating write because of permission violation with write on embedded object") {
16✔
1723
        create_subscription("class_TopLevel", [](auto q, auto col) {
2✔
1724
            return q.equal(col, "bizz").Or().equal(col, "foo");
2✔
1725
        });
2✔
1726

1727
        CppContext c(realm);
2✔
1728
        realm->begin_transaction();
2✔
1729
        auto invalid_obj = ObjectId::gen();
2✔
1730
        auto obj = Object::create(c, realm, "TopLevel",
2✔
1731
                                  std::any(AnyDict{{"_id", invalid_obj},
2✔
1732
                                                   {"queryable_str_field", "foo"s},
2✔
1733
                                                   {"embedded_obj", AnyDict{{"str_field", "bar"s}}}}));
2✔
1734
        realm->commit_transaction();
2✔
1735
        realm->begin_transaction();
2✔
1736
        obj.set_property_value(c, "queryable_str_field", std::any{"bizz"s});
2✔
1737
        realm->commit_transaction();
2✔
1738
        realm->begin_transaction();
2✔
1739
        auto embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
1740
        embedded_obj.set_property_value(c, "str_field", std::any{"baz"s});
2✔
1741
        realm->commit_transaction();
2✔
1742

1743
        validate_sync_error(
2✔
1744
            std::move(error_future).get(), invalid_obj, "TopLevel",
2✔
1745
            util::format("write to ObjectID(\"%1\") in table \"TopLevel\" not allowed", invalid_obj.to_string()));
2✔
1746

1747
        wait_for_advance(*realm);
2✔
1748

1749
        obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any(invalid_obj));
2✔
1750
        embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
1751
        REQUIRE(util::any_cast<std::string&&>(obj.get_property_value<std::any>(c, "queryable_str_field")) == "foo");
2!
1752
        REQUIRE(util::any_cast<std::string&&>(embedded_obj.get_property_value<std::any>(c, "str_field")) == "bar");
2!
1753

1754
        realm->begin_transaction();
2✔
1755
        embedded_obj.set_property_value(c, "str_field", std::any{"baz"s});
2✔
1756
        realm->commit_transaction();
2✔
1757

1758
        wait_for_upload(*realm);
2✔
1759
        wait_for_download(*realm);
2✔
1760

1761
        wait_for_advance(*realm);
2✔
1762
        obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any(invalid_obj));
2✔
1763
        embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
1764
        REQUIRE(embedded_obj.get_column_value<StringData>("str_field") == "baz");
2!
1765
    }
2✔
1766

1767
    SECTION("compensating write for writing a top-level object that is out-of-view") {
16✔
1768
        create_subscription("class_TopLevel", [](auto q, auto col) {
2✔
1769
            return q.equal(col, "foo");
2✔
1770
        });
2✔
1771

1772
        CppContext c(realm);
2✔
1773
        realm->begin_transaction();
2✔
1774
        auto valid_obj = ObjectId::gen();
2✔
1775
        auto invalid_obj = ObjectId::gen();
2✔
1776
        Object::create(c, realm, "TopLevel",
2✔
1777
                       std::any(AnyDict{
2✔
1778
                           {"_id", valid_obj},
2✔
1779
                           {"queryable_str_field", "foo"s},
2✔
1780
                       }));
2✔
1781
        Object::create(c, realm, "TopLevel",
2✔
1782
                       std::any(AnyDict{
2✔
1783
                           {"_id", invalid_obj},
2✔
1784
                           {"queryable_str_field", "bar"s},
2✔
1785
                       }));
2✔
1786
        realm->commit_transaction();
2✔
1787

1788
        validate_sync_error(std::move(error_future).get(), invalid_obj, "TopLevel",
2✔
1789
                            "object is outside of the current query view");
2✔
1790

1791
        wait_for_advance(*realm);
2✔
1792

1793
        auto top_level_table = realm->read_group().get_table("class_TopLevel");
2✔
1794
        REQUIRE(top_level_table->size() == 1);
2!
1795
        REQUIRE(top_level_table->get_object_with_primary_key(valid_obj));
2!
1796

1797
        // Verify that a valid object afterwards does not produce an error
1798
        realm->begin_transaction();
2✔
1799
        Object::create(c, realm, "TopLevel",
2✔
1800
                       std::any(AnyDict{
2✔
1801
                           {"_id", ObjectId::gen()},
2✔
1802
                           {"queryable_str_field", "foo"s},
2✔
1803
                       }));
2✔
1804
        realm->commit_transaction();
2✔
1805

1806
        wait_for_upload(*realm);
2✔
1807
        wait_for_download(*realm);
2✔
1808
    }
2✔
1809

1810
    SECTION("compensating writes for each primary key type") {
16✔
1811
        SECTION("int") {
8✔
1812
            create_subscription("class_Int PK", [](auto q, auto col) {
2✔
1813
                return q.equal(col, "foo");
2✔
1814
            });
2✔
1815
            realm->begin_transaction();
2✔
1816
            realm->read_group().get_table("class_Int PK")->create_object_with_primary_key(123456);
2✔
1817
            realm->commit_transaction();
2✔
1818

1819
            validate_sync_error(std::move(error_future).get(), 123456, "Int PK",
2✔
1820
                                "write to 123456 in table \"Int PK\" not allowed");
2✔
1821
        }
2✔
1822

1823
        SECTION("short string") {
8✔
1824
            create_subscription("class_String PK", [](auto q, auto col) {
2✔
1825
                return q.equal(col, "foo");
2✔
1826
            });
2✔
1827
            realm->begin_transaction();
2✔
1828
            realm->read_group().get_table("class_String PK")->create_object_with_primary_key("short");
2✔
1829
            realm->commit_transaction();
2✔
1830

1831
            validate_sync_error(std::move(error_future).get(), "short", "String PK",
2✔
1832
                                "write to \"short\" in table \"String PK\" not allowed");
2✔
1833
        }
2✔
1834

1835
        SECTION("long string") {
8✔
1836
            create_subscription("class_String PK", [](auto q, auto col) {
2✔
1837
                return q.equal(col, "foo");
2✔
1838
            });
2✔
1839
            realm->begin_transaction();
2✔
1840
            const char* pk = "long string which won't fit in the SSO buffer";
2✔
1841
            realm->read_group().get_table("class_String PK")->create_object_with_primary_key(pk);
2✔
1842
            realm->commit_transaction();
2✔
1843

1844
            validate_sync_error(std::move(error_future).get(), pk, "String PK",
2✔
1845
                                util::format("write to \"%1\" in table \"String PK\" not allowed", pk));
2✔
1846
        }
2✔
1847

1848
        SECTION("uuid") {
8✔
1849
            create_subscription("class_UUID PK", [](auto q, auto col) {
2✔
1850
                return q.equal(col, "foo");
2✔
1851
            });
2✔
1852
            realm->begin_transaction();
2✔
1853
            UUID pk("01234567-9abc-4def-9012-3456789abcde");
2✔
1854
            realm->read_group().get_table("class_UUID PK")->create_object_with_primary_key(pk);
2✔
1855
            realm->commit_transaction();
2✔
1856

1857
            validate_sync_error(std::move(error_future).get(), pk, "UUID PK",
2✔
1858
                                util::format("write to UUID(%1) in table \"UUID PK\" not allowed", pk));
2✔
1859
        }
2✔
1860
    }
8✔
1861

1862
    // Clear the Realm afterwards as we're reusing an app
1863
    realm->begin_transaction();
16✔
1864
    table->clear();
16✔
1865
    realm->commit_transaction();
16✔
1866
    wait_for_upload(*realm);
16✔
1867
    realm.reset();
16✔
1868

1869
    // Add new sections before this
1870
    SECTION("teardown") {
16✔
1871
        harness->app()->sync_manager()->wait_for_sessions_to_terminate();
2✔
1872
        harness.reset();
2✔
1873
    }
2✔
1874
}
16✔
1875

1876
TEST_CASE("flx: query on non-queryable field results in query error message", "[sync][flx][query][baas]") {
8✔
1877
    static std::optional<FLXSyncTestHarness> harness;
8✔
1878
    if (!harness) {
8✔
1879
        harness.emplace("flx_bad_query");
2✔
1880
    }
2✔
1881

1882
    auto create_subscription = [](SharedRealm realm, StringData table_name, StringData column_name, auto make_query) {
10✔
1883
        auto table = realm->read_group().get_table(table_name);
10✔
1884
        auto queryable_field = table->get_column_key(column_name);
10✔
1885
        auto new_query = realm->get_active_subscription_set().make_mutable_copy();
10✔
1886
        new_query.insert_or_assign(make_query(Query(table), queryable_field));
10✔
1887
        return new_query.commit();
10✔
1888
    };
10✔
1889

1890
    auto check_status = [](auto status) {
8✔
1891
        CHECK(!status.is_ok());
8!
1892
        std::string reason = status.get_status().reason();
8✔
1893
        // Depending on the version of baas used, it may return 'Invalid query:' or
1894
        // 'Client provided query with bad syntax:'
1895
        if ((reason.find("Invalid query:") == std::string::npos &&
8✔
1896
             reason.find("Client provided query with bad syntax:") == std::string::npos) ||
8!
1897
            reason.find("\"TopLevel\": key \"non_queryable_field\" is not a queryable field") == std::string::npos) {
8✔
1898
            FAIL(util::format("Error reason did not match expected: `%1`", reason));
×
1899
        }
×
1900
    };
8✔
1901

1902
    SECTION("Good query after bad query") {
8✔
1903
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
1904
            auto subs = create_subscription(realm, "class_TopLevel", "non_queryable_field", [](auto q, auto c) {
2✔
1905
                return q.equal(c, "bar");
2✔
1906
            });
2✔
1907
            auto sub_res = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1908
            check_status(sub_res);
2✔
1909

1910
            CHECK(realm->get_active_subscription_set().version() == 0);
2!
1911
            CHECK(realm->get_latest_subscription_set().version() == 1);
2!
1912

1913
            subs = create_subscription(realm, "class_TopLevel", "queryable_str_field", [](auto q, auto c) {
2✔
1914
                return q.equal(c, "foo");
2✔
1915
            });
2✔
1916
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
1917

1918
            CHECK(realm->get_active_subscription_set().version() == 2);
2!
1919
            CHECK(realm->get_latest_subscription_set().version() == 2);
2!
1920
        });
2✔
1921
    }
2✔
1922

1923
    SECTION("Bad query after bad query") {
8✔
1924
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
1925
            auto sync_session = realm->sync_session();
2✔
1926
            sync_session->pause();
2✔
1927

1928
            auto subs = create_subscription(realm, "class_TopLevel", "non_queryable_field", [](auto q, auto c) {
2✔
1929
                return q.equal(c, "bar");
2✔
1930
            });
2✔
1931
            auto subs2 = create_subscription(realm, "class_TopLevel", "non_queryable_field", [](auto q, auto c) {
2✔
1932
                return q.equal(c, "bar");
2✔
1933
            });
2✔
1934

1935
            sync_session->resume();
2✔
1936

1937
            auto sub_res = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1938
            auto sub_res2 =
2✔
1939
                subs2.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1940

1941
            check_status(sub_res);
2✔
1942
            check_status(sub_res2);
2✔
1943

1944
            CHECK(realm->get_active_subscription_set().version() == 0);
2!
1945
            CHECK(realm->get_latest_subscription_set().version() == 2);
2!
1946
        });
2✔
1947
    }
2✔
1948

1949
    // Test for issue #6839, where wait for download after committing a new subscription and then
1950
    // wait for the subscription complete notification was leading to a garbage reason value in the
1951
    // status provided to the subscription complete callback.
1952
    SECTION("Download during bad query") {
8✔
1953
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
1954
            // Wait for steady state before committing the new subscription
1955
            REQUIRE(!wait_for_download(*realm));
2!
1956

1957
            auto subs = create_subscription(realm, "class_TopLevel", "non_queryable_field", [](auto q, auto c) {
2✔
1958
                return q.equal(c, "bar");
2✔
1959
            });
2✔
1960
            // Wait for download is actually waiting for the subscription to be applied after it was committed
1961
            REQUIRE(!wait_for_download(*realm));
2!
1962
            // After subscription is complete or fails during wait for download, this function completes
1963
            // without blocking
1964
            auto result = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
1965
            // Verify error occurred
1966
            check_status(result);
2✔
1967
        });
2✔
1968
    }
2✔
1969

1970
    // Add new sections before this
1971
    SECTION("teardown") {
8✔
1972
        harness->app()->sync_manager()->wait_for_sessions_to_terminate();
2✔
1973
        harness.reset();
2✔
1974
    }
2✔
1975
}
8✔
1976

1977
#if REALM_ENABLE_GEOSPATIAL
1978
TEST_CASE("flx: geospatial", "[sync][flx][geospatial][baas]") {
6✔
1979
    static std::optional<FLXSyncTestHarness> harness;
6✔
1980
    if (!harness) {
6✔
1981
        Schema schema{
2✔
1982
            {"restaurant",
2✔
1983
             {
2✔
1984
                 {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
1985
                 {"queryable_str_field", PropertyType::String},
2✔
1986
                 {"location", PropertyType::Object | PropertyType::Nullable, "geoPointType"},
2✔
1987
                 {"array", PropertyType::Object | PropertyType::Array, "geoPointType"},
2✔
1988
             }},
2✔
1989
            {"geoPointType",
2✔
1990
             ObjectSchema::ObjectType::Embedded,
2✔
1991
             {
2✔
1992
                 {"type", PropertyType::String},
2✔
1993
                 {"coordinates", PropertyType::Double | PropertyType::Array},
2✔
1994
             }},
2✔
1995
        };
2✔
1996
        FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field", "location"}};
2✔
1997
        harness.emplace("flx_geospatial", server_schema);
2✔
1998
    }
2✔
1999

2000
    auto create_subscription = [](SharedRealm realm, StringData table_name, StringData column_name, auto make_query) {
18✔
2001
        auto table = realm->read_group().get_table(table_name);
18✔
2002
        auto queryable_field = table->get_column_key(column_name);
18✔
2003
        auto new_query = realm->get_active_subscription_set().make_mutable_copy();
18✔
2004
        new_query.insert_or_assign(make_query(Query(table), queryable_field));
18✔
2005
        return new_query.commit();
18✔
2006
    };
18✔
2007

2008
    SECTION("Server supports a basic geowithin FLX query") {
6✔
2009
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
2010
            const realm::AppSession& app_session = harness->session().app_session();
2✔
2011
            auto sync_service = app_session.admin_api.get_sync_service(app_session.server_app_id);
2✔
2012

2013
            AdminAPISession::ServiceConfig config =
2✔
2014
                app_session.admin_api.get_config(app_session.server_app_id, sync_service);
2✔
2015
            auto subs = create_subscription(realm, "class_restaurant", "location", [](Query q, ColKey c) {
2✔
2016
                GeoBox area{GeoPoint{0.2, 0.2}, GeoPoint{0.7, 0.7}};
2✔
2017
                Query query = q.get_table()->column<Link>(c).geo_within(area);
2✔
2018
                std::string ser = query.get_description();
2✔
2019
                return query;
2✔
2020
            });
2✔
2021
            auto sub_res = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
2022
            CHECK(sub_res.is_ok());
2!
2023
            CHECK(realm->get_active_subscription_set().version() == 1);
2!
2024
            CHECK(realm->get_latest_subscription_set().version() == 1);
2!
2025
        });
2✔
2026
    }
2✔
2027

2028
    SECTION("geospatial query consistency: local/server/FLX") {
6✔
2029
        harness->do_with_new_user([&](std::shared_ptr<SyncUser> user) {
2✔
2030
            SyncTestFile config(user, harness->schema(), SyncConfig::FLXSyncEnabled{});
2✔
2031
            auto error_pf = util::make_promise_future<SyncError>();
2✔
2032
            config.sync_config->error_handler =
2✔
2033
                [promise = std::make_shared<util::Promise<SyncError>>(std::move(error_pf.promise))](
2✔
2034
                    std::shared_ptr<SyncSession>, SyncError error) {
2✔
2035
                    promise->emplace_value(std::move(error));
2✔
2036
                };
2✔
2037

2038
            auto realm = Realm::get_shared_realm(config);
2✔
2039

2040
            auto subs = create_subscription(realm, "class_restaurant", "queryable_str_field", [](Query q, ColKey c) {
2✔
2041
                return q.equal(c, "synced");
2✔
2042
            });
2✔
2043
            auto make_polygon_filter = [&](const GeoPolygon& polygon) -> bson::BsonDocument {
20✔
2044
                bson::BsonArray inner{};
20✔
2045
                REALM_ASSERT_3(polygon.points.size(), ==, 1);
20✔
2046
                for (auto& point : polygon.points[0]) {
94✔
2047
                    inner.push_back(bson::BsonArray{point.longitude, point.latitude});
94✔
2048
                }
94✔
2049
                bson::BsonArray coords;
20✔
2050
                coords.push_back(inner);
20✔
2051
                bson::BsonDocument geo_bson{{{"type", "Polygon"}, {"coordinates", coords}}};
20✔
2052
                bson::BsonDocument filter{
20✔
2053
                    {"location", bson::BsonDocument{{"$geoWithin", bson::BsonDocument{{"$geometry", geo_bson}}}}}};
20✔
2054
                return filter;
20✔
2055
            };
20✔
2056
            auto make_circle_filter = [&](const GeoCircle& circle) -> bson::BsonDocument {
6✔
2057
                bson::BsonArray coords{circle.center.longitude, circle.center.latitude};
6✔
2058
                bson::BsonArray inner;
6✔
2059
                inner.push_back(coords);
6✔
2060
                inner.push_back(circle.radius_radians);
6✔
2061
                bson::BsonDocument filter{
6✔
2062
                    {"location", bson::BsonDocument{{"$geoWithin", bson::BsonDocument{{"$centerSphere", inner}}}}}};
6✔
2063
                return filter;
6✔
2064
            };
6✔
2065
            auto run_query_on_server = [&](const bson::BsonDocument& filter,
2✔
2066
                                           std::optional<std::string> expected_error = {}) -> size_t {
26✔
2067
                auto remote_client = harness->app()->current_user()->mongo_client("BackingDB");
26✔
2068
                auto db = remote_client.db(harness->session().app_session().config.mongo_dbname);
26✔
2069
                auto restaurant_collection = db["restaurant"];
26✔
2070
                bool processed = false;
26✔
2071
                constexpr int64_t limit = 1000;
26✔
2072
                size_t matches = 0;
26✔
2073
                restaurant_collection.count(filter, limit, [&](uint64_t count, util::Optional<AppError> error) {
26✔
2074
                    processed = true;
26✔
2075
                    if (error) {
26✔
2076
                        if (!expected_error) {
12✔
2077
                            util::format(std::cout, "query error: %1\n", error->reason());
×
2078
                            FAIL(error);
×
2079
                        }
×
2080
                        else {
12✔
2081
                            std::string reason = std::string(error->reason());
12✔
2082
                            std::transform(reason.begin(), reason.end(), reason.begin(), toLowerAscii);
12✔
2083
                            std::transform(expected_error->begin(), expected_error->end(), expected_error->begin(),
12✔
2084
                                           toLowerAscii);
12✔
2085
                            auto pos = reason.find(*expected_error);
12✔
2086
                            if (pos == std::string::npos) {
12✔
2087
                                util::format(std::cout, "mismatch error: '%1' and '%2'\n", reason, *expected_error);
×
2088
                                FAIL(reason);
×
2089
                            }
×
2090
                        }
12✔
2091
                    }
12✔
2092
                    matches = size_t(count);
26✔
2093
                });
26✔
2094
                REQUIRE(processed);
26!
2095
                return matches;
26✔
2096
            };
26✔
2097
            auto sub_res = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
2✔
2098
            CHECK(sub_res.is_ok());
2!
2099
            CHECK(realm->get_active_subscription_set().version() == 1);
2!
2100
            CHECK(realm->get_latest_subscription_set().version() == 1);
2!
2101

2102
            CppContext c(realm);
2✔
2103
            int64_t pk = 0;
2✔
2104
            auto add_point = [&](GeoPoint p) {
16✔
2105
                Object::create(
16✔
2106
                    c, realm, "restaurant",
16✔
2107
                    std::any(AnyDict{
16✔
2108
                        {"_id", ++pk},
16✔
2109
                        {"queryable_str_field", "synced"s},
16✔
2110
                        {"location", AnyDict{{"type", "Point"s},
16✔
2111
                                             {"coordinates", std::vector<std::any>{p.longitude, p.latitude}}}}}));
16✔
2112
            };
16✔
2113
            std::vector<GeoPoint> points = {
2✔
2114
                GeoPoint{-74.006, 40.712800000000001},            // New York city
2✔
2115
                GeoPoint{12.568300000000001, 55.676099999999998}, // Copenhagen
2✔
2116
                GeoPoint{12.082599999999999, 55.628},             // ragnarok, Roskilde
2✔
2117
                GeoPoint{-180.1, -90.1},                          // invalid
2✔
2118
                GeoPoint{0, 90},                                  // north pole
2✔
2119
                GeoPoint{-82.68193, 84.74653},                    // northern point that falls within a box later
2✔
2120
                GeoPoint{82.55243, 84.54981}, // another northern point, but on the other side of the pole
2✔
2121
                GeoPoint{2129, 89},           // invalid
2✔
2122
            };
2✔
2123
            constexpr size_t invalids_to_be_compensated = 2; // 4, 8
2✔
2124
            realm->begin_transaction();
2✔
2125
            for (auto& point : points) {
16✔
2126
                add_point(point);
16✔
2127
            }
16✔
2128
            realm->commit_transaction();
2✔
2129
            const auto& error = error_pf.future.get();
2✔
2130
            REQUIRE(!error.is_fatal);
2!
2131
            REQUIRE(error.status == ErrorCodes::SyncCompensatingWrite);
2!
2132
            REQUIRE(error.compensating_writes_info.size() == invalids_to_be_compensated);
2!
2133
            REQUIRE_THAT(error.compensating_writes_info[0].reason,
2✔
2134
                         Catch::Matchers::ContainsSubstring("in table \"restaurant\" will corrupt geojson data"));
2✔
2135
            REQUIRE_THAT(error.compensating_writes_info[1].reason,
2✔
2136
                         Catch::Matchers::ContainsSubstring("in table \"restaurant\" will corrupt geojson data"));
2✔
2137

2138
            {
2✔
2139
                auto table = realm->read_group().get_table("class_restaurant");
2✔
2140
                CHECK(table->size() == points.size());
2!
2141
                Obj obj = table->get_object_with_primary_key(Mixed{1});
2✔
2142
                REQUIRE(obj);
2!
2143
                Geospatial geo = obj.get<Geospatial>("location");
2✔
2144
                REQUIRE(geo.get_type_string() == "Point");
2!
2145
                REQUIRE(geo.get_type() == Geospatial::Type::Point);
2!
2146
                GeoPoint point = geo.get<GeoPoint>();
2✔
2147
                REQUIRE(point.longitude == points[0].longitude);
2!
2148
                REQUIRE(point.latitude == points[0].latitude);
2!
2149
                REQUIRE(!point.get_altitude());
2!
2150
                ColKey location_col = table->get_column_key("location");
2✔
2151
                auto run_query_locally = [&table, &location_col](Geospatial bounds) -> size_t {
26✔
2152
                    Query query = table->column<Link>(location_col).geo_within(Geospatial(bounds));
26✔
2153
                    return query.find_all().size();
26✔
2154
                };
26✔
2155
                auto run_query_as_flx = [&](Geospatial bounds) -> size_t {
14✔
2156
                    size_t num_objects = 0;
14✔
2157
                    harness->do_with_new_realm([&](SharedRealm realm) {
14✔
2158
                        auto subs =
14✔
2159
                            create_subscription(realm, "class_restaurant", "location", [&](Query q, ColKey c) {
14✔
2160
                                return q.get_table()->column<Link>(c).geo_within(Geospatial(bounds));
14✔
2161
                            });
14✔
2162
                        auto sub_res =
14✔
2163
                            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get_no_throw();
14✔
2164
                        CHECK(sub_res.is_ok());
14!
2165
                        CHECK(realm->get_active_subscription_set().version() == 1);
14!
2166
                        realm->refresh();
14✔
2167
                        num_objects = realm->get_class("restaurant").num_objects();
14✔
2168
                    });
14✔
2169
                    return num_objects;
14✔
2170
                };
14✔
2171

2172
                reset_utils::wait_for_num_objects_in_atlas(harness->app()->current_user(),
2✔
2173
                                                           harness->session().app_session(), "restaurant",
2✔
2174
                                                           points.size() - invalids_to_be_compensated);
2✔
2175

2176
                {
2✔
2177
                    GeoPolygon bounds{
2✔
2178
                        {{GeoPoint{-80, 40.7128}, GeoPoint{20, 60}, GeoPoint{20, 20}, GeoPoint{-80, 40.7128}}}};
2✔
2179
                    size_t local_matches = run_query_locally(bounds);
2✔
2180
                    size_t server_results = run_query_on_server(make_polygon_filter(bounds));
2✔
2181
                    size_t flx_results = run_query_as_flx(bounds);
2✔
2182
                    CHECK(flx_results == local_matches);
2!
2183
                    CHECK(server_results == local_matches);
2!
2184
                }
2✔
2185
                {
2✔
2186
                    GeoCircle circle{.5, GeoPoint{0, 90}};
2✔
2187
                    size_t local_matches = run_query_locally(circle);
2✔
2188
                    size_t server_results = run_query_on_server(make_circle_filter(circle));
2✔
2189
                    size_t flx_results = run_query_as_flx(circle);
2✔
2190
                    CHECK(server_results == local_matches);
2!
2191
                    CHECK(flx_results == local_matches);
2!
2192
                }
2✔
2193
                { // a ring with 3 points without a matching begin/end is an error
2✔
2194
                    GeoPolygon open_bounds{{{GeoPoint{-80, 40.7128}, GeoPoint{20, 60}, GeoPoint{20, 20}}}};
2✔
2195
                    CHECK_THROWS_WITH(run_query_locally(open_bounds),
2✔
2196
                                      "Invalid region in GEOWITHIN query for parameter 'GeoPolygon({[-80, 40.7128], "
2✔
2197
                                      "[20, 60], [20, 20]})': 'Ring is not closed, first vertex 'GeoPoint([-80, "
2✔
2198
                                      "40.7128])' does not equal last vertex 'GeoPoint([20, 20])''");
2✔
2199
                    run_query_on_server(make_polygon_filter(open_bounds), "(BadValue) Loop is not closed");
2✔
2200
                }
2✔
2201
                {
2✔
2202
                    GeoCircle circle = GeoCircle::from_kms(10, GeoPoint{-180.1, -90.1});
2✔
2203
                    CHECK_THROWS_WITH(run_query_locally(circle),
2✔
2204
                                      "Invalid region in GEOWITHIN query for parameter 'GeoCircle([-180.1, -90.1], "
2✔
2205
                                      "0.00156787)': 'Longitude/latitude is out of bounds, lng: -180.1 lat: -90.1'");
2✔
2206
                    run_query_on_server(make_circle_filter(circle), "(BadValue) longitude/latitude is out of bounds");
2✔
2207
                }
2✔
2208
                {
2✔
2209
                    GeoCircle circle = GeoCircle::from_kms(-1, GeoPoint{0, 0});
2✔
2210
                    CHECK_THROWS_WITH(run_query_locally(circle),
2✔
2211
                                      "Invalid region in GEOWITHIN query for parameter 'GeoCircle([0, 0], "
2✔
2212
                                      "-0.000156787)': 'The radius of a circle must be a non-negative number'");
2✔
2213
                    run_query_on_server(make_circle_filter(circle),
2✔
2214
                                        "(BadValue) radius must be a non-negative number");
2✔
2215
                }
2✔
2216
                {
2✔
2217
                    // This box is from Gershøj to CPH airport. It includes CPH and Ragnarok but not NYC.
2218
                    std::vector<Geospatial> valid_box_variations = {
2✔
2219
                        GeoBox{GeoPoint{11.97575, 55.71601},
2✔
2220
                               GeoPoint{12.64773, 55.61211}}, // Gershøj, CPH Airport (Top Left, Bottom Right)
2✔
2221
                        GeoBox{GeoPoint{12.64773, 55.61211},
2✔
2222
                               GeoPoint{11.97575, 55.71601}}, // CPH Airport, Gershøj (Bottom Right, Top Left)
2✔
2223
                        GeoBox{GeoPoint{12.64773, 55.71601},
2✔
2224
                               GeoPoint{11.97575, 55.61211}}, // Upper Right, Bottom Left
2✔
2225
                        GeoBox{GeoPoint{11.97575, 55.61211},
2✔
2226
                               GeoPoint{12.64773, 55.71601}}, // Bottom Left, Upper Right
2✔
2227
                    };
2✔
2228
                    constexpr size_t expected_results = 2;
2✔
2229
                    for (auto& geo : valid_box_variations) {
8✔
2230
                        size_t local_matches = run_query_locally(geo);
8✔
2231
                        size_t server_matches =
8✔
2232
                            run_query_on_server(make_polygon_filter(geo.get<GeoBox>().to_polygon()));
8✔
2233
                        size_t flx_matches = run_query_as_flx(geo);
8✔
2234
                        CHECK(local_matches == expected_results);
8!
2235
                        CHECK(server_matches == expected_results);
8!
2236
                        CHECK(flx_matches == expected_results);
8!
2237
                    }
8✔
2238
                    std::vector<Geospatial> invalid_boxes = {
2✔
2239
                        GeoBox{GeoPoint{11.97575, 55.71601}, GeoPoint{11.97575, 55.71601}}, // same point twice
2✔
2240
                        GeoBox{GeoPoint{11.97575, 55.71601},
2✔
2241
                               GeoPoint{11.97575, 57.0}}, // two points on the same longitude
2✔
2242
                        GeoBox{GeoPoint{11.97575, 55.71601},
2✔
2243
                               GeoPoint{12, 55.71601}}, // two points on the same latitude
2✔
2244
                    };
2✔
2245
                    for (auto& geo : invalid_boxes) {
6✔
2246
                        REQUIRE_THROWS_CONTAINING(run_query_locally(geo),
6✔
2247
                                                  "Invalid region in GEOWITHIN query for parameter 'GeoPolygon");
6✔
2248
                        run_query_on_server(make_polygon_filter(geo.get<GeoBox>().to_polygon()),
6✔
2249
                                            "(BadValue) Loop must have at least 3 different vertices");
6✔
2250
                    }
6✔
2251
                }
2✔
2252
                { // a box region that wraps the north pole. It contains the north pole point
2✔
2253
                    // and two others, one each on distinct sides of the globe.
2254
                    constexpr double lat = 82.83799;
2✔
2255
                    Geospatial north_pole_box =
2✔
2256
                        GeoPolygon{{{GeoPoint{-78.33951, lat}, GeoPoint{-90.33951, lat}, GeoPoint{90.33951, lat},
2✔
2257
                                     GeoPoint{78.33951, lat}, GeoPoint{-78.33951, lat}}}};
2✔
2258
                    constexpr size_t num_matching_points = 3;
2✔
2259
                    size_t local_matches = run_query_locally(north_pole_box);
2✔
2260
                    size_t server_matches =
2✔
2261
                        run_query_on_server(make_polygon_filter(north_pole_box.get<GeoPolygon>()));
2✔
2262
                    size_t flx_matches = run_query_as_flx(north_pole_box);
2✔
2263
                    CHECK(local_matches == num_matching_points);
2!
2264
                    CHECK(server_matches == num_matching_points);
2!
2265
                    CHECK(flx_matches == num_matching_points);
2!
2266
                }
2✔
2267
            }
2✔
2268
        });
2✔
2269
    }
2✔
2270

2271
    // Add new sections before this
2272
    SECTION("teardown") {
6✔
2273
        harness->app()->sync_manager()->wait_for_sessions_to_terminate();
2✔
2274
        harness.reset();
2✔
2275
    }
2✔
2276
}
6✔
2277
#endif // REALM_ENABLE_GEOSPATIAL
2278

2279
TEST_CASE("flx: interrupted bootstrap restarts/recovers on reconnect", "[sync][flx][bootstrap][baas]") {
2✔
2280
    FLXSyncTestHarness harness("flx_bootstrap_reconnect", {g_large_array_schema, {"queryable_int_field"}});
2✔
2281

2282
    std::vector<ObjectId> obj_ids_at_end = fill_large_array_schema(harness);
2✔
2283
    SyncTestFile interrupted_realm_config(harness.app()->current_user(), harness.schema(),
2✔
2284
                                          SyncConfig::FLXSyncEnabled{});
2✔
2285

2286
    {
2✔
2287
        auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
2288
        Realm::Config config = interrupted_realm_config;
2✔
2289
        config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
2290
        auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
2291
        config.sync_config->on_sync_client_event_hook =
2✔
2292
            [promise = std::move(shared_promise), seen_version_one = false](std::weak_ptr<SyncSession> weak_session,
2✔
2293
                                                                            const SyncClientHookData& data) mutable {
34✔
2294
                if (data.event != SyncClientHookEvent::DownloadMessageReceived) {
34✔
2295
                    return SyncClientHookAction::NoAction;
26✔
2296
                }
26✔
2297

2298
                auto session = weak_session.lock();
8✔
2299
                if (!session) {
8✔
2300
                    return SyncClientHookAction::NoAction;
×
2301
                }
×
2302

2303
                // If we haven't seen at least one download message for query version 1, then do nothing yet.
2304
                if (data.query_version == 0 || (data.query_version == 1 && !std::exchange(seen_version_one, true))) {
8✔
2305
                    return SyncClientHookAction::NoAction;
6✔
2306
                }
6✔
2307

2308
                REQUIRE(data.query_version == 1);
2!
2309
                REQUIRE(data.batch_state == sync::DownloadBatchState::MoreToCome);
2!
2310
                auto latest_subs = session->get_flx_subscription_store()->get_latest();
2✔
2311
                REQUIRE(latest_subs.version() == 1);
2!
2312
                REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::Bootstrapping);
2!
2313

2314
                session->close();
2✔
2315
                promise->emplace_value();
2✔
2316

2317
                return SyncClientHookAction::TriggerReconnect;
2✔
2318
            };
2✔
2319

2320
        auto realm = Realm::get_shared_realm(config);
2✔
2321
        {
2✔
2322
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2323
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
2324
            mut_subs.insert_or_assign(Query(table));
2✔
2325
            mut_subs.commit();
2✔
2326
        }
2✔
2327

2328
        interrupted.get();
2✔
2329
        realm->sync_session()->shutdown_and_wait();
2✔
2330
    }
2✔
2331

2332
    // Verify that the file was fully closed
2333
    REQUIRE(DB::call_with_lock(interrupted_realm_config.path, [](auto&) {}));
2!
2334

2335
    {
2✔
2336
        DBOptions options;
2✔
2337
        options.encryption_key = test_util::crypt_key();
2✔
2338
        auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
2339
        auto sub_store = sync::SubscriptionStore::create(realm);
2✔
2340
        auto version_info = sub_store->get_version_info();
2✔
2341
        REQUIRE(version_info.active == 0);
2!
2342
        REQUIRE(version_info.latest == 1);
2!
2343
        auto latest_subs = sub_store->get_latest();
2✔
2344
        REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::Bootstrapping);
2!
2345
        REQUIRE(latest_subs.size() == 1);
2!
2346
        REQUIRE(latest_subs.at(0).object_class_name == "TopLevel");
2!
2347
    }
2✔
2348

2349
    auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
2350
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
2351
    realm->get_latest_subscription_set().get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2352
    wait_for_advance(*realm);
2✔
2353
    REQUIRE(table->size() == obj_ids_at_end.size());
2!
2354
    for (auto& id : obj_ids_at_end) {
10✔
2355
        REQUIRE(table->find_primary_key(Mixed{id}));
10!
2356
    }
10✔
2357

2358
    auto active_subs = realm->get_active_subscription_set();
2✔
2359
    auto latest_subs = realm->get_latest_subscription_set();
2✔
2360
    REQUIRE(active_subs.version() == latest_subs.version());
2!
2361
    REQUIRE(active_subs.version() == int64_t(1));
2!
2362
}
2✔
2363

2364
TEST_CASE("flx: dev mode uploads schema before query change", "[sync][flx][query][baas]") {
2✔
2365
    FLXSyncTestHarness::ServerSchema server_schema;
2✔
2366
    auto default_schema = FLXSyncTestHarness::default_server_schema();
2✔
2367
    server_schema.queryable_fields = default_schema.queryable_fields;
2✔
2368
    server_schema.dev_mode_enabled = true;
2✔
2369
    server_schema.schema = Schema{};
2✔
2370

2371
    FLXSyncTestHarness harness("flx_dev_mode", server_schema);
2✔
2372
    auto foo_obj_id = ObjectId::gen();
2✔
2373
    auto bar_obj_id = ObjectId::gen();
2✔
2374
    harness.do_with_new_realm(
2✔
2375
        [&](SharedRealm realm) {
2✔
2376
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
2377
            // auto queryable_str_field = table->get_column_key("queryable_str_field");
2378
            // auto queryable_int_field = table->get_column_key("queryable_int_field");
2379
            auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2380
            new_query.insert_or_assign(Query(table));
2✔
2381
            new_query.commit();
2✔
2382

2383
            CppContext c(realm);
2✔
2384
            realm->begin_transaction();
2✔
2385
            Object::create(c, realm, "TopLevel",
2✔
2386
                           std::any(AnyDict{{"_id", foo_obj_id},
2✔
2387
                                            {"queryable_str_field", "foo"s},
2✔
2388
                                            {"queryable_int_field", static_cast<int64_t>(5)},
2✔
2389
                                            {"non_queryable_field", "non queryable 1"s}}));
2✔
2390
            Object::create(c, realm, "TopLevel",
2✔
2391
                           std::any(AnyDict{{"_id", bar_obj_id},
2✔
2392
                                            {"queryable_str_field", "bar"s},
2✔
2393
                                            {"queryable_int_field", static_cast<int64_t>(10)},
2✔
2394
                                            {"non_queryable_field", "non queryable 2"s}}));
2✔
2395
            realm->commit_transaction();
2✔
2396

2397
            wait_for_upload(*realm);
2✔
2398
        },
2✔
2399
        default_schema.schema);
2✔
2400

2401
    harness.do_with_new_realm(
2✔
2402
        [&](SharedRealm realm) {
2✔
2403
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
2404
            auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
2405
            auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2406
            new_query.insert_or_assign(Query(table).greater_equal(queryable_int_field, int64_t(5)));
2✔
2407
            auto subs = new_query.commit();
2✔
2408
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2409
            wait_for_download(*realm);
2✔
2410
            Results results(realm, table);
2✔
2411

2412
            realm->refresh();
2✔
2413
            CHECK(results.size() == 2);
2!
2414
            CHECK(table->get_object_with_primary_key({foo_obj_id}).is_valid());
2!
2415
            CHECK(table->get_object_with_primary_key({bar_obj_id}).is_valid());
2!
2416
        },
2✔
2417
        default_schema.schema);
2✔
2418
}
2✔
2419

2420
// This is a test case for the server's fix for RCORE-969
2421
TEST_CASE("flx: change-of-query history divergence", "[sync][flx][query][baas]") {
2✔
2422
    FLXSyncTestHarness harness("flx_coq_divergence");
2✔
2423

2424
    // first we create an object on the server and upload it.
2425
    auto foo_obj_id = ObjectId::gen();
2✔
2426
    harness.load_initial_data([&](SharedRealm realm) {
2✔
2427
        CppContext c(realm);
2✔
2428
        Object::create(c, realm, "TopLevel",
2✔
2429
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
2430
                                        {"queryable_str_field", "foo"s},
2✔
2431
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
2432
                                        {"non_queryable_field", "created as initial data seed"s}}));
2✔
2433
    });
2✔
2434

2435
    // Now create another realm and wait for it to be fully synchronized with bootstrap version zero. i.e.
2436
    // our progress counters should be past the history entry containing the object created above.
2437
    auto test_file_config = harness.make_test_file();
2✔
2438
    auto realm = Realm::get_shared_realm(test_file_config);
2✔
2439
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
2440
    auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
2441

2442
    realm->get_latest_subscription_set().get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2443
    wait_for_upload(*realm);
2✔
2444
    wait_for_download(*realm);
2✔
2445

2446
    // Now disconnect the sync session
2447
    realm->sync_session()->pause();
2✔
2448

2449
    // And move the "foo" object created above into view and create a different diverging copy of it locally.
2450
    auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2451
    mut_subs.insert_or_assign(Query(table).equal(queryable_str_field, "foo"));
2✔
2452
    auto subs = mut_subs.commit();
2✔
2453

2454
    realm->begin_transaction();
2✔
2455
    CppContext c(realm);
2✔
2456
    Object::create(c, realm, "TopLevel",
2✔
2457
                   std::any(AnyDict{{"_id", foo_obj_id},
2✔
2458
                                    {"queryable_str_field", "foo"s},
2✔
2459
                                    {"queryable_int_field", static_cast<int64_t>(10)},
2✔
2460
                                    {"non_queryable_field", "created locally"s}}));
2✔
2461
    realm->commit_transaction();
2✔
2462

2463
    // Reconnect the sync session and wait for the subscription that moved "foo" into view to be fully synchronized.
2464
    realm->sync_session()->resume();
2✔
2465
    wait_for_upload(*realm);
2✔
2466
    wait_for_download(*realm);
2✔
2467
    subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2468

2469
    wait_for_advance(*realm);
2✔
2470

2471
    // The bootstrap should have erase/re-created our object and we should have the version from the server
2472
    // locally.
2473
    auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{foo_obj_id});
2✔
2474
    REQUIRE(obj.get_obj().get<int64_t>("queryable_int_field") == 5);
2!
2475
    REQUIRE(obj.get_obj().get<StringData>("non_queryable_field") == "created as initial data seed");
2!
2476

2477
    // Likewise, if we create a new realm and download all the objects, we should see the initial server version
2478
    // in the new realm rather than the "created locally" one.
2479
    harness.load_initial_data([&](SharedRealm realm) {
2✔
2480
        CppContext c(realm);
2✔
2481

2482
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{foo_obj_id});
2✔
2483
        REQUIRE(obj.get_obj().get<int64_t>("queryable_int_field") == 5);
2!
2484
        REQUIRE(obj.get_obj().get<StringData>("non_queryable_field") == "created as initial data seed");
2!
2485
    });
2✔
2486
}
2✔
2487

2488
TEST_CASE("flx: writes work offline", "[sync][flx][baas]") {
2✔
2489
    FLXSyncTestHarness harness("flx_offline_writes");
2✔
2490

2491
    harness.do_with_new_realm([&](SharedRealm realm) {
2✔
2492
        auto sync_session = realm->sync_session();
2✔
2493
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
2494
        auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
2495
        auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
2496
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2497
        new_query.insert_or_assign(Query(table));
2✔
2498
        new_query.commit();
2✔
2499

2500
        auto foo_obj_id = ObjectId::gen();
2✔
2501
        auto bar_obj_id = ObjectId::gen();
2✔
2502

2503
        CppContext c(realm);
2✔
2504
        realm->begin_transaction();
2✔
2505
        Object::create(c, realm, "TopLevel",
2✔
2506
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
2507
                                        {"queryable_str_field", "foo"s},
2✔
2508
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
2509
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
2510
        Object::create(c, realm, "TopLevel",
2✔
2511
                       std::any(AnyDict{{"_id", bar_obj_id},
2✔
2512
                                        {"queryable_str_field", "bar"s},
2✔
2513
                                        {"queryable_int_field", static_cast<int64_t>(10)},
2✔
2514
                                        {"non_queryable_field", "non queryable 2"s}}));
2✔
2515
        realm->commit_transaction();
2✔
2516

2517
        wait_for_upload(*realm);
2✔
2518
        wait_for_download(*realm);
2✔
2519
        sync_session->pause();
2✔
2520

2521
        // Make it so the subscriptions only match the "foo" object
2522
        {
2✔
2523
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2524
            mut_subs.clear();
2✔
2525
            mut_subs.insert_or_assign(Query(table).equal(queryable_str_field, "foo"));
2✔
2526
            mut_subs.commit();
2✔
2527
        }
2✔
2528

2529
        // Make foo so that it will match the next subscription update. This checks whether you can do
2530
        // multiple subscription set updates offline and that the last one eventually takes effect when
2531
        // you come back online and fully synchronize.
2532
        {
2✔
2533
            Results results(realm, table);
2✔
2534
            realm->begin_transaction();
2✔
2535
            auto foo_obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
2536
            foo_obj.set<int64_t>(queryable_int_field, 15);
2✔
2537
            realm->commit_transaction();
2✔
2538
        }
2✔
2539

2540
        // Update our subscriptions so that both foo/bar will be included
2541
        {
2✔
2542
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2543
            mut_subs.clear();
2✔
2544
            mut_subs.insert_or_assign(Query(table).greater_equal(queryable_int_field, static_cast<int64_t>(10)));
2✔
2545
            mut_subs.commit();
2✔
2546
        }
2✔
2547

2548
        // Make foo out of view for the current subscription.
2549
        {
2✔
2550
            Results results(realm, table);
2✔
2551
            realm->begin_transaction();
2✔
2552
            auto foo_obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
2553
            foo_obj.set<int64_t>(queryable_int_field, 0);
2✔
2554
            realm->commit_transaction();
2✔
2555
        }
2✔
2556

2557
        sync_session->resume();
2✔
2558
        wait_for_upload(*realm);
2✔
2559
        wait_for_download(*realm);
2✔
2560

2561
        realm->refresh();
2✔
2562
        Results results(realm, table);
2✔
2563
        CHECK(results.size() == 1);
2!
2564
        CHECK(table->get_object_with_primary_key({bar_obj_id}).is_valid());
2!
2565
    });
2✔
2566
}
2✔
2567

2568
TEST_CASE("flx: writes work without waiting for sync", "[sync][flx][baas]") {
2✔
2569
    FLXSyncTestHarness harness("flx_offline_writes");
2✔
2570

2571
    harness.do_with_new_realm([&](SharedRealm realm) {
2✔
2572
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
2573
        auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
2574
        auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
2575
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2576
        new_query.insert_or_assign(Query(table));
2✔
2577
        new_query.commit();
2✔
2578

2579
        auto foo_obj_id = ObjectId::gen();
2✔
2580
        auto bar_obj_id = ObjectId::gen();
2✔
2581

2582
        CppContext c(realm);
2✔
2583
        realm->begin_transaction();
2✔
2584
        Object::create(c, realm, "TopLevel",
2✔
2585
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
2586
                                        {"queryable_str_field", "foo"s},
2✔
2587
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
2588
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
2589
        Object::create(c, realm, "TopLevel",
2✔
2590
                       std::any(AnyDict{{"_id", bar_obj_id},
2✔
2591
                                        {"queryable_str_field", "bar"s},
2✔
2592
                                        {"queryable_int_field", static_cast<int64_t>(10)},
2✔
2593
                                        {"non_queryable_field", "non queryable 2"s}}));
2✔
2594
        realm->commit_transaction();
2✔
2595

2596
        wait_for_upload(*realm);
2✔
2597

2598
        // Make it so the subscriptions only match the "foo" object
2599
        {
2✔
2600
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2601
            mut_subs.clear();
2✔
2602
            mut_subs.insert_or_assign(Query(table).equal(queryable_str_field, "foo"));
2✔
2603
            mut_subs.commit();
2✔
2604
        }
2✔
2605

2606
        // Make foo so that it will match the next subscription update. This checks whether you can do
2607
        // multiple subscription set updates without waiting and that the last one eventually takes effect when
2608
        // you fully synchronize.
2609
        {
2✔
2610
            Results results(realm, table);
2✔
2611
            realm->begin_transaction();
2✔
2612
            auto foo_obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
2613
            foo_obj.set<int64_t>(queryable_int_field, 15);
2✔
2614
            realm->commit_transaction();
2✔
2615
        }
2✔
2616

2617
        // Update our subscriptions so that both foo/bar will be included
2618
        {
2✔
2619
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2620
            mut_subs.clear();
2✔
2621
            mut_subs.insert_or_assign(Query(table).greater_equal(queryable_int_field, static_cast<int64_t>(10)));
2✔
2622
            mut_subs.commit();
2✔
2623
        }
2✔
2624

2625
        // Make foo out-of-view for the current subscription.
2626
        {
2✔
2627
            Results results(realm, table);
2✔
2628
            realm->begin_transaction();
2✔
2629
            auto foo_obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
2630
            foo_obj.set<int64_t>(queryable_int_field, 0);
2✔
2631
            realm->commit_transaction();
2✔
2632
        }
2✔
2633

2634
        wait_for_upload(*realm);
2✔
2635
        wait_for_download(*realm);
2✔
2636

2637
        realm->refresh();
2✔
2638
        Results results(realm, table);
2✔
2639
        CHECK(results.size() == 1);
2!
2640
        Obj obj = results.get(0);
2✔
2641
        CHECK(obj.get_primary_key().get_object_id() == bar_obj_id);
2!
2642
        CHECK(table->get_object_with_primary_key({bar_obj_id}).is_valid());
2!
2643
    });
2✔
2644
}
2✔
2645

2646
TEST_CASE("flx: verify websocket protocol number and prefixes", "[sync][protocol]") {
2✔
2647
    // Update the expected value whenever the protocol version is updated - this ensures
2648
    // that the current protocol version does not change unexpectedly.
2649
    REQUIRE(13 == sync::get_current_protocol_version());
2✔
2650
    // This was updated in Protocol V8 to use '#' instead of '/' to support the Web SDK
2651
    REQUIRE("com.mongodb.realm-sync#" == sync::get_pbs_websocket_protocol_prefix());
2✔
2652
    REQUIRE("com.mongodb.realm-query-sync#" == sync::get_flx_websocket_protocol_prefix());
2✔
2653
}
2✔
2654

2655
// TODO: remote-baas: This test fails consistently with Windows remote baas server - to be fixed in RCORE-1674
2656
#ifndef _WIN32
2657
TEST_CASE("flx: subscriptions persist after closing/reopening", "[sync][flx][baas]") {
2✔
2658
    FLXSyncTestHarness harness("flx_bad_query");
2✔
2659
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
2660

2661
    {
2✔
2662
        auto orig_realm = Realm::get_shared_realm(config);
2✔
2663
        auto mut_subs = orig_realm->get_latest_subscription_set().make_mutable_copy();
2✔
2664
        mut_subs.insert_or_assign(Query(orig_realm->read_group().get_table("class_TopLevel")));
2✔
2665
        mut_subs.commit();
2✔
2666
        orig_realm->close();
2✔
2667
    }
2✔
2668

2669
    {
2✔
2670
        auto new_realm = Realm::get_shared_realm(config);
2✔
2671
        auto latest_subs = new_realm->get_latest_subscription_set();
2✔
2672
        CHECK(latest_subs.size() == 1);
2!
2673
        latest_subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
2674
    }
2✔
2675
}
2✔
2676
#endif
2677

2678
TEST_CASE("flx: no subscription store created for PBS app", "[sync][flx][baas]") {
2✔
2679
    auto server_app_config = minimal_app_config("flx_connect_as_pbs", g_minimal_schema);
2✔
2680
    TestAppSession session(create_app(server_app_config));
2✔
2681
    SyncTestFile config(session.app()->current_user(), bson::Bson{}, g_minimal_schema);
2✔
2682

2683
    auto realm = Realm::get_shared_realm(config);
2✔
2684
    CHECK(!wait_for_download(*realm));
2!
2685
    CHECK(!wait_for_upload(*realm));
2!
2686

2687
    CHECK(!realm->sync_session()->get_flx_subscription_store());
2!
2688

2689
    CHECK_THROWS_AS(realm->get_active_subscription_set(), IllegalOperation);
2✔
2690
    CHECK_THROWS_AS(realm->get_latest_subscription_set(), IllegalOperation);
2✔
2691
}
2✔
2692

2693
TEST_CASE("flx: connect to FLX as PBS returns an error", "[sync][flx][baas]") {
2✔
2694
    FLXSyncTestHarness harness("connect_to_flx_as_pbs");
2✔
2695
    SyncTestFile config(harness.app()->current_user(), bson::Bson{}, harness.schema());
2✔
2696
    std::mutex sync_error_mutex;
2✔
2697
    util::Optional<SyncError> sync_error;
2✔
2698
    config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
2✔
2699
        std::lock_guard<std::mutex> lk(sync_error_mutex);
2✔
2700
        sync_error = std::move(error);
2✔
2701
    };
2✔
2702
    auto realm = Realm::get_shared_realm(config);
2✔
2703
    timed_wait_for([&] {
3,971✔
2704
        std::lock_guard<std::mutex> lk(sync_error_mutex);
3,971✔
2705
        return static_cast<bool>(sync_error);
3,971✔
2706
    });
3,971✔
2707

2708
    CHECK(sync_error->status == ErrorCodes::WrongSyncType);
2!
2709
    CHECK(sync_error->server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
2710
}
2✔
2711

2712
TEST_CASE("flx: connect to FLX with partition value returns an error", "[sync][flx][protocol][baas]") {
2✔
2713
    FLXSyncTestHarness harness("connect_to_flx_as_pbs");
2✔
2714
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
2715
    config.sync_config->partition_value = "\"foobar\"";
2✔
2716

2717
    REQUIRE_EXCEPTION(Realm::get_shared_realm(config), IllegalCombination,
2✔
2718
                      "Cannot specify a partition value when flexible sync is enabled");
2✔
2719
}
2✔
2720

2721
TEST_CASE("flx: connect to PBS as FLX returns an error", "[sync][flx][protocol][baas]") {
2✔
2722
    auto server_app_config = minimal_app_config("flx_connect_as_pbs", g_minimal_schema);
2✔
2723
    TestAppSession session(create_app(server_app_config));
2✔
2724
    auto app = session.app();
2✔
2725
    auto user = app->current_user();
2✔
2726

2727
    SyncTestFile config(user, g_minimal_schema, SyncConfig::FLXSyncEnabled{});
2✔
2728

2729
    std::mutex sync_error_mutex;
2✔
2730
    util::Optional<SyncError> sync_error;
2✔
2731
    config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
2✔
2732
        std::lock_guard lk(sync_error_mutex);
2✔
2733
        sync_error = std::move(error);
2✔
2734
    };
2✔
2735
    auto realm = Realm::get_shared_realm(config);
2✔
2736
    timed_wait_for([&] {
27,992✔
2737
        std::lock_guard lk(sync_error_mutex);
27,992✔
2738
        return static_cast<bool>(sync_error);
27,992✔
2739
    });
27,992✔
2740

2741
    CHECK(sync_error->status == ErrorCodes::WrongSyncType);
2!
2742
    CHECK(sync_error->server_requests_action == sync::ProtocolErrorInfo::Action::ApplicationBug);
2!
2743
}
2✔
2744

2745
TEST_CASE("flx: commit subscription while refreshing the access token", "[sync][flx][token][baas]") {
2✔
2746
    auto transport = std::make_shared<HookedTransport<>>();
2✔
2747
    FLXSyncTestHarness harness("flx_wait_access_token2", FLXSyncTestHarness::default_server_schema(), transport);
2✔
2748
    auto app = harness.app();
2✔
2749
    std::shared_ptr<User> user = app->current_user();
2✔
2750
    REQUIRE(user);
2!
2751
    REQUIRE(!user->access_token_refresh_required());
2!
2752
    // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client.
2753
    std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
2✔
2754
    using namespace std::chrono_literals;
2✔
2755
    auto expires = std::chrono::system_clock::to_time_t(now - 30s);
2✔
2756
    user->update_data_for_testing([&](UserData& data) {
2✔
2757
        data.access_token = RealmJWT(encode_fake_jwt("fake_access_token", expires));
2✔
2758
    });
2✔
2759
    REQUIRE(user->access_token_refresh_required());
2!
2760

2761
    bool seen_waiting_for_access_token = false;
2✔
2762
    // Commit a subcription set while there is no sync session.
2763
    // A session is created when the access token is refreshed.
2764
    transport->request_hook = [&](const Request&) {
2✔
2765
        auto user = app->current_user();
2✔
2766
        REQUIRE(user);
2!
2767
        for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) {
2✔
2768
            if (session->state() == SyncSession::State::WaitingForAccessToken) {
2✔
2769
                REQUIRE(!seen_waiting_for_access_token);
2!
2770
                seen_waiting_for_access_token = true;
2✔
2771

2772
                auto store = session->get_flx_subscription_store();
2✔
2773
                REQUIRE(store);
2!
2774
                auto mut_subs = store->get_latest().make_mutable_copy();
2✔
2775
                mut_subs.commit();
2✔
2776
            }
2✔
2777
        }
2✔
2778
        return std::nullopt;
2✔
2779
    };
2✔
2780
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
2781
    // This triggers the token refresh.
2782
    auto r = Realm::get_shared_realm(config);
2✔
2783
    REQUIRE(seen_waiting_for_access_token);
2!
2784
}
2✔
2785

2786
TEST_CASE("flx: bootstrap batching prevents orphan documents", "[sync][flx][bootstrap][baas]") {
8✔
2787
    struct NovelException : public std::exception {
8✔
2788
        const char* what() const noexcept override
8✔
2789
        {
8✔
2790
            return "Oh no, a really weird exception happened!";
2✔
2791
        }
2✔
2792
    };
8✔
2793

2794
    FLXSyncTestHarness harness("flx_bootstrap_batching", {g_large_array_schema, {"queryable_int_field"}});
8✔
2795

2796
    std::vector<ObjectId> obj_ids_at_end = fill_large_array_schema(harness);
8✔
2797
    SyncTestFile interrupted_realm_config(harness.app()->current_user(), harness.schema(),
8✔
2798
                                          SyncConfig::FLXSyncEnabled{});
8✔
2799

2800
    auto check_interrupted_state = [&](const DBRef& realm) {
8✔
2801
        auto tr = realm->start_read();
8✔
2802
        auto top_level = tr->get_table("class_TopLevel");
8✔
2803
        REQUIRE(top_level);
8!
2804
        REQUIRE(top_level->is_empty());
8!
2805

2806
        auto sub_store = sync::SubscriptionStore::create(realm);
8✔
2807
        auto version_info = sub_store->get_version_info();
8✔
2808
        REQUIRE(version_info.latest == 1);
8!
2809
        REQUIRE(version_info.active == 0);
8!
2810
        auto latest_subs = sub_store->get_latest();
8✔
2811
        REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::Bootstrapping);
8!
2812
        REQUIRE(latest_subs.size() == 1);
8!
2813
        REQUIRE(latest_subs.at(0).object_class_name == "TopLevel");
8!
2814
    };
8✔
2815

2816
    auto mutate_realm = [&] {
8✔
2817
        harness.load_initial_data([&](SharedRealm realm) {
4✔
2818
            auto table = realm->read_group().get_table("class_TopLevel");
4✔
2819
            Results res(realm, Query(table).greater(table->get_column_key("queryable_int_field"), int64_t(10)));
4✔
2820
            REQUIRE(res.size() == 2);
4!
2821
            res.clear();
4✔
2822
        });
4✔
2823
    };
4✔
2824

2825
    SECTION("unknown exception occurs during bootstrap application on session startup") {
8✔
2826
        {
2✔
2827
            auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
2828
            Realm::Config config = interrupted_realm_config;
2✔
2829
            config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
2830
            auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
2831
            config.sync_config->on_sync_client_event_hook =
2✔
2832
                [promise = std::move(shared_promise)](std::weak_ptr<SyncSession> weak_session,
2✔
2833
                                                      const SyncClientHookData& data) mutable {
62✔
2834
                    if (data.event != SyncClientHookEvent::BootstrapMessageProcessed) {
62✔
2835
                        return SyncClientHookAction::NoAction;
48✔
2836
                    }
48✔
2837
                    auto session = weak_session.lock();
14✔
2838
                    if (!session) {
14✔
2839
                        return SyncClientHookAction::NoAction;
×
2840
                    }
×
2841

2842
                    if (data.query_version == 1 && data.batch_state == sync::DownloadBatchState::LastInBatch) {
14✔
2843
                        session->close();
2✔
2844
                        promise->emplace_value();
2✔
2845
                        return SyncClientHookAction::EarlyReturn;
2✔
2846
                    }
2✔
2847
                    return SyncClientHookAction::NoAction;
12✔
2848
                };
14✔
2849
            auto realm = Realm::get_shared_realm(config);
2✔
2850
            {
2✔
2851
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2852
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
2853
                mut_subs.insert_or_assign(Query(table));
2✔
2854
                mut_subs.commit();
2✔
2855
            }
2✔
2856

2857
            interrupted.get();
2✔
2858
            realm->sync_session()->shutdown_and_wait();
2✔
2859
            realm->close();
2✔
2860
        }
2✔
2861

2862
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
2863

2864
        // Open up the realm without the sync client attached and verify that the realm got interrupted in the state
2865
        // we expected it to be in.
2866
        {
2✔
2867
            DBOptions options;
2✔
2868
            options.encryption_key = test_util::crypt_key();
2✔
2869
            auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
2870
            auto logger = util::Logger::get_default_logger();
2✔
2871
            sync::PendingBootstrapStore bootstrap_store(realm, *logger);
2✔
2872
            REQUIRE(bootstrap_store.has_pending());
2!
2873
            auto pending_batch = bootstrap_store.peek_pending(1024 * 1024 * 16);
2✔
2874
            REQUIRE(pending_batch.query_version == 1);
2!
2875
            REQUIRE(pending_batch.progress);
2!
2876

2877
            check_interrupted_state(realm);
2✔
2878
        }
2✔
2879

2880
        auto error_pf = util::make_promise_future<SyncError>();
2✔
2881
        interrupted_realm_config.sync_config->error_handler =
2✔
2882
            [promise = std::make_shared<util::Promise<SyncError>>(std::move(error_pf.promise))](
2✔
2883
                std::shared_ptr<SyncSession>, SyncError error) {
2✔
2884
                promise->emplace_value(std::move(error));
2✔
2885
            };
2✔
2886

2887
        interrupted_realm_config.sync_config->on_sync_client_event_hook =
2✔
2888
            [&, download_message_received = false](std::weak_ptr<SyncSession>,
2✔
2889
                                                   const SyncClientHookData& data) mutable {
6✔
2890
                if (data.event == SyncClientHookEvent::DownloadMessageReceived) {
6✔
2891
                    download_message_received = true;
×
2892
                }
×
2893
                if (data.event != SyncClientHookEvent::BootstrapBatchAboutToProcess) {
6✔
2894
                    return SyncClientHookAction::NoAction;
4✔
2895
                }
4✔
2896

2897
                REQUIRE(!download_message_received);
2!
2898
                throw NovelException{};
2✔
2899
                return SyncClientHookAction::NoAction;
×
2900
            };
2✔
2901

2902
        auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
2903
        const auto& error = error_pf.future.get();
2✔
2904
        REQUIRE(!error.is_fatal);
2!
2905
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::Warning);
2!
2906
        REQUIRE(error.status == ErrorCodes::UnknownError);
2!
2907
        REQUIRE_THAT(error.status.reason(),
2✔
2908
                     Catch::Matchers::ContainsSubstring("Oh no, a really weird exception happened!"));
2✔
2909
    }
2✔
2910

2911
    SECTION("exception occurs during bootstrap application") {
8✔
2912
        Status error_status(ErrorCodes::OutOfMemory, "no more memory!");
2✔
2913
        {
2✔
2914
            auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
2915
            Realm::Config config = interrupted_realm_config;
2✔
2916
            config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
2917
            config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_session,
2✔
2918
                                                                const SyncClientHookData& data) mutable {
62✔
2919
                if (data.event != SyncClientHookEvent::BootstrapBatchAboutToProcess) {
62✔
2920
                    return SyncClientHookAction::NoAction;
58✔
2921
                }
58✔
2922
                auto session = weak_session.lock();
4✔
2923
                if (!session) {
4✔
2924
                    return SyncClientHookAction::NoAction;
×
2925
                }
×
2926

2927
                if (data.query_version == 1 && data.batch_state == sync::DownloadBatchState::MoreToCome) {
4✔
2928
                    throw sync::IntegrationException(error_status);
2✔
2929
                }
2✔
2930
                return SyncClientHookAction::NoAction;
2✔
2931
            };
4✔
2932
            auto error_pf = util::make_promise_future<SyncError>();
2✔
2933
            config.sync_config->error_handler =
2✔
2934
                [promise = std::make_shared<util::Promise<SyncError>>(std::move(error_pf.promise))](
2✔
2935
                    std::shared_ptr<SyncSession>, SyncError error) {
2✔
2936
                    promise->emplace_value(std::move(error));
2✔
2937
                };
2✔
2938

2939

2940
            auto realm = Realm::get_shared_realm(config);
2✔
2941
            {
2✔
2942
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
2943
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
2944
                mut_subs.insert_or_assign(Query(table));
2✔
2945
                mut_subs.commit();
2✔
2946
            }
2✔
2947

2948
            auto error = error_pf.future.get();
2✔
2949
            REQUIRE(error.status.reason() == error_status.reason());
2!
2950
            REQUIRE(error.status == error_status);
2!
2951
            realm->sync_session()->shutdown_and_wait();
2✔
2952
            realm->close();
2✔
2953
        }
2✔
2954

2955
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
2956

2957
        // Open up the realm without the sync client attached and verify that the realm got interrupted in the state
2958
        // we expected it to be in.
2959
        {
2✔
2960
            DBOptions options;
2✔
2961
            options.encryption_key = test_util::crypt_key();
2✔
2962
            auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
2963
            util::StderrLogger logger;
2✔
2964
            sync::PendingBootstrapStore bootstrap_store(realm, logger);
2✔
2965
            REQUIRE(bootstrap_store.has_pending());
2!
2966
            auto pending_batch = bootstrap_store.peek_pending(1024 * 1024 * 16);
2✔
2967
            REQUIRE(pending_batch.query_version == 1);
2!
2968
            REQUIRE(pending_batch.progress);
2!
2969

2970
            check_interrupted_state(realm);
2✔
2971
        }
2✔
2972

2973
        auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
2974
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
2975
        realm->get_latest_subscription_set()
2✔
2976
            .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
2977
            .get();
2✔
2978

2979
        wait_for_advance(*realm);
2✔
2980

2981
        REQUIRE(table->size() == obj_ids_at_end.size());
2!
2982
        for (auto& id : obj_ids_at_end) {
10✔
2983
            REQUIRE(table->find_primary_key(Mixed{id}));
10!
2984
        }
10✔
2985
    }
2✔
2986

2987
    SECTION("interrupted before final bootstrap message") {
8✔
2988
        {
2✔
2989
            auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
2990
            Realm::Config config = interrupted_realm_config;
2✔
2991
            config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
2992
            auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
2993
            config.sync_config->on_sync_client_event_hook =
2✔
2994
                [promise = std::move(shared_promise)](std::weak_ptr<SyncSession> weak_session,
2✔
2995
                                                      const SyncClientHookData& data) mutable {
28✔
2996
                    if (data.event != SyncClientHookEvent::BootstrapMessageProcessed) {
28✔
2997
                        return SyncClientHookAction::NoAction;
24✔
2998
                    }
24✔
2999
                    auto session = weak_session.lock();
4✔
3000
                    if (!session) {
4✔
3001
                        return SyncClientHookAction::NoAction;
×
3002
                    }
×
3003

3004
                    if (data.query_version == 1 && data.batch_state == sync::DownloadBatchState::MoreToCome) {
4✔
3005
                        session->force_close();
2✔
3006
                        promise->emplace_value();
2✔
3007
                        return SyncClientHookAction::TriggerReconnect;
2✔
3008
                    }
2✔
3009
                    return SyncClientHookAction::NoAction;
2✔
3010
                };
4✔
3011
            auto realm = Realm::get_shared_realm(config);
2✔
3012
            {
2✔
3013
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3014
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
3015
                mut_subs.insert_or_assign(Query(table));
2✔
3016
                mut_subs.commit();
2✔
3017
            }
2✔
3018

3019
            interrupted.get();
2✔
3020
            realm->sync_session()->shutdown_and_wait();
2✔
3021
            realm->close();
2✔
3022
        }
2✔
3023

3024
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
3025

3026
        // Open up the realm without the sync client attached and verify that the realm got interrupted in the state
3027
        // we expected it to be in.
3028
        {
2✔
3029
            DBOptions options;
2✔
3030
            options.encryption_key = test_util::crypt_key();
2✔
3031
            auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
3032
            auto logger = util::Logger::get_default_logger();
2✔
3033
            sync::PendingBootstrapStore bootstrap_store(realm, *logger);
2✔
3034
            REQUIRE(bootstrap_store.has_pending());
2!
3035
            auto pending_batch = bootstrap_store.peek_pending(1024 * 1024 * 16);
2✔
3036
            REQUIRE(pending_batch.query_version == 1);
2!
3037
            REQUIRE(!pending_batch.progress);
2!
3038
            REQUIRE(pending_batch.remaining_changesets == 0);
2!
3039
            REQUIRE(pending_batch.changesets.size() == 1);
2!
3040

3041
            check_interrupted_state(realm);
2✔
3042
        }
2✔
3043

3044
        // Now we'll open a different realm and make some changes that would leave orphan objects on the client
3045
        // if the bootstrap batches weren't being cached until lastInBatch were true.
3046
        mutate_realm();
2✔
3047

3048
        // Finally re-open the realm whose bootstrap we interrupted and just wait for it to finish downloading.
3049
        auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
3050
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
3051
        realm->get_latest_subscription_set()
2✔
3052
            .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
3053
            .get();
2✔
3054

3055
        wait_for_advance(*realm);
2✔
3056
        auto expected_obj_ids = util::Span<ObjectId>(obj_ids_at_end).sub_span(0, 3);
2✔
3057

3058
        REQUIRE(table->size() == expected_obj_ids.size());
2!
3059
        for (auto& id : expected_obj_ids) {
6✔
3060
            REQUIRE(table->find_primary_key(Mixed{id}));
6!
3061
        }
6✔
3062
    }
2✔
3063

3064
    SECTION("interrupted after final bootstrap message before processing") {
8✔
3065
        {
2✔
3066
            auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
3067
            Realm::Config config = interrupted_realm_config;
2✔
3068
            config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
3069
            auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
3070
            config.sync_config->on_sync_client_event_hook =
2✔
3071
                [promise = std::move(shared_promise)](std::weak_ptr<SyncSession> weak_session,
2✔
3072
                                                      const SyncClientHookData& data) mutable {
60✔
3073
                    if (data.event != SyncClientHookEvent::BootstrapMessageProcessed) {
60✔
3074
                        return SyncClientHookAction::NoAction;
46✔
3075
                    }
46✔
3076
                    auto session = weak_session.lock();
14✔
3077
                    if (!session) {
14✔
3078
                        return SyncClientHookAction::NoAction;
×
3079
                    }
×
3080

3081
                    if (data.query_version == 1 && data.batch_state == sync::DownloadBatchState::LastInBatch) {
14✔
3082
                        session->force_close();
2✔
3083
                        promise->emplace_value();
2✔
3084
                        return SyncClientHookAction::TriggerReconnect;
2✔
3085
                    }
2✔
3086
                    return SyncClientHookAction::NoAction;
12✔
3087
                };
14✔
3088
            auto realm = Realm::get_shared_realm(config);
2✔
3089
            {
2✔
3090
                auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3091
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
3092
                mut_subs.insert_or_assign(Query(table));
2✔
3093
                mut_subs.commit();
2✔
3094
            }
2✔
3095

3096
            interrupted.get();
2✔
3097
            realm->sync_session()->shutdown_and_wait();
2✔
3098
            realm->close();
2✔
3099
        }
2✔
3100

3101
        REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
3102

3103
        // Open up the realm without the sync client attached and verify that the realm got interrupted in the state
3104
        // we expected it to be in.
3105
        {
2✔
3106
            DBOptions options;
2✔
3107
            options.encryption_key = test_util::crypt_key();
2✔
3108
            auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
3109
            auto logger = util::Logger::get_default_logger();
2✔
3110
            sync::PendingBootstrapStore bootstrap_store(realm, *logger);
2✔
3111
            REQUIRE(bootstrap_store.has_pending());
2!
3112
            auto pending_batch = bootstrap_store.peek_pending(1024 * 1024 * 16);
2✔
3113
            REQUIRE(pending_batch.query_version == 1);
2!
3114
            REQUIRE(static_cast<bool>(pending_batch.progress));
2!
3115
            REQUIRE(pending_batch.remaining_changesets == 0);
2!
3116
            REQUIRE(pending_batch.changesets.size() == 6);
2!
3117

3118
            check_interrupted_state(realm);
2✔
3119
        }
2✔
3120

3121
        // Now we'll open a different realm and make some changes that would leave orphan objects on the client
3122
        // if the bootstrap batches weren't being cached until lastInBatch were true.
3123
        mutate_realm();
2✔
3124

3125
        auto [saw_valid_state_promise, saw_valid_state_future] = util::make_promise_future<void>();
2✔
3126
        auto shared_saw_valid_state_promise =
2✔
3127
            std::make_shared<decltype(saw_valid_state_promise)>(std::move(saw_valid_state_promise));
2✔
3128
        // This hook will let us check what the state of the realm is before it's integrated any new download
3129
        // messages from the server. This should be the full 5 object bootstrap that was received before we
3130
        // called mutate_realm().
3131
        interrupted_realm_config.sync_config->on_sync_client_event_hook =
2✔
3132
            [&, promise = std::move(shared_saw_valid_state_promise)](std::weak_ptr<SyncSession> weak_session,
2✔
3133
                                                                     const SyncClientHookData& data) {
38✔
3134
                if (data.event != SyncClientHookEvent::DownloadMessageReceived) {
38✔
3135
                    return SyncClientHookAction::NoAction;
36✔
3136
                }
36✔
3137
                auto session = weak_session.lock();
2✔
3138
                if (!session) {
2✔
3139
                    return SyncClientHookAction::NoAction;
×
3140
                }
×
3141

3142
                if (data.query_version != 1 || data.batch_state == sync::DownloadBatchState::MoreToCome) {
2✔
3143
                    return SyncClientHookAction::NoAction;
×
3144
                }
×
3145

3146
                auto latest_sub_set = session->get_flx_subscription_store()->get_latest();
2✔
3147
                auto active_sub_set = session->get_flx_subscription_store()->get_active();
2✔
3148
                auto version_info = session->get_flx_subscription_store()->get_version_info();
2✔
3149
                REQUIRE(version_info.pending_mark == active_sub_set.version());
2!
3150
                REQUIRE(version_info.active == active_sub_set.version());
2!
3151
                REQUIRE(version_info.latest == latest_sub_set.version());
2!
3152
                REQUIRE(latest_sub_set.version() == active_sub_set.version());
2!
3153
                REQUIRE(active_sub_set.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3154

3155
                auto db = SyncSession::OnlyForTesting::get_db(*session);
2✔
3156
                auto tr = db->start_read();
2✔
3157

3158
                auto table = tr->get_table("class_TopLevel");
2✔
3159
                REQUIRE(table->size() == obj_ids_at_end.size());
2!
3160
                for (auto& id : obj_ids_at_end) {
10✔
3161
                    REQUIRE(table->find_primary_key(Mixed{id}));
10!
3162
                }
10✔
3163

3164
                promise->emplace_value();
2✔
3165
                return SyncClientHookAction::NoAction;
2✔
3166
            };
2✔
3167

3168
        // Finally re-open the realm whose bootstrap we interrupted and just wait for it to finish downloading.
3169
        auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
3170
        saw_valid_state_future.get();
2✔
3171
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
3172
        realm->get_latest_subscription_set()
2✔
3173
            .get_state_change_notification(sync::SubscriptionSet::State::Complete)
2✔
3174
            .get();
2✔
3175

3176
        wait_for_advance(*realm);
2✔
3177
        auto expected_obj_ids = util::Span<ObjectId>(obj_ids_at_end).sub_span(0, 3);
2✔
3178

3179
        // After we've downloaded all the mutations there should only by 3 objects left.
3180
        REQUIRE(table->size() == expected_obj_ids.size());
2!
3181
        for (auto& id : expected_obj_ids) {
6✔
3182
            REQUIRE(table->find_primary_key(Mixed{id}));
6!
3183
        }
6✔
3184
    }
2✔
3185
}
8✔
3186

3187
// Check that a document with the given id is present and has the expected fields
3188
static void check_document(const std::vector<bson::BsonDocument>& documents, ObjectId id,
3189
                           std::initializer_list<std::pair<const char*, bson::Bson>> fields)
3190
{
428✔
3191
    auto it = std::find_if(documents.begin(), documents.end(), [&](auto&& doc) {
43,096✔
3192
        auto val = doc.find("_id");
43,096✔
3193
        REQUIRE(val);
43,096!
3194
        return *val == id;
43,096✔
3195
    });
43,096✔
3196
    REQUIRE(it != documents.end());
428!
3197
    auto& doc = *it;
428✔
3198
    for (auto& [name, expected_value] : fields) {
434✔
3199
        auto val = doc.find(name);
434✔
3200
        REQUIRE(val);
434!
3201

3202
        // bson documents are ordered  but Realm dictionaries aren't, so the
3203
        // document might validly be in a different order than we expected and
3204
        // we need to do a comparison that doesn't check order.
3205
        if (expected_value.type() == bson::Bson::Type::Document) {
434✔
3206
            REQUIRE(static_cast<const bson::BsonDocument&>(*val) ==
8!
3207
                    static_cast<const bson::BsonDocument&>(expected_value));
8✔
3208
        }
8✔
3209
        else {
426✔
3210
            REQUIRE(*val == expected_value);
426!
3211
        }
426✔
3212
    }
434✔
3213
}
428✔
3214

3215
TEST_CASE("flx: data ingest", "[sync][flx][data ingest][baas]") {
22✔
3216
    using namespace ::realm::bson;
22✔
3217

3218
    static auto server_schema = [] {
22✔
3219
        FLXSyncTestHarness::ServerSchema server_schema;
2✔
3220
        server_schema.queryable_fields = {"queryable_str_field"};
2✔
3221
        server_schema.schema = {
2✔
3222
            {"Asymmetric",
2✔
3223
             ObjectSchema::ObjectType::TopLevelAsymmetric,
2✔
3224
             {
2✔
3225
                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3226
                 {"location", PropertyType::String | PropertyType::Nullable},
2✔
3227
                 {"embedded obj", PropertyType::Object | PropertyType::Nullable, "Embedded"},
2✔
3228
                 {"embedded list", PropertyType::Object | PropertyType::Array, "Embedded"},
2✔
3229
                 {"embedded dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary,
2✔
3230
                  "Embedded"},
2✔
3231
                 {"link obj", PropertyType::Object | PropertyType::Nullable, "TopLevel"},
2✔
3232
                 {"link list", PropertyType::Object | PropertyType::Array, "TopLevel"},
2✔
3233
                 {"link dictionary", PropertyType::Object | PropertyType::Nullable | PropertyType::Dictionary,
2✔
3234
                  "TopLevel"},
2✔
3235
             }},
2✔
3236
            {"Embedded", ObjectSchema::ObjectType::Embedded, {{"value", PropertyType::String}}},
2✔
3237
            {"TopLevel",
2✔
3238
             {
2✔
3239
                 {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3240
                 {"value", PropertyType::Int},
2✔
3241
             }},
2✔
3242
        };
2✔
3243
        return server_schema;
2✔
3244
    }();
2✔
3245
    static auto harness = std::make_unique<FLXSyncTestHarness>("asymmetric_sync", server_schema);
22✔
3246

3247
    // We reuse a single app for each section, so tests will see the documents
3248
    // created by previous tests and we need to add those documents to the count
3249
    // we're waiting for
3250
    static std::unordered_map<std::string, size_t> previous_count;
22✔
3251
    auto get_documents = [&](const char* name, size_t expected_count) {
22✔
3252
        auto& count = previous_count[name];
18✔
3253
        auto documents =
18✔
3254
            harness->session().get_documents(*harness->app()->current_user(), name, count + expected_count);
18✔
3255
        count = documents.size();
18✔
3256
        return documents;
18✔
3257
    };
18✔
3258

3259
    SECTION("basic object construction") {
22✔
3260
        auto foo_obj_id = ObjectId::gen();
2✔
3261
        auto bar_obj_id = ObjectId::gen();
2✔
3262
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3263
            realm->begin_transaction();
2✔
3264
            CppContext c(realm);
2✔
3265
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}}));
2✔
3266
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", bar_obj_id}, {"location", "bar"s}}));
2✔
3267
            realm->commit_transaction();
2✔
3268

3269
            auto documents = get_documents("Asymmetric", 2);
2✔
3270
            check_document(documents, foo_obj_id, {{"location", "foo"}});
2✔
3271
            check_document(documents, bar_obj_id, {{"location", "bar"}});
2✔
3272
        });
2✔
3273

3274
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3275
            wait_for_download(*realm);
2✔
3276

3277
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3278
            REQUIRE(table->size() == 0);
2!
3279
            // Cannot query asymmetric tables.
3280
            CHECK_THROWS_AS(Query(table), LogicError);
2✔
3281
        });
2✔
3282
    }
2✔
3283

3284
    SECTION("do not allow objects with same key within the same transaction") {
22✔
3285
        auto foo_obj_id = ObjectId::gen();
2✔
3286
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3287
            realm->begin_transaction();
2✔
3288
            CppContext c(realm);
2✔
3289
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}}));
2✔
3290
            CHECK_THROWS_WITH(
2✔
3291
                Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "bar"s}})),
2✔
3292
                "Attempting to create an object of type 'Asymmetric' with an existing primary key value 'not "
2✔
3293
                "implemented'");
2✔
3294
            realm->commit_transaction();
2✔
3295

3296
            auto documents = get_documents("Asymmetric", 1);
2✔
3297
            check_document(documents, foo_obj_id, {{"location", "foo"}});
2✔
3298
        });
2✔
3299
    }
2✔
3300

3301
    SECTION("create multiple objects - separate commits") {
22✔
3302
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3303
            CppContext c(realm);
2✔
3304
            std::vector<ObjectId> obj_ids;
2✔
3305
            for (int i = 0; i < 100; ++i) {
202✔
3306
                realm->begin_transaction();
200✔
3307
                obj_ids.push_back(ObjectId::gen());
200✔
3308
                Object::create(c, realm, "Asymmetric",
200✔
3309
                               std::any(AnyDict{
200✔
3310
                                   {"_id", obj_ids.back()},
200✔
3311
                                   {"location", util::format("foo_%1", i)},
200✔
3312
                               }));
200✔
3313
                realm->commit_transaction();
200✔
3314
            }
200✔
3315

3316
            wait_for_upload(*realm);
2✔
3317
            wait_for_download(*realm);
2✔
3318

3319
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3320
            REQUIRE(table->size() == 0);
2!
3321

3322
            auto documents = get_documents("Asymmetric", 100);
2✔
3323
            for (int i = 0; i < 100; ++i) {
202✔
3324
                check_document(documents, obj_ids[i], {{"location", util::format("foo_%1", i)}});
200✔
3325
            }
200✔
3326
        });
2✔
3327
    }
2✔
3328

3329
    SECTION("create multiple objects - same commit") {
22✔
3330
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3331
            CppContext c(realm);
2✔
3332
            realm->begin_transaction();
2✔
3333
            std::vector<ObjectId> obj_ids;
2✔
3334
            for (int i = 0; i < 100; ++i) {
202✔
3335
                obj_ids.push_back(ObjectId::gen());
200✔
3336
                Object::create(c, realm, "Asymmetric",
200✔
3337
                               std::any(AnyDict{
200✔
3338
                                   {"_id", obj_ids.back()},
200✔
3339
                                   {"location", util::format("bar_%1", i)},
200✔
3340
                               }));
200✔
3341
            }
200✔
3342
            realm->commit_transaction();
2✔
3343

3344
            wait_for_upload(*realm);
2✔
3345
            wait_for_download(*realm);
2✔
3346

3347
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3348
            REQUIRE(table->size() == 0);
2!
3349

3350
            auto documents = get_documents("Asymmetric", 100);
2✔
3351
            for (int i = 0; i < 100; ++i) {
202✔
3352
                check_document(documents, obj_ids[i], {{"location", util::format("bar_%1", i)}});
200✔
3353
            }
200✔
3354
        });
2✔
3355
    }
2✔
3356

3357
    SECTION("open with schema mismatch on IsAsymmetric") {
22✔
3358
        auto schema = server_schema.schema;
2✔
3359
        schema.find("Asymmetric")->table_type = ObjectSchema::ObjectType::TopLevel;
2✔
3360

3361
        harness->do_with_new_user([&](std::shared_ptr<SyncUser> user) {
2✔
3362
            SyncTestFile config(user, schema, SyncConfig::FLXSyncEnabled{});
2✔
3363
            auto [error_promise, error_future] = util::make_promise_future<SyncError>();
2✔
3364
            auto error_count = 0;
2✔
3365
            auto err_handler = [promise = util::CopyablePromiseHolder(std::move(error_promise)),
2✔
3366
                                &error_count](std::shared_ptr<SyncSession>, SyncError err) mutable {
4✔
3367
                ++error_count;
4✔
3368
                if (error_count == 1) {
4✔
3369
                    // Bad changeset detected by the client.
3370
                    CHECK(err.status == ErrorCodes::BadChangeset);
2!
3371
                }
2✔
3372
                else if (error_count == 2) {
2✔
3373
                    // Server asking for a client reset.
3374
                    CHECK(err.status == ErrorCodes::SyncClientResetRequired);
2!
3375
                    CHECK(err.is_client_reset_requested());
2!
3376
                    promise.get_promise().emplace_value(std::move(err));
2✔
3377
                }
2✔
3378
            };
4✔
3379

3380
            config.sync_config->error_handler = err_handler;
2✔
3381
            auto realm = Realm::get_shared_realm(config);
2✔
3382

3383
            auto err = error_future.get();
2✔
3384
            CHECK(error_count == 2);
2!
3385
        });
2✔
3386
    }
2✔
3387

3388
    SECTION("basic embedded object construction") {
22✔
3389
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3390
            auto obj_id = ObjectId::gen();
2✔
3391
            realm->begin_transaction();
2✔
3392
            CppContext c(realm);
2✔
3393
            Object::create(c, realm, "Asymmetric",
2✔
3394
                           std::any(AnyDict{
2✔
3395
                               {"_id", obj_id},
2✔
3396
                               {"embedded obj", AnyDict{{"value", "foo"s}}},
2✔
3397
                           }));
2✔
3398
            realm->commit_transaction();
2✔
3399
            wait_for_upload(*realm);
2✔
3400

3401
            auto documents = get_documents("Asymmetric", 1);
2✔
3402
            check_document(documents, obj_id, {{"embedded obj", BsonDocument{{"value", "foo"}}}});
2✔
3403
        });
2✔
3404

3405
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3406
            wait_for_download(*realm);
2✔
3407

3408
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3409
            REQUIRE(table->size() == 0);
2!
3410
        });
2✔
3411
    }
2✔
3412

3413
    SECTION("replace embedded object") {
22✔
3414
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3415
            CppContext c(realm);
2✔
3416
            auto foo_obj_id = ObjectId::gen();
2✔
3417

3418
            realm->begin_transaction();
2✔
3419
            Object::create(c, realm, "Asymmetric",
2✔
3420
                           std::any(AnyDict{
2✔
3421
                               {"_id", foo_obj_id},
2✔
3422
                               {"embedded obj", AnyDict{{"value", "foo"s}}},
2✔
3423
                           }));
2✔
3424
            realm->commit_transaction();
2✔
3425

3426
            // Update embedded field to `null`. The server discards this write
3427
            // as asymmetric sync can only create new objects.
3428
            realm->begin_transaction();
2✔
3429
            Object::create(c, realm, "Asymmetric",
2✔
3430
                           std::any(AnyDict{
2✔
3431
                               {"_id", foo_obj_id},
2✔
3432
                               {"embedded obj", std::any()},
2✔
3433
                           }));
2✔
3434
            realm->commit_transaction();
2✔
3435

3436
            // create a second object so that we can know when the translator
3437
            // has processed everything
3438
            realm->begin_transaction();
2✔
3439
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", ObjectId::gen()}, {}}));
2✔
3440
            realm->commit_transaction();
2✔
3441

3442
            wait_for_upload(*realm);
2✔
3443
            wait_for_download(*realm);
2✔
3444

3445
            auto table = realm->read_group().get_table("class_Asymmetric");
2✔
3446
            REQUIRE(table->size() == 0);
2!
3447

3448
            auto documents = get_documents("Asymmetric", 2);
2✔
3449
            check_document(documents, foo_obj_id, {{"embedded obj", BsonDocument{{"value", "foo"}}}});
2✔
3450
        });
2✔
3451
    }
2✔
3452

3453
    SECTION("embedded collections") {
22✔
3454
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3455
            CppContext c(realm);
2✔
3456
            auto obj_id = ObjectId::gen();
2✔
3457

3458
            realm->begin_transaction();
2✔
3459
            Object::create(c, realm, "Asymmetric",
2✔
3460
                           std::any(AnyDict{
2✔
3461
                               {"_id", obj_id},
2✔
3462
                               {"embedded list", AnyVector{AnyDict{{"value", "foo"s}}, AnyDict{{"value", "bar"s}}}},
2✔
3463
                               {"embedded dictionary",
2✔
3464
                                AnyDict{
2✔
3465
                                    {"key1", AnyDict{{"value", "foo"s}}},
2✔
3466
                                    {"key2", AnyDict{{"value", "bar"s}}},
2✔
3467
                                }},
2✔
3468
                           }));
2✔
3469
            realm->commit_transaction();
2✔
3470

3471
            auto documents = get_documents("Asymmetric", 1);
2✔
3472
            check_document(
2✔
3473
                documents, obj_id,
2✔
3474
                {
2✔
3475
                    {"embedded list", BsonArray{BsonDocument{{"value", "foo"}}, BsonDocument{{"value", "bar"}}}},
2✔
3476
                    {"embedded dictionary",
2✔
3477
                     BsonDocument{
2✔
3478
                         {"key1", BsonDocument{{"value", "foo"}}},
2✔
3479
                         {"key2", BsonDocument{{"value", "bar"}}},
2✔
3480
                     }},
2✔
3481
                });
2✔
3482
        });
2✔
3483
    }
2✔
3484

3485
    SECTION("asymmetric table not allowed in PBS") {
22✔
3486
        Schema schema{
2✔
3487
            {"Asymmetric2",
2✔
3488
             ObjectSchema::ObjectType::TopLevelAsymmetric,
2✔
3489
             {
2✔
3490
                 {"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
3491
                 {"location", PropertyType::Int},
2✔
3492
                 {"reading", PropertyType::Int},
2✔
3493
             }},
2✔
3494
        };
2✔
3495

3496
        SyncTestFile config(harness->app()->current_user(), Bson{}, schema);
2✔
3497
        REQUIRE_EXCEPTION(
2✔
3498
            Realm::get_shared_realm(config), SchemaValidationFailed,
2✔
3499
            Catch::Matchers::ContainsSubstring("Asymmetric table 'Asymmetric2' not allowed in partition based sync"));
2✔
3500
    }
2✔
3501

3502
    SECTION("links to top-level objects") {
22✔
3503
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3504
            subscribe_to_all_and_bootstrap(*realm);
2✔
3505

3506
            ObjectId obj_id = ObjectId::gen();
2✔
3507
            std::array<ObjectId, 5> target_obj_ids;
2✔
3508
            for (auto& id : target_obj_ids) {
10✔
3509
                id = ObjectId::gen();
10✔
3510
            }
10✔
3511

3512
            realm->begin_transaction();
2✔
3513
            CppContext c(realm);
2✔
3514
            Object::create(c, realm, "Asymmetric",
2✔
3515
                           std::any(AnyDict{
2✔
3516
                               {"_id", obj_id},
2✔
3517
                               {"link obj", AnyDict{{"_id", target_obj_ids[0]}, {"value", INT64_C(10)}}},
2✔
3518
                               {"link list",
2✔
3519
                                AnyVector{
2✔
3520
                                    AnyDict{{"_id", target_obj_ids[1]}, {"value", INT64_C(11)}},
2✔
3521
                                    AnyDict{{"_id", target_obj_ids[2]}, {"value", INT64_C(12)}},
2✔
3522
                                }},
2✔
3523
                               {"link dictionary",
2✔
3524
                                AnyDict{
2✔
3525
                                    {"key1", AnyDict{{"_id", target_obj_ids[3]}, {"value", INT64_C(13)}}},
2✔
3526
                                    {"key2", AnyDict{{"_id", target_obj_ids[4]}, {"value", INT64_C(14)}}},
2✔
3527
                                }},
2✔
3528
                           }));
2✔
3529
            realm->commit_transaction();
2✔
3530
            wait_for_upload(*realm);
2✔
3531

3532
            auto docs1 = get_documents("Asymmetric", 1);
2✔
3533
            check_document(docs1, obj_id,
2✔
3534
                           {{"link obj", target_obj_ids[0]},
2✔
3535
                            {"link list", BsonArray{{target_obj_ids[1], target_obj_ids[2]}}},
2✔
3536
                            {
2✔
3537
                                "link dictionary",
2✔
3538
                                BsonDocument{
2✔
3539
                                    {"key1", target_obj_ids[3]},
2✔
3540
                                    {"key2", target_obj_ids[4]},
2✔
3541
                                },
2✔
3542
                            }});
2✔
3543

3544
            auto docs2 = get_documents("TopLevel", 5);
2✔
3545
            for (int64_t i = 0; i < 5; ++i) {
12✔
3546
                check_document(docs2, target_obj_ids[i], {{"value", 10 + i}});
10✔
3547
            }
10✔
3548
        });
2✔
3549
    }
2✔
3550

3551
    // Add any new test sections above this point
3552

3553
    SECTION("teardown") {
22✔
3554
        harness.reset();
2✔
3555
    }
2✔
3556
}
22✔
3557

3558
TEST_CASE("flx: data ingest - dev mode", "[sync][flx][data ingest][baas]") {
2✔
3559
    FLXSyncTestHarness::ServerSchema server_schema;
2✔
3560
    server_schema.dev_mode_enabled = true;
2✔
3561
    server_schema.schema = Schema{};
2✔
3562

3563
    auto schema = Schema{{"Asymmetric",
2✔
3564
                          ObjectSchema::ObjectType::TopLevelAsymmetric,
2✔
3565
                          {
2✔
3566
                              {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3567
                              {"location", PropertyType::String | PropertyType::Nullable},
2✔
3568
                          }},
2✔
3569
                         {"TopLevel",
2✔
3570
                          {
2✔
3571
                              {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3572
                              {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
3573
                          }}};
2✔
3574

3575
    FLXSyncTestHarness harness("asymmetric_sync", server_schema);
2✔
3576

3577
    auto foo_obj_id = ObjectId::gen();
2✔
3578
    auto bar_obj_id = ObjectId::gen();
2✔
3579

3580
    harness.do_with_new_realm(
2✔
3581
        [&](SharedRealm realm) {
2✔
3582
            CppContext c(realm);
2✔
3583
            realm->begin_transaction();
2✔
3584
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", foo_obj_id}, {"location", "foo"s}}));
2✔
3585
            Object::create(c, realm, "Asymmetric", std::any(AnyDict{{"_id", bar_obj_id}, {"location", "bar"s}}));
2✔
3586
            realm->commit_transaction();
2✔
3587
            User* user = dynamic_cast<User*>(realm->config().sync_config->user.get());
2✔
3588
            REALM_ASSERT(user);
2✔
3589
            auto docs = harness.session().get_documents(*user, "Asymmetric", 2);
2✔
3590
            check_document(docs, foo_obj_id, {{"location", "foo"}});
2✔
3591
            check_document(docs, bar_obj_id, {{"location", "bar"}});
2✔
3592
        },
2✔
3593
        schema);
2✔
3594
}
2✔
3595

3596
TEST_CASE("flx: data ingest - write not allowed", "[sync][flx][data ingest][baas]") {
2✔
3597
    AppCreateConfig::ServiceRole role;
2✔
3598
    role.name = "asymmetric_write_perms";
2✔
3599

3600
    AppCreateConfig::ServiceRoleDocumentFilters doc_filters;
2✔
3601
    doc_filters.read = true;
2✔
3602
    doc_filters.write = false;
2✔
3603
    role.document_filters = doc_filters;
2✔
3604

3605
    role.insert_filter = true;
2✔
3606
    role.delete_filter = true;
2✔
3607
    role.read = true;
2✔
3608
    role.write = true;
2✔
3609

3610
    Schema schema({
2✔
3611
        {"Asymmetric",
2✔
3612
         ObjectSchema::ObjectType::TopLevelAsymmetric,
2✔
3613
         {
2✔
3614
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
3615
             {"location", PropertyType::String | PropertyType::Nullable},
2✔
3616
             {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "Embedded"},
2✔
3617
         }},
2✔
3618
        {"Embedded",
2✔
3619
         ObjectSchema::ObjectType::Embedded,
2✔
3620
         {
2✔
3621
             {"value", PropertyType::String | PropertyType::Nullable},
2✔
3622
         }},
2✔
3623
    });
2✔
3624
    FLXSyncTestHarness::ServerSchema server_schema{schema, {}, {role}};
2✔
3625
    FLXSyncTestHarness harness("asymmetric_sync", server_schema);
2✔
3626

3627
    auto error_received_pf = util::make_promise_future<void>();
2✔
3628
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3629
    config.sync_config->on_sync_client_event_hook =
2✔
3630
        [promise = util::CopyablePromiseHolder(std::move(error_received_pf.promise))](
2✔
3631
            std::weak_ptr<SyncSession> weak_session, const SyncClientHookData& data) mutable {
24✔
3632
            if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
24✔
3633
                return SyncClientHookAction::NoAction;
20✔
3634
            }
20✔
3635
            auto session = weak_session.lock();
4✔
3636
            REQUIRE(session);
4!
3637

3638
            auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
4✔
3639

3640
            if (error_code == sync::ProtocolError::initial_sync_not_completed) {
4✔
3641
                return SyncClientHookAction::NoAction;
2✔
3642
            }
2✔
3643

3644
            REQUIRE(error_code == sync::ProtocolError::write_not_allowed);
2!
3645
            REQUIRE_FALSE(data.error_info->compensating_write_server_version.has_value());
2!
3646
            REQUIRE_FALSE(data.error_info->compensating_writes.empty());
2!
3647
            promise.get_promise().emplace_value();
2✔
3648

3649
            return SyncClientHookAction::EarlyReturn;
2✔
3650
        };
2✔
3651

3652
    auto realm = Realm::get_shared_realm(config);
2✔
3653

3654
    // Create an asymmetric object and upload it to the server.
3655
    {
2✔
3656
        realm->begin_transaction();
2✔
3657
        CppContext c(realm);
2✔
3658
        Object::create(c, realm, "Asymmetric",
2✔
3659
                       std::any(AnyDict{{"_id", ObjectId::gen()}, {"embedded_obj", AnyDict{{"value", "foo"s}}}}));
2✔
3660
        realm->commit_transaction();
2✔
3661
    }
2✔
3662

3663
    error_received_pf.future.get();
2✔
3664
    realm->close();
2✔
3665
}
2✔
3666

3667
TEST_CASE("flx: send client error", "[sync][flx][baas]") {
2✔
3668
    FLXSyncTestHarness harness("flx_client_error");
2✔
3669

3670
    // An integration error is simulated while bootstrapping.
3671
    // This results in the client sending an error message to the server.
3672
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3673
    config.sync_config->simulate_integration_error = true;
2✔
3674

3675
    auto [error_promise, error_future] = util::make_promise_future<SyncError>();
2✔
3676
    auto error_count = 0;
2✔
3677
    auto err_handler = [promise = util::CopyablePromiseHolder(std::move(error_promise)),
2✔
3678
                        &error_count](std::shared_ptr<SyncSession>, SyncError err) mutable {
4✔
3679
        ++error_count;
4✔
3680
        if (error_count == 1) {
4✔
3681
            // Bad changeset detected by the client.
3682
            CHECK(err.status == ErrorCodes::BadChangeset);
2!
3683
        }
2✔
3684
        else if (error_count == 2) {
2✔
3685
            // Server asking for a client reset.
3686
            CHECK(err.status == ErrorCodes::SyncClientResetRequired);
2!
3687
            CHECK(err.is_client_reset_requested());
2!
3688
            promise.get_promise().emplace_value(std::move(err));
2✔
3689
        }
2✔
3690
    };
4✔
3691

3692
    config.sync_config->error_handler = err_handler;
2✔
3693
    auto realm = Realm::get_shared_realm(config);
2✔
3694
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
3695
    auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3696
    new_query.insert_or_assign(Query(table));
2✔
3697
    new_query.commit();
2✔
3698

3699
    auto err = error_future.get();
2✔
3700
    CHECK(error_count == 2);
2!
3701
}
2✔
3702

3703
TEST_CASE("flx: bootstraps contain all changes", "[sync][flx][bootstrap][baas]") {
6✔
3704
    FLXSyncTestHarness harness("bootstrap_full_sync");
6✔
3705

3706
    auto setup_subs = [](SharedRealm& realm) {
12✔
3707
        auto table = realm->read_group().get_table("class_TopLevel");
12✔
3708
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
12✔
3709
        new_query.clear();
12✔
3710
        auto col = table->get_column_key("queryable_str_field");
12✔
3711
        new_query.insert_or_assign(Query(table).equal(col, StringData("bar")).Or().equal(col, StringData("bizz")));
12✔
3712
        return new_query.commit();
12✔
3713
    };
12✔
3714

3715
    auto bar_obj_id = ObjectId::gen();
6✔
3716
    auto bizz_obj_id = ObjectId::gen();
6✔
3717
    auto setup_and_poison_cache = [&] {
6✔
3718
        harness.load_initial_data([&](SharedRealm realm) {
6✔
3719
            CppContext c(realm);
6✔
3720
            Object::create(c, realm, "TopLevel",
6✔
3721
                           std::any(AnyDict{{"_id", bar_obj_id},
6✔
3722
                                            {"queryable_str_field", std::string{"bar"}},
6✔
3723
                                            {"queryable_int_field", static_cast<int64_t>(10)},
6✔
3724
                                            {"non_queryable_field", std::string{"non queryable 2"}}}));
6✔
3725
        });
6✔
3726

3727
        harness.do_with_new_realm([&](SharedRealm realm) {
6✔
3728
            // first set a subscription to force the creation/caching of a broker snapshot on the server.
3729
            setup_subs(realm).get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
6✔
3730
            wait_for_advance(*realm);
6✔
3731
            auto table = realm->read_group().get_table("class_TopLevel");
6✔
3732
            REQUIRE(table->find_primary_key(bar_obj_id));
6!
3733

3734
            // Then create an object that won't be in the cached snapshot - this is the object that if we didn't
3735
            // wait for a MARK message to come back, we'd miss it in our results.
3736
            CppContext c(realm);
6✔
3737
            realm->begin_transaction();
6✔
3738
            Object::create(c, realm, "TopLevel",
6✔
3739
                           std::any(AnyDict{{"_id", bizz_obj_id},
6✔
3740
                                            {"queryable_str_field", std::string{"bizz"}},
6✔
3741
                                            {"queryable_int_field", static_cast<int64_t>(15)},
6✔
3742
                                            {"non_queryable_field", std::string{"non queryable 3"}}}));
6✔
3743
            realm->commit_transaction();
6✔
3744
            wait_for_upload(*realm);
6✔
3745
        });
6✔
3746
    };
6✔
3747

3748
    SECTION("regular subscription change") {
6✔
3749
        SyncTestFile triggered_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3750
        std::atomic<bool> saw_truncated_bootstrap{false};
2✔
3751
        triggered_config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_sess,
2✔
3752
                                                                      const SyncClientHookData& data) {
47✔
3753
            auto sess = weak_sess.lock();
47✔
3754
            if (!sess || data.event != SyncClientHookEvent::BootstrapProcessed || data.query_version != 1) {
47✔
3755
                return SyncClientHookAction::NoAction;
45✔
3756
            }
45✔
3757

3758
            auto latest_subs = sess->get_flx_subscription_store()->get_latest();
2✔
3759
            REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3760
            REQUIRE(data.num_changesets == 1);
2!
3761
            auto db = SyncSession::OnlyForTesting::get_db(*sess);
2✔
3762
            auto read_tr = db->start_read();
2✔
3763
            auto table = read_tr->get_table("class_TopLevel");
2✔
3764
            REQUIRE(table->find_primary_key(bar_obj_id));
2!
3765
            REQUIRE_FALSE(table->find_primary_key(bizz_obj_id));
2!
3766
            saw_truncated_bootstrap.store(true);
2✔
3767

3768
            return SyncClientHookAction::NoAction;
2✔
3769
        };
2✔
3770
        auto problem_realm = Realm::get_shared_realm(triggered_config);
2✔
3771

3772
        // Setup the problem realm by waiting for it to be fully synchronized with an empty query, so the router
3773
        // on the server should have no new history entries, and then pause the router so it doesn't get any of
3774
        // the changes we're about to create.
3775
        wait_for_upload(*problem_realm);
2✔
3776
        wait_for_download(*problem_realm);
2✔
3777

3778
        nlohmann::json command_request = {
2✔
3779
            {"command", "PAUSE_ROUTER_SESSION"},
2✔
3780
        };
2✔
3781
        auto resp_body =
2✔
3782
            SyncSession::OnlyForTesting::send_test_command(*problem_realm->sync_session(), command_request.dump())
2✔
3783
                .get();
2✔
3784
        REQUIRE(resp_body == "{}");
2!
3785

3786
        // Put some data into the server, this will be the data that will be in the broker cache.
3787
        setup_and_poison_cache();
2✔
3788

3789
        // Setup queries on the problem realm to bootstrap from the cached object. Bootstrapping will also resume
3790
        // the router, so all we need to do is wait for the subscription set to be complete and notifications to be
3791
        // processed.
3792
        setup_subs(problem_realm).get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
3793
        wait_for_advance(*problem_realm);
2✔
3794

3795
        REQUIRE(saw_truncated_bootstrap.load());
2!
3796
        auto table = problem_realm->read_group().get_table("class_TopLevel");
2✔
3797
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3798
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3799
    }
2✔
3800

3801
// TODO: remote-baas: This test fails intermittently with Windows remote baas server - to be fixed in RCORE-1674
3802
#ifndef _WIN32
6✔
3803
    SECTION("disconnect between bootstrap and mark") {
6✔
3804
        SyncTestFile triggered_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3805
        auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
3806
        triggered_config.sync_config->on_sync_client_event_hook =
2✔
3807
            [promise = util::CopyablePromiseHolder(std::move(interrupted_promise)), &bizz_obj_id,
2✔
3808
             &bar_obj_id](std::weak_ptr<SyncSession> weak_sess, const SyncClientHookData& data) mutable {
49✔
3809
                auto sess = weak_sess.lock();
49✔
3810
                if (!sess || data.event != SyncClientHookEvent::BootstrapProcessed || data.query_version != 1) {
49✔
3811
                    return SyncClientHookAction::NoAction;
47✔
3812
                }
47✔
3813

3814
                auto latest_subs = sess->get_flx_subscription_store()->get_latest();
2✔
3815
                REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3816
                REQUIRE(data.num_changesets == 1);
2!
3817
                auto db = SyncSession::OnlyForTesting::get_db(*sess);
2✔
3818
                auto read_tr = db->start_read();
2✔
3819
                auto table = read_tr->get_table("class_TopLevel");
2✔
3820
                REQUIRE(table->find_primary_key(bar_obj_id));
2!
3821
                REQUIRE_FALSE(table->find_primary_key(bizz_obj_id));
2!
3822

3823
                sess->pause();
2✔
3824
                promise.get_promise().emplace_value();
2✔
3825
                return SyncClientHookAction::NoAction;
2✔
3826
            };
2✔
3827
        auto problem_realm = Realm::get_shared_realm(triggered_config);
2✔
3828

3829
        // Setup the problem realm by waiting for it to be fully synchronized with an empty query, so the router
3830
        // on the server should have no new history entries, and then pause the router so it doesn't get any of
3831
        // the changes we're about to create.
3832
        wait_for_upload(*problem_realm);
2✔
3833
        wait_for_download(*problem_realm);
2✔
3834

3835
        nlohmann::json command_request = {
2✔
3836
            {"command", "PAUSE_ROUTER_SESSION"},
2✔
3837
        };
2✔
3838
        auto resp_body =
2✔
3839
            SyncSession::OnlyForTesting::send_test_command(*problem_realm->sync_session(), command_request.dump())
2✔
3840
                .get();
2✔
3841
        REQUIRE(resp_body == "{}");
2!
3842

3843
        // Put some data into the server, this will be the data that will be in the broker cache.
3844
        setup_and_poison_cache();
2✔
3845

3846
        // Setup queries on the problem realm to bootstrap from the cached object. Bootstrapping will also resume
3847
        // the router, so all we need to do is wait for the subscription set to be complete and notifications to be
3848
        // processed.
3849
        auto sub_set = setup_subs(problem_realm);
2✔
3850
        auto sub_complete_future = sub_set.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
3851

3852
        interrupted.get();
2✔
3853
        problem_realm->sync_session()->shutdown_and_wait();
2✔
3854
        REQUIRE(sub_complete_future.is_ready());
2!
3855
        sub_set.refresh();
2✔
3856
        REQUIRE(sub_set.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3857

3858
        sub_complete_future = sub_set.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
3859
        problem_realm->sync_session()->resume();
2✔
3860
        sub_complete_future.get();
2✔
3861
        wait_for_advance(*problem_realm);
2✔
3862

3863
        sub_set.refresh();
2✔
3864
        REQUIRE(sub_set.state() == sync::SubscriptionSet::State::Complete);
2!
3865
        auto table = problem_realm->read_group().get_table("class_TopLevel");
2✔
3866
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3867
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3868
    }
2✔
3869
#endif
6✔
3870
    SECTION("error/suspend between bootstrap and mark") {
6✔
3871
        SyncTestFile triggered_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
3872
        triggered_config.sync_config->on_sync_client_event_hook =
2✔
3873
            [&bizz_obj_id, &bar_obj_id](std::weak_ptr<SyncSession> weak_sess, const SyncClientHookData& data) {
49✔
3874
                auto sess = weak_sess.lock();
49✔
3875
                if (!sess || data.event != SyncClientHookEvent::BootstrapProcessed || data.query_version != 1) {
49✔
3876
                    return SyncClientHookAction::NoAction;
47✔
3877
                }
47✔
3878

3879
                auto latest_subs = sess->get_flx_subscription_store()->get_latest();
2✔
3880
                REQUIRE(latest_subs.state() == sync::SubscriptionSet::State::AwaitingMark);
2!
3881
                REQUIRE(data.num_changesets == 1);
2!
3882
                auto db = SyncSession::OnlyForTesting::get_db(*sess);
2✔
3883
                auto read_tr = db->start_read();
2✔
3884
                auto table = read_tr->get_table("class_TopLevel");
2✔
3885
                REQUIRE(table->find_primary_key(bar_obj_id));
2!
3886
                REQUIRE_FALSE(table->find_primary_key(bizz_obj_id));
2!
3887

3888
                return SyncClientHookAction::TriggerReconnect;
2✔
3889
            };
2✔
3890
        auto problem_realm = Realm::get_shared_realm(triggered_config);
2✔
3891

3892
        // Setup the problem realm by waiting for it to be fully synchronized with an empty query, so the router
3893
        // on the server should have no new history entries, and then pause the router so it doesn't get any of
3894
        // the changes we're about to create.
3895
        wait_for_upload(*problem_realm);
2✔
3896
        wait_for_download(*problem_realm);
2✔
3897

3898
        nlohmann::json command_request = {
2✔
3899
            {"command", "PAUSE_ROUTER_SESSION"},
2✔
3900
        };
2✔
3901
        auto resp_body =
2✔
3902
            SyncSession::OnlyForTesting::send_test_command(*problem_realm->sync_session(), command_request.dump())
2✔
3903
                .get();
2✔
3904
        REQUIRE(resp_body == "{}");
2!
3905

3906
        // Put some data into the server, this will be the data that will be in the broker cache.
3907
        setup_and_poison_cache();
2✔
3908

3909
        // Setup queries on the problem realm to bootstrap from the cached object. Bootstrapping will also resume
3910
        // the router, so all we need to do is wait for the subscription set to be complete and notifications to be
3911
        // processed.
3912
        auto sub_set = setup_subs(problem_realm);
2✔
3913
        auto sub_complete_future = sub_set.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
3914

3915
        sub_complete_future.get();
2✔
3916
        wait_for_advance(*problem_realm);
2✔
3917

3918
        sub_set.refresh();
2✔
3919
        REQUIRE(sub_set.state() == sync::SubscriptionSet::State::Complete);
2!
3920
        auto table = problem_realm->read_group().get_table("class_TopLevel");
2✔
3921
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3922
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3923
    }
2✔
3924
}
6✔
3925

3926
TEST_CASE("flx: convert flx sync realm to bundled realm", "[app][flx][baas]") {
12✔
3927
    static auto foo_obj_id = ObjectId::gen();
12✔
3928
    static auto bar_obj_id = ObjectId::gen();
12✔
3929
    static auto bizz_obj_id = ObjectId::gen();
12✔
3930
    static std::optional<FLXSyncTestHarness> harness;
12✔
3931
    if (!harness) {
12✔
3932
        harness.emplace("bundled_flx_realms");
2✔
3933
        harness->load_initial_data([&](SharedRealm realm) {
2✔
3934
            CppContext c(realm);
2✔
3935
            Object::create(c, realm, "TopLevel",
2✔
3936
                           std::any(AnyDict{{"_id", foo_obj_id},
2✔
3937
                                            {"queryable_str_field", "foo"s},
2✔
3938
                                            {"queryable_int_field", static_cast<int64_t>(5)},
2✔
3939
                                            {"non_queryable_field", "non queryable 1"s}}));
2✔
3940
            Object::create(c, realm, "TopLevel",
2✔
3941
                           std::any(AnyDict{{"_id", bar_obj_id},
2✔
3942
                                            {"queryable_str_field", "bar"s},
2✔
3943
                                            {"queryable_int_field", static_cast<int64_t>(10)},
2✔
3944
                                            {"non_queryable_field", "non queryable 2"s}}));
2✔
3945
        });
2✔
3946
    }
2✔
3947

3948
    SECTION("flx to flx (should succeed)") {
12✔
3949
        create_user_and_log_in(harness->app());
2✔
3950
        SyncTestFile target_config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{});
2✔
3951
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3952
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
3953
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3954
            mut_subs.insert_or_assign(Query(table).greater(table->get_column_key("queryable_int_field"), 5));
2✔
3955
            auto subs = std::move(mut_subs).commit();
2✔
3956

3957
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
3958
            wait_for_advance(*realm);
2✔
3959

3960
            realm->convert(target_config);
2✔
3961
        });
2✔
3962

3963
        auto target_realm = Realm::get_shared_realm(target_config);
2✔
3964

3965
        target_realm->begin_transaction();
2✔
3966
        CppContext c(target_realm);
2✔
3967
        Object::create(c, target_realm, "TopLevel",
2✔
3968
                       std::any(AnyDict{{"_id", bizz_obj_id},
2✔
3969
                                        {"queryable_str_field", "bizz"s},
2✔
3970
                                        {"queryable_int_field", static_cast<int64_t>(15)},
2✔
3971
                                        {"non_queryable_field", "non queryable 3"s}}));
2✔
3972
        target_realm->commit_transaction();
2✔
3973

3974
        wait_for_upload(*target_realm);
2✔
3975
        wait_for_download(*target_realm);
2✔
3976

3977
        auto latest_subs = target_realm->get_active_subscription_set();
2✔
3978
        auto table = target_realm->read_group().get_table("class_TopLevel");
2✔
3979
        REQUIRE(latest_subs.size() == 1);
2!
3980
        REQUIRE(latest_subs.at(0).object_class_name == "TopLevel");
2!
3981
        REQUIRE(latest_subs.at(0).query_string ==
2!
3982
                Query(table).greater(table->get_column_key("queryable_int_field"), 5).get_description());
2✔
3983

3984
        REQUIRE(table->size() == 2);
2!
3985
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
3986
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
3987
        REQUIRE_FALSE(table->find_primary_key(foo_obj_id));
2!
3988
    }
2✔
3989

3990
    SECTION("flx to local (should succeed)") {
12✔
3991
        TestFile target_config;
2✔
3992

3993
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
3994
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
3995
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
3996
            mut_subs.insert_or_assign(Query(table).greater(table->get_column_key("queryable_int_field"), 5));
2✔
3997
            auto subs = std::move(mut_subs).commit();
2✔
3998

3999
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
4000
            wait_for_advance(*realm);
2✔
4001

4002
            target_config.schema = realm->schema();
2✔
4003
            target_config.schema_version = realm->schema_version();
2✔
4004
            realm->convert(target_config);
2✔
4005
        });
2✔
4006

4007
        auto target_realm = Realm::get_shared_realm(target_config);
2✔
4008
        REQUIRE_THROWS(target_realm->get_active_subscription_set());
2✔
4009

4010
        auto table = target_realm->read_group().get_table("class_TopLevel");
2✔
4011
        REQUIRE(table->size() == 2);
2!
4012
        REQUIRE(table->find_primary_key(bar_obj_id));
2!
4013
        REQUIRE(table->find_primary_key(bizz_obj_id));
2!
4014
        REQUIRE_FALSE(table->find_primary_key(foo_obj_id));
2!
4015
    }
2✔
4016

4017
    SECTION("flx to pbs (should fail to convert)") {
12✔
4018
        create_user_and_log_in(harness->app());
2✔
4019
        SyncTestFile target_config(harness->app()->current_user(), "12345"s, harness->schema());
2✔
4020
        harness->do_with_new_realm([&](SharedRealm realm) {
2✔
4021
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
4022
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4023
            mut_subs.insert_or_assign(Query(table).greater(table->get_column_key("queryable_int_field"), 5));
2✔
4024
            auto subs = std::move(mut_subs).commit();
2✔
4025

4026
            subs.get_state_change_notification(sync::SubscriptionSet::State::Complete).get();
2✔
4027
            wait_for_advance(*realm);
2✔
4028

4029
            REQUIRE_THROWS(realm->convert(target_config));
2✔
4030
        });
2✔
4031
    }
2✔
4032

4033
    SECTION("pbs to flx (should fail to convert)") {
12✔
4034
        create_user_and_log_in(harness->app());
2✔
4035
        SyncTestFile target_config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{});
2✔
4036

4037
        auto pbs_app_config = minimal_app_config("pbs_to_flx_convert", harness->schema());
2✔
4038

4039
        TestAppSession pbs_app_session(create_app(pbs_app_config));
2✔
4040
        SyncTestFile source_config(pbs_app_session.app()->current_user(), "54321"s, pbs_app_config.schema);
2✔
4041
        auto realm = Realm::get_shared_realm(source_config);
2✔
4042

4043
        realm->begin_transaction();
2✔
4044
        CppContext c(realm);
2✔
4045
        Object::create(c, realm, "TopLevel",
2✔
4046
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
4047
                                        {"queryable_str_field", "foo"s},
2✔
4048
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
4049
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
4050
        realm->commit_transaction();
2✔
4051

4052
        REQUIRE_THROWS(realm->convert(target_config));
2✔
4053
    }
2✔
4054

4055
    SECTION("local to flx (should fail to convert)") {
12✔
4056
        TestFile source_config;
2✔
4057
        source_config.schema = harness->schema();
2✔
4058
        source_config.schema_version = 1;
2✔
4059

4060
        auto realm = Realm::get_shared_realm(source_config);
2✔
4061
        auto foo_obj_id = ObjectId::gen();
2✔
4062

4063
        realm->begin_transaction();
2✔
4064
        CppContext c(realm);
2✔
4065
        Object::create(c, realm, "TopLevel",
2✔
4066
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
4067
                                        {"queryable_str_field", "foo"s},
2✔
4068
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
4069
                                        {"non_queryable_field", "non queryable 1"s}}));
2✔
4070
        realm->commit_transaction();
2✔
4071

4072
        create_user_and_log_in(harness->app());
2✔
4073
        SyncTestFile target_config(harness->app()->current_user(), harness->schema(), SyncConfig::FLXSyncEnabled{});
2✔
4074

4075
        REQUIRE_THROWS(realm->convert(target_config));
2✔
4076
    }
2✔
4077

4078
    // Add new sections before this
4079
    SECTION("teardown") {
12✔
4080
        harness->app()->sync_manager()->wait_for_sessions_to_terminate();
2✔
4081
        harness.reset();
2✔
4082
    }
2✔
4083
}
12✔
4084

4085
TEST_CASE("flx: compensating write errors get re-sent across sessions", "[sync][flx][compensating write][baas]") {
2✔
4086
    AppCreateConfig::ServiceRole role;
2✔
4087
    role.name = "compensating_write_perms";
2✔
4088

4089
    AppCreateConfig::ServiceRoleDocumentFilters doc_filters;
2✔
4090
    doc_filters.read = true;
2✔
4091
    doc_filters.write =
2✔
4092
        nlohmann::json{{"queryable_str_field", nlohmann::json{{"$in", nlohmann::json::array({"foo", "bar"})}}}};
2✔
4093
    role.document_filters = doc_filters;
2✔
4094

4095
    role.insert_filter = true;
2✔
4096
    role.delete_filter = true;
2✔
4097
    role.read = true;
2✔
4098
    role.write = true;
2✔
4099
    FLXSyncTestHarness::ServerSchema server_schema{
2✔
4100
        g_simple_embedded_obj_schema, {"queryable_str_field", "queryable_int_field"}, {role}};
2✔
4101
    FLXSyncTestHarness::Config harness_config("flx_bad_query", server_schema);
2✔
4102
    harness_config.reconnect_mode = ReconnectMode::testing;
2✔
4103
    FLXSyncTestHarness harness(std::move(harness_config));
2✔
4104

4105
    auto test_obj_id_1 = ObjectId::gen();
2✔
4106
    auto test_obj_id_2 = ObjectId::gen();
2✔
4107

4108
    create_user_and_log_in(harness.app());
2✔
4109
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4110

4111
    {
2✔
4112
        auto error_received_pf = util::make_promise_future<void>();
2✔
4113
        config.sync_config->on_sync_client_event_hook =
2✔
4114
            [promise = util::CopyablePromiseHolder(std::move(error_received_pf.promise))](
2✔
4115
                std::weak_ptr<SyncSession> weak_session, const SyncClientHookData& data) mutable {
46✔
4116
                if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
46✔
4117
                    return SyncClientHookAction::NoAction;
44✔
4118
                }
44✔
4119
                auto session = weak_session.lock();
2✔
4120
                REQUIRE(session);
2!
4121

4122
                auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
2✔
4123

4124
                if (error_code == sync::ProtocolError::initial_sync_not_completed) {
2✔
4125
                    return SyncClientHookAction::NoAction;
×
4126
                }
×
4127

4128
                REQUIRE(error_code == sync::ProtocolError::compensating_write);
2!
4129
                REQUIRE_FALSE(data.error_info->compensating_writes.empty());
2!
4130
                promise.get_promise().emplace_value();
2✔
4131

4132
                return SyncClientHookAction::TriggerReconnect;
2✔
4133
            };
2✔
4134

4135
        auto realm = Realm::get_shared_realm(config);
2✔
4136
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
4137
        auto queryable_str_field = table->get_column_key("queryable_str_field");
2✔
4138
        auto new_query = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4139
        new_query.insert_or_assign(Query(table).equal(queryable_str_field, "bizz"));
2✔
4140
        std::move(new_query).commit();
2✔
4141

4142
        wait_for_upload(*realm);
2✔
4143
        wait_for_download(*realm);
2✔
4144

4145
        CppContext c(realm);
2✔
4146
        realm->begin_transaction();
2✔
4147
        Object::create(c, realm, "TopLevel",
2✔
4148
                       util::Any(AnyDict{
2✔
4149
                           {"_id", test_obj_id_1},
2✔
4150
                           {"queryable_str_field", std::string{"foo"}},
2✔
4151
                       }));
2✔
4152
        realm->commit_transaction();
2✔
4153

4154
        realm->begin_transaction();
2✔
4155
        Object::create(c, realm, "TopLevel",
2✔
4156
                       util::Any(AnyDict{
2✔
4157
                           {"_id", test_obj_id_2},
2✔
4158
                           {"queryable_str_field", std::string{"baz"}},
2✔
4159
                       }));
2✔
4160
        realm->commit_transaction();
2✔
4161

4162
        error_received_pf.future.get();
2✔
4163
        realm->sync_session()->shutdown_and_wait();
2✔
4164
        config.sync_config->on_sync_client_event_hook = {};
2✔
4165
    }
2✔
4166

4167
    _impl::RealmCoordinator::clear_all_caches();
2✔
4168

4169
    std::mutex errors_mutex;
2✔
4170
    std::condition_variable new_compensating_write;
2✔
4171
    std::vector<std::pair<ObjectId, sync::version_type>> error_to_download_version;
2✔
4172
    std::vector<sync::CompensatingWriteErrorInfo> compensating_writes;
2✔
4173
    sync::version_type download_version;
2✔
4174

4175
    config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_session,
2✔
4176
                                                        const SyncClientHookData& data) mutable {
23✔
4177
        auto session = weak_session.lock();
23✔
4178
        if (!session) {
23✔
4179
            return SyncClientHookAction::NoAction;
×
4180
        }
×
4181

4182
        if (data.event != SyncClientHookEvent::ErrorMessageReceived) {
23✔
4183
            if (data.event == SyncClientHookEvent::DownloadMessageReceived) {
19✔
4184
                download_version = data.progress.download.server_version;
5✔
4185
            }
5✔
4186

4187
            return SyncClientHookAction::NoAction;
19✔
4188
        }
19✔
4189

4190
        auto error_code = sync::ProtocolError(data.error_info->raw_error_code);
4✔
4191
        REQUIRE(error_code == sync::ProtocolError::compensating_write);
4!
4192
        REQUIRE(!data.error_info->compensating_writes.empty());
4!
4193
        std::lock_guard<std::mutex> lk(errors_mutex);
4✔
4194
        for (const auto& compensating_write : data.error_info->compensating_writes) {
4✔
4195
            error_to_download_version.emplace_back(compensating_write.primary_key.get_object_id(),
4✔
4196
                                                   *data.error_info->compensating_write_server_version);
4✔
4197
        }
4✔
4198

4199
        return SyncClientHookAction::NoAction;
4✔
4200
    };
4✔
4201

4202
    config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
4✔
4203
        std::unique_lock<std::mutex> lk(errors_mutex);
4✔
4204
        REQUIRE(error.status == ErrorCodes::SyncCompensatingWrite);
4!
4205
        for (const auto& compensating_write : error.compensating_writes_info) {
4✔
4206
            auto tracked_error = std::find_if(error_to_download_version.begin(), error_to_download_version.end(),
4✔
4207
                                              [&](const auto& pair) {
6✔
4208
                                                  return pair.first == compensating_write.primary_key.get_object_id();
6✔
4209
                                              });
6✔
4210
            REQUIRE(tracked_error != error_to_download_version.end());
4!
4211
            CHECK(tracked_error->second <= download_version);
4!
4212
            compensating_writes.push_back(compensating_write);
4✔
4213
        }
4✔
4214
        new_compensating_write.notify_one();
4✔
4215
    };
4✔
4216

4217
    auto realm = Realm::get_shared_realm(config);
2✔
4218

4219
    wait_for_upload(*realm);
2✔
4220
    wait_for_download(*realm);
2✔
4221

4222
    std::unique_lock<std::mutex> lk(errors_mutex);
2✔
4223
    new_compensating_write.wait_for(lk, std::chrono::seconds(30), [&] {
2✔
4224
        return compensating_writes.size() == 2;
2✔
4225
    });
2✔
4226

4227
    REQUIRE(compensating_writes.size() == 2);
2!
4228
    auto& write_info = compensating_writes[0];
2✔
4229
    CHECK(write_info.primary_key.is_type(type_ObjectId));
2!
4230
    CHECK(write_info.primary_key.get_object_id() == test_obj_id_1);
2!
4231
    CHECK(write_info.object_name == "TopLevel");
2!
4232
    CHECK_THAT(write_info.reason, Catch::Matchers::ContainsSubstring("object is outside of the current query view"));
2✔
4233

4234
    write_info = compensating_writes[1];
2✔
4235
    REQUIRE(write_info.primary_key.is_type(type_ObjectId));
2!
4236
    REQUIRE(write_info.primary_key.get_object_id() == test_obj_id_2);
2!
4237
    REQUIRE(write_info.object_name == "TopLevel");
2!
4238
    REQUIRE(write_info.reason ==
2!
4239
            util::format("write to ObjectID(\"%1\") in table \"TopLevel\" not allowed", test_obj_id_2));
2✔
4240
    auto top_level_table = realm->read_group().get_table("class_TopLevel");
2✔
4241
    REQUIRE(top_level_table->is_empty());
2!
4242
}
2✔
4243

4244
TEST_CASE("flx: bootstrap changesets are applied continuously", "[sync][flx][bootstrap][baas]") {
2✔
4245
    FLXSyncTestHarness harness("flx_bootstrap_ordering", {g_large_array_schema, {"queryable_int_field"}});
2✔
4246
    fill_large_array_schema(harness);
2✔
4247

4248
    std::unique_ptr<std::thread> th;
2✔
4249
    sync::version_type user_commit_version = UINT_FAST64_MAX;
2✔
4250
    sync::version_type bootstrap_version = UINT_FAST64_MAX;
2✔
4251
    SharedRealm realm;
2✔
4252
    std::condition_variable cv;
2✔
4253
    std::mutex mutex;
2✔
4254
    bool allow_to_commit = false;
2✔
4255

4256
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4257
    auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
4258
    auto shared_promise = std::make_shared<util::Promise<void>>(std::move(interrupted_promise));
2✔
4259
    config.sync_config->on_sync_client_event_hook =
2✔
4260
        [promise = std::move(shared_promise), &th, &realm, &user_commit_version, &bootstrap_version, &cv, &mutex,
2✔
4261
         &allow_to_commit](std::weak_ptr<SyncSession> weak_session, const SyncClientHookData& data) {
88✔
4262
            if (data.query_version == 0) {
88✔
4263
                return SyncClientHookAction::NoAction;
20✔
4264
            }
20✔
4265
            if (data.event != SyncClientHookEvent::DownloadMessageIntegrated) {
68✔
4266
                return SyncClientHookAction::NoAction;
56✔
4267
            }
56✔
4268
            auto session = weak_session.lock();
12✔
4269
            if (!session) {
12✔
4270
                return SyncClientHookAction::NoAction;
×
4271
            }
×
4272
            if (data.batch_state != sync::DownloadBatchState::MoreToCome) {
12✔
4273
                // Read version after bootstrap is done.
4274
                auto db = TestHelper::get_db(realm);
2✔
4275
                ReadTransaction rt(db);
2✔
4276
                bootstrap_version = rt.get_version();
2✔
4277
                {
2✔
4278
                    std::lock_guard<std::mutex> lock(mutex);
2✔
4279
                    allow_to_commit = true;
2✔
4280
                }
2✔
4281
                cv.notify_one();
2✔
4282
                session->force_close();
2✔
4283
                promise->emplace_value();
2✔
4284
                return SyncClientHookAction::NoAction;
2✔
4285
            }
2✔
4286

4287
            if (th) {
10✔
4288
                return SyncClientHookAction::NoAction;
8✔
4289
            }
8✔
4290

4291
            auto func = [&] {
2✔
4292
                // Attempt to commit a local change after the first bootstrap batch was committed.
4293
                auto db = TestHelper::get_db(realm);
2✔
4294
                WriteTransaction wt(db);
2✔
4295
                TableRef table = wt.get_table("class_TopLevel");
2✔
4296
                table->create_object_with_primary_key(ObjectId::gen());
2✔
4297
                {
2✔
4298
                    std::unique_lock<std::mutex> lock(mutex);
2✔
4299
                    // Wait to commit until we read the final bootstrap version.
4300
                    cv.wait(lock, [&] {
2✔
4301
                        return allow_to_commit;
2✔
4302
                    });
2✔
4303
                }
2✔
4304
                user_commit_version = wt.commit();
2✔
4305
            };
2✔
4306
            th = std::make_unique<std::thread>(std::move(func));
2✔
4307

4308
            return SyncClientHookAction::NoAction;
2✔
4309
        };
10✔
4310

4311
    realm = Realm::get_shared_realm(config);
2✔
4312
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
4313
    Query query(table);
2✔
4314
    {
2✔
4315
        auto new_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4316
        new_subs.insert_or_assign(query);
2✔
4317
        new_subs.commit();
2✔
4318
    }
2✔
4319
    interrupted.get();
2✔
4320
    th->join();
2✔
4321

4322
    // The user commit is the last one.
4323
    CHECK(user_commit_version == bootstrap_version + 1);
2!
4324
}
2✔
4325

4326
TEST_CASE("flx: open realm + register subscription callback while bootstrapping",
4327
          "[sync][flx][bootstrap][async open][baas]") {
14✔
4328
    FLXSyncTestHarness harness("flx_bootstrap_and_subscribe");
14✔
4329
    auto foo_obj_id = ObjectId::gen();
14✔
4330
    int64_t foo_obj_queryable_int = 5;
14✔
4331
    harness.load_initial_data([&](SharedRealm realm) {
14✔
4332
        CppContext c(realm);
14✔
4333
        Object::create(c, realm, "TopLevel",
14✔
4334
                       std::any(AnyDict{{"_id", foo_obj_id},
14✔
4335
                                        {"queryable_str_field", "foo"s},
14✔
4336
                                        {"queryable_int_field", foo_obj_queryable_int},
14✔
4337
                                        {"non_queryable_field", "created as initial data seed"s}}));
14✔
4338
    });
14✔
4339
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
14✔
4340

4341
    std::atomic<bool> subscription_invoked = false;
14✔
4342
    auto subscription_pf = util::make_promise_future<bool>();
14✔
4343
    // create a subscription to commit when realm is open for the first time or asked to rerun on open
4344
    auto init_subscription_callback_with_promise =
14✔
4345
        [&, promise_holder = util::CopyablePromiseHolder(std::move(subscription_pf.promise))](
14✔
4346
            std::shared_ptr<Realm> realm) mutable {
14✔
4347
            REQUIRE(realm);
8!
4348
            auto table = realm->read_group().get_table("class_TopLevel");
8✔
4349
            Query query(table);
8✔
4350
            auto subscription = realm->get_latest_subscription_set();
8✔
4351
            auto mutable_subscription = subscription.make_mutable_copy();
8✔
4352
            mutable_subscription.insert_or_assign(query);
8✔
4353
            auto promise = promise_holder.get_promise();
8✔
4354
            mutable_subscription.commit();
8✔
4355
            subscription_invoked = true;
8✔
4356
            promise.emplace_value(true);
8✔
4357
        };
8✔
4358
    // verify that the subscription has changed the database
4359
    auto verify_subscription = [](SharedRealm realm) {
14✔
4360
        REQUIRE(realm);
12!
4361
        auto table_ref = realm->read_group().get_table("class_TopLevel");
12✔
4362
        REQUIRE(table_ref);
12!
4363
        REQUIRE(table_ref->get_column_count() == 4);
12!
4364
        REQUIRE(table_ref->get_column_key("_id"));
12!
4365
        REQUIRE(table_ref->get_column_key("queryable_str_field"));
12!
4366
        REQUIRE(table_ref->get_column_key("queryable_int_field"));
12!
4367
        REQUIRE(table_ref->get_column_key("non_queryable_field"));
12!
4368
        REQUIRE(table_ref->size() == 1);
12!
4369
        auto str_col = table_ref->get_column_key("queryable_str_field");
12✔
4370
        REQUIRE(table_ref->get_object(0).get<String>(str_col) == "foo");
12!
4371
        return true;
12✔
4372
    };
12✔
4373

4374
    SECTION("Sync open") {
14✔
4375
        // sync open with subscription callback. Subscription will be run, since this is the first time that realm is
4376
        // opened
4377
        subscription_invoked = false;
2✔
4378
        config.sync_config->subscription_initializer = init_subscription_callback_with_promise;
2✔
4379
        auto realm = Realm::get_shared_realm(config);
2✔
4380
        REQUIRE(subscription_pf.future.get());
2!
4381
        auto sb = realm->get_latest_subscription_set();
2✔
4382
        auto future = sb.get_state_change_notification(realm::sync::SubscriptionSet::State::Complete);
2✔
4383
        auto state = future.get();
2✔
4384
        REQUIRE(state == realm::sync::SubscriptionSet::State::Complete);
2!
4385
        realm->refresh(); // refresh is needed otherwise table_ref->size() would be 0
2✔
4386
        REQUIRE(verify_subscription(realm));
2!
4387
    }
2✔
4388

4389
    SECTION("Sync Open + Async Open") {
14✔
4390
        {
2✔
4391
            subscription_invoked = false;
2✔
4392
            config.sync_config->subscription_initializer = init_subscription_callback_with_promise;
2✔
4393
            auto realm = Realm::get_shared_realm(config);
2✔
4394
            REQUIRE(subscription_pf.future.get());
2!
4395
            auto sb = realm->get_latest_subscription_set();
2✔
4396
            auto future = sb.get_state_change_notification(realm::sync::SubscriptionSet::State::Complete);
2✔
4397
            auto state = future.get();
2✔
4398
            REQUIRE(state == realm::sync::SubscriptionSet::State::Complete);
2!
4399
            realm->refresh(); // refresh is needed otherwise table_ref->size() would be 0
2✔
4400
            REQUIRE(verify_subscription(realm));
2!
4401
        }
2✔
4402
        {
2✔
4403
            auto subscription_pf_async = util::make_promise_future<bool>();
2✔
4404
            auto init_subscription_asyc_callback =
2✔
4405
                [promise_holder_async = util::CopyablePromiseHolder(std::move(subscription_pf_async.promise))](
2✔
4406
                    std::shared_ptr<Realm> realm) mutable {
2✔
4407
                    REQUIRE(realm);
2!
4408
                    auto table = realm->read_group().get_table("class_TopLevel");
2✔
4409
                    Query query(table);
2✔
4410
                    auto subscription = realm->get_latest_subscription_set();
2✔
4411
                    auto mutable_subscription = subscription.make_mutable_copy();
2✔
4412
                    mutable_subscription.insert_or_assign(query);
2✔
4413
                    auto promise = promise_holder_async.get_promise();
2✔
4414
                    mutable_subscription.commit();
2✔
4415
                    promise.emplace_value(true);
2✔
4416
                };
2✔
4417
            auto open_realm_pf = util::make_promise_future<bool>();
2✔
4418
            auto open_realm_completed_callback = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
2✔
4419
                bool result = false;
2✔
4420
                if (!err) {
2✔
4421
                    result =
2✔
4422
                        verify_subscription(Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
2✔
4423
                }
2✔
4424
                open_realm_pf.promise.emplace_value(result);
2✔
4425
            };
2✔
4426

4427
            config.sync_config->subscription_initializer = init_subscription_asyc_callback;
2✔
4428
            config.sync_config->rerun_init_subscription_on_open = true;
2✔
4429
            auto async_open = Realm::get_synchronized_realm(config);
2✔
4430
            async_open->start(open_realm_completed_callback);
2✔
4431
            REQUIRE(open_realm_pf.future.get());
2!
4432
            REQUIRE(subscription_pf_async.future.get());
2!
4433
            config.sync_config->rerun_init_subscription_on_open = false;
2✔
4434
            auto realm = Realm::get_shared_realm(config);
2✔
4435
            REQUIRE(realm->get_latest_subscription_set().version() == 2);
2!
4436
            REQUIRE(realm->get_active_subscription_set().version() == 2);
2!
4437
        }
2✔
4438
    }
2✔
4439

4440
    SECTION("Async open") {
14✔
4441
        SECTION("Initial async open with no rerun on open set") {
10✔
4442
            // subscription will be run since this is the first time we are opening the realm file.
4443
            auto open_realm_pf = util::make_promise_future<bool>();
4✔
4444
            auto open_realm_completed_callback = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
4✔
4445
                bool result = false;
4✔
4446
                if (!err) {
4✔
4447
                    result =
4✔
4448
                        verify_subscription(Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
4✔
4449
                }
4✔
4450
                open_realm_pf.promise.emplace_value(result);
4✔
4451
            };
4✔
4452

4453
            config.sync_config->subscription_initializer = init_subscription_callback_with_promise;
4✔
4454
            auto async_open = Realm::get_synchronized_realm(config);
4✔
4455
            async_open->start(open_realm_completed_callback);
4✔
4456
            REQUIRE(open_realm_pf.future.get());
4!
4457
            REQUIRE(subscription_pf.future.get());
4!
4458

4459
            SECTION("rerun on open = false. Subscription not run") {
4✔
4460
                subscription_invoked = false;
2✔
4461
                auto async_open = Realm::get_synchronized_realm(config);
2✔
4462
                auto open_realm_pf = util::make_promise_future<bool>();
2✔
4463
                auto open_realm_completed_callback = [&](ThreadSafeReference, std::exception_ptr e) mutable {
2✔
4464
                    // no need to verify if the subscription has changed the db, since it has not run as we test
4465
                    // below
4466
                    open_realm_pf.promise.emplace_value(!e);
2✔
4467
                };
2✔
4468
                async_open->start(open_realm_completed_callback);
2✔
4469
                REQUIRE(open_realm_pf.future.get());
2!
4470
                REQUIRE_FALSE(subscription_invoked.load());
2!
4471
            }
2✔
4472

4473
            SECTION("rerun on open = true. Subscription not run cause realm already opened once") {
4✔
4474
                subscription_invoked = false;
2✔
4475
                auto realm = Realm::get_shared_realm(config);
2✔
4476
                auto init_subscription = [&subscription_invoked](std::shared_ptr<Realm> realm) mutable {
2✔
4477
                    REQUIRE(realm);
×
4478
                    auto table = realm->read_group().get_table("class_TopLevel");
×
4479
                    Query query(table);
×
4480
                    auto subscription = realm->get_latest_subscription_set();
×
4481
                    auto mutable_subscription = subscription.make_mutable_copy();
×
4482
                    mutable_subscription.insert_or_assign(query);
×
4483
                    mutable_subscription.commit();
×
4484
                    subscription_invoked.store(true);
×
4485
                };
×
4486
                config.sync_config->rerun_init_subscription_on_open = true;
2✔
4487
                config.sync_config->subscription_initializer = init_subscription;
2✔
4488
                auto async_open = Realm::get_synchronized_realm(config);
2✔
4489
                auto open_realm_pf = util::make_promise_future<bool>();
2✔
4490
                auto open_realm_completed_callback = [&](ThreadSafeReference, std::exception_ptr e) mutable {
2✔
4491
                    // no need to verify if the subscription has changed the db, since it has not run as we test
4492
                    // below
4493
                    open_realm_pf.promise.emplace_value(!e);
2✔
4494
                };
2✔
4495
                async_open->start(open_realm_completed_callback);
2✔
4496
                REQUIRE(open_realm_pf.future.get());
2!
4497
                REQUIRE_FALSE(subscription_invoked.load());
2!
4498
                REQUIRE(realm->get_latest_subscription_set().version() == 1);
2!
4499
                REQUIRE(realm->get_active_subscription_set().version() == 1);
2!
4500
            }
2✔
4501
        }
4✔
4502

4503
        SECTION("rerun on open set for multiple async open tasks (subscription runs only once)") {
10✔
4504
            auto init_subscription = [](std::shared_ptr<Realm> realm) mutable {
8✔
4505
                REQUIRE(realm);
8!
4506
                auto table = realm->read_group().get_table("class_TopLevel");
8✔
4507
                Query query(table);
8✔
4508
                auto subscription = realm->get_latest_subscription_set();
8✔
4509
                auto mutable_subscription = subscription.make_mutable_copy();
8✔
4510
                mutable_subscription.insert_or_assign(query);
8✔
4511
                mutable_subscription.commit();
8✔
4512
            };
8✔
4513

4514
            auto open_task1_pf = util::make_promise_future<SharedRealm>();
4✔
4515
            auto open_task2_pf = util::make_promise_future<SharedRealm>();
4✔
4516
            auto open_callback1 = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
4✔
4517
                REQUIRE_FALSE(err);
4!
4518
                open_task1_pf.promise.emplace_value(
4✔
4519
                    Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
4✔
4520
            };
4✔
4521
            auto open_callback2 = [&](ThreadSafeReference ref, std::exception_ptr err) mutable {
4✔
4522
                REQUIRE_FALSE(err);
4!
4523
                open_task2_pf.promise.emplace_value(
4✔
4524
                    Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
4✔
4525
            };
4✔
4526

4527
            config.sync_config->rerun_init_subscription_on_open = true;
4✔
4528
            config.sync_config->subscription_initializer = init_subscription;
4✔
4529

4530
            SECTION("Realm was already created, but we want to rerun on first open using multiple tasks") {
4✔
4531
                {
2✔
4532
                    subscription_invoked = false;
2✔
4533
                    auto realm = Realm::get_shared_realm(config);
2✔
4534
                    auto sb = realm->get_latest_subscription_set();
2✔
4535
                    auto future = sb.get_state_change_notification(realm::sync::SubscriptionSet::State::Complete);
2✔
4536
                    auto state = future.get();
2✔
4537
                    REQUIRE(state == realm::sync::SubscriptionSet::State::Complete);
2!
4538
                    realm->refresh(); // refresh is needed otherwise table_ref->size() would be 0
2✔
4539
                    REQUIRE(verify_subscription(realm));
2!
4540
                    REQUIRE(realm->get_latest_subscription_set().version() == 1);
2!
4541
                    REQUIRE(realm->get_active_subscription_set().version() == 1);
2!
4542
                }
2✔
4543

4544
                auto async_open_task1 = Realm::get_synchronized_realm(config);
2✔
4545
                auto async_open_task2 = Realm::get_synchronized_realm(config);
2✔
4546
                async_open_task1->start(open_callback1);
2✔
4547
                async_open_task2->start(open_callback2);
2✔
4548

4549
                auto realm1 = open_task1_pf.future.get();
2✔
4550
                auto realm2 = open_task2_pf.future.get();
2✔
4551

4552
                const auto version_expected = 2;
2✔
4553
                auto r1_latest = realm1->get_latest_subscription_set().version();
2✔
4554
                auto r1_active = realm1->get_active_subscription_set().version();
2✔
4555
                REQUIRE(realm2->get_latest_subscription_set().version() == r1_latest);
2!
4556
                REQUIRE(realm2->get_active_subscription_set().version() == r1_active);
2!
4557
                REQUIRE(r1_latest == version_expected);
2!
4558
                REQUIRE(r1_active == version_expected);
2!
4559
            }
2✔
4560
            SECTION("First time realm is created but opened via open async. Both tasks could run the subscription") {
4✔
4561
                auto async_open_task1 = Realm::get_synchronized_realm(config);
2✔
4562
                auto async_open_task2 = Realm::get_synchronized_realm(config);
2✔
4563
                async_open_task1->start(open_callback1);
2✔
4564
                async_open_task2->start(open_callback2);
2✔
4565
                auto realm1 = open_task1_pf.future.get();
2✔
4566
                auto realm2 = open_task2_pf.future.get();
2✔
4567

4568
                auto r1_latest = realm1->get_latest_subscription_set().version();
2✔
4569
                auto r1_active = realm1->get_active_subscription_set().version();
2✔
4570
                REQUIRE(realm2->get_latest_subscription_set().version() == r1_latest);
2!
4571
                REQUIRE(realm2->get_active_subscription_set().version() == r1_active);
2!
4572
                // the callback may be run twice, if task1 is the first task to open realm
4573
                // but it is scheduled after tasks2, which have opened realm later but
4574
                // by the time it runs, subscription version is equal to 0 (realm creation).
4575
                // This can only happen the first time that realm is created. All the other times
4576
                // the init_sb callback is guaranteed to run once.
4577
                REQUIRE(r1_latest >= 1);
2!
4578
                REQUIRE(r1_latest <= 2);
2!
4579
                REQUIRE(r1_active >= 1);
2!
4580
                REQUIRE(r1_active <= 2);
2!
4581
            }
2✔
4582
        }
4✔
4583

4584
        SECTION("Wait to bootstrap all pending subscriptions even when subscription_initializer is not used") {
10✔
4585
            // Client 1
4586
            {
2✔
4587
                auto realm = Realm::get_shared_realm(config);
2✔
4588
                // Create subscription (version = 1) and bootstrap data.
4589
                subscribe_to_all_and_bootstrap(*realm);
2✔
4590
                realm->sync_session()->shutdown_and_wait();
2✔
4591

4592
                // Create a new subscription (version = 2) while the session is closed.
4593
                // The new subscription does not match the object bootstrapped at version 1.
4594
                auto mutable_subscription = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4595
                mutable_subscription.clear();
2✔
4596
                auto table = realm->read_group().get_table("class_TopLevel");
2✔
4597
                auto queryable_int_field = table->get_column_key("queryable_int_field");
2✔
4598
                mutable_subscription.insert_or_assign(
2✔
4599
                    Query(table).not_equal(queryable_int_field, foo_obj_queryable_int));
2✔
4600
                mutable_subscription.commit();
2✔
4601

4602
                realm->close();
2✔
4603
            }
2✔
4604

4605
            REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(config.path));
2!
4606

4607
            // Client 2 uploads data matching Client 1's subscription at version 1
4608
            harness.load_initial_data([&](SharedRealm realm) {
2✔
4609
                CppContext c(realm);
2✔
4610
                Object::create(c, realm, "TopLevel",
2✔
4611
                               std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
4612
                                                {"queryable_str_field", "bar"s},
2✔
4613
                                                {"queryable_int_field", 2 * foo_obj_queryable_int},
2✔
4614
                                                {"non_queryable_field", "some data"s}}));
2✔
4615
            });
2✔
4616

4617
            // Client 1 opens the realm asynchronously and expects the task to complete
4618
            // when the subscription at version 2 finishes bootstrapping.
4619
            auto realm = successfully_async_open_realm(config);
2✔
4620

4621
            // Check subscription at version 2 is marked Complete.
4622
            CHECK(realm->get_latest_subscription_set().version() == 2);
2!
4623
            CHECK(realm->get_active_subscription_set().version() == 2);
2!
4624
        }
2✔
4625
    }
10✔
4626
}
14✔
4627
TEST_CASE("flx sync: Client reset during async open", "[sync][flx][client reset][async open][baas]") {
2✔
4628
    FLXSyncTestHarness harness("flx_bootstrap_reset");
2✔
4629
    auto foo_obj_id = ObjectId::gen();
2✔
4630
    std::atomic<bool> subscription_invoked = false;
2✔
4631
    harness.load_initial_data([&](SharedRealm realm) {
2✔
4632
        CppContext c(realm);
2✔
4633
        Object::create(c, realm, "TopLevel",
2✔
4634
                       std::any(AnyDict{{"_id", foo_obj_id},
2✔
4635
                                        {"queryable_str_field", "foo"s},
2✔
4636
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
4637
                                        {"non_queryable_field", "created as initial data seed"s}}));
2✔
4638
    });
2✔
4639
    SyncTestFile realm_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4640

4641
    auto subscription_callback = [&](std::shared_ptr<Realm> realm) {
2✔
4642
        REQUIRE(realm);
2!
4643
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
4644
        Query query(table);
2✔
4645
        auto subscription = realm->get_latest_subscription_set();
2✔
4646
        auto mutable_subscription = subscription.make_mutable_copy();
2✔
4647
        mutable_subscription.insert_or_assign(query);
2✔
4648
        subscription_invoked = true;
2✔
4649
        mutable_subscription.commit();
2✔
4650
    };
2✔
4651

4652
    realm_config.sync_config->client_resync_mode = ClientResyncMode::Recover;
2✔
4653
    realm_config.sync_config->subscription_initializer = subscription_callback;
2✔
4654

4655
    bool client_reset_triggered = false;
2✔
4656
    realm_config.sync_config->on_sync_client_event_hook = [&](std::weak_ptr<SyncSession> weak_sess,
2✔
4657
                                                              const SyncClientHookData& event_data) {
65✔
4658
        auto sess = weak_sess.lock();
65✔
4659
        if (!sess) {
65✔
4660
            return SyncClientHookAction::NoAction;
×
4661
        }
×
4662
        if (sess->path() != realm_config.path) {
65✔
4663
            return SyncClientHookAction::NoAction;
24✔
4664
        }
24✔
4665

4666
        if (event_data.event != SyncClientHookEvent::DownloadMessageReceived) {
41✔
4667
            return SyncClientHookAction::NoAction;
35✔
4668
        }
35✔
4669

4670
        if (client_reset_triggered) {
6✔
4671
            return SyncClientHookAction::NoAction;
4✔
4672
        }
4✔
4673

4674
        client_reset_triggered = true;
2✔
4675
        reset_utils::trigger_client_reset(harness.session().app_session(), *sess);
2✔
4676
        return SyncClientHookAction::SuspendWithRetryableError;
2✔
4677
    };
6✔
4678

4679
    auto before_callback_called = util::make_promise_future<void>();
2✔
4680
    realm_config.sync_config->notify_before_client_reset = [&](std::shared_ptr<Realm> realm) {
2✔
4681
        CHECK(realm->schema_version() == 0);
2!
4682
        before_callback_called.promise.emplace_value();
2✔
4683
    };
2✔
4684

4685
    auto after_callback_called = util::make_promise_future<void>();
2✔
4686
    realm_config.sync_config->notify_after_client_reset = [&](std::shared_ptr<Realm> realm, ThreadSafeReference,
2✔
4687
                                                              bool) {
2✔
4688
        CHECK(realm->schema_version() == 0);
2!
4689
        after_callback_called.promise.emplace_value();
2✔
4690
    };
2✔
4691

4692
    auto realm_task = Realm::get_synchronized_realm(realm_config);
2✔
4693
    auto realm_pf = util::make_promise_future<SharedRealm>();
2✔
4694
    realm_task->start([&](ThreadSafeReference ref, std::exception_ptr ex) {
2✔
4695
        auto& promise = realm_pf.promise;
2✔
4696
        try {
2✔
4697
            if (ex) {
2✔
4698
                std::rethrow_exception(ex);
×
4699
            }
×
4700
            promise.emplace_value(Realm::get_shared_realm(std::move(ref), util::Scheduler::make_dummy()));
2✔
4701
        }
2✔
4702
        catch (...) {
2✔
4703
            promise.set_error(exception_to_status());
×
4704
        }
×
4705
    });
2✔
4706
    auto realm = realm_pf.future.get();
2✔
4707
    before_callback_called.future.get();
2✔
4708
    after_callback_called.future.get();
2✔
4709
    REQUIRE(subscription_invoked.load());
2!
4710
}
2✔
4711

4712
// Test that resending pending subscription sets does not cause any inconsistencies in the progress cursors.
4713
TEST_CASE("flx sync: resend pending subscriptions when reconnecting", "[sync][flx][baas]") {
2✔
4714
    FLXSyncTestHarness harness("flx_pending_subscriptions", {g_large_array_schema, {"queryable_int_field"}});
2✔
4715

4716
    std::vector<ObjectId> obj_ids_at_end = fill_large_array_schema(harness);
2✔
4717
    SyncTestFile interrupted_realm_config(harness.app()->current_user(), harness.schema(),
2✔
4718
                                          SyncConfig::FLXSyncEnabled{});
2✔
4719

4720
    {
2✔
4721
        auto pf = util::make_promise_future<void>();
2✔
4722
        Realm::Config config = interrupted_realm_config;
2✔
4723
        config.sync_config = std::make_shared<SyncConfig>(*interrupted_realm_config.sync_config);
2✔
4724
        config.sync_config->on_sync_client_event_hook =
2✔
4725
            [promise = util::CopyablePromiseHolder(std::move(pf.promise))](std::weak_ptr<SyncSession> weak_session,
2✔
4726
                                                                           const SyncClientHookData& data) mutable {
84✔
4727
                if (data.event != SyncClientHookEvent::BootstrapMessageProcessed &&
84✔
4728
                    data.event != SyncClientHookEvent::BootstrapProcessed) {
84✔
4729
                    return SyncClientHookAction::NoAction;
66✔
4730
                }
66✔
4731
                auto session = weak_session.lock();
18✔
4732
                if (!session) {
18✔
4733
                    return SyncClientHookAction::NoAction;
×
4734
                }
×
4735
                if (data.query_version != 1) {
18✔
4736
                    return SyncClientHookAction::NoAction;
4✔
4737
                }
4✔
4738

4739
                // Commit a subscriptions set whenever a bootstrap message is received for query version 1.
4740
                if (data.event == SyncClientHookEvent::BootstrapMessageProcessed) {
14✔
4741
                    auto latest_subs = session->get_flx_subscription_store()->get_latest().make_mutable_copy();
12✔
4742
                    latest_subs.commit();
12✔
4743
                    return SyncClientHookAction::NoAction;
12✔
4744
                }
12✔
4745
                // At least one subscription set was created.
4746
                CHECK(session->get_flx_subscription_store()->get_latest().version() > 1);
2!
4747
                promise.get_promise().emplace_value();
2✔
4748
                // Reconnect once query version 1 is bootstrapped.
4749
                return SyncClientHookAction::TriggerReconnect;
2✔
4750
            };
2✔
4751

4752
        auto realm = Realm::get_shared_realm(config);
2✔
4753
        {
2✔
4754
            auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
2✔
4755
            auto table = realm->read_group().get_table("class_TopLevel");
2✔
4756
            mut_subs.insert_or_assign(Query(table));
2✔
4757
            mut_subs.commit();
2✔
4758
        }
2✔
4759
        pf.future.get();
2✔
4760
        realm->sync_session()->shutdown_and_wait();
2✔
4761
        realm->close();
2✔
4762
    }
2✔
4763

4764
    REQUIRE_FALSE(_impl::RealmCoordinator::get_existing_coordinator(interrupted_realm_config.path));
2!
4765

4766
    // Check at least one subscription set needs to be resent.
4767
    {
2✔
4768
        DBOptions options;
2✔
4769
        options.encryption_key = test_util::crypt_key();
2✔
4770
        auto realm = DB::create(sync::make_client_replication(), interrupted_realm_config.path, options);
2✔
4771
        auto sub_store = sync::SubscriptionStore::create(realm);
2✔
4772
        auto version_info = sub_store->get_version_info();
2✔
4773
        REQUIRE(version_info.latest > version_info.active);
2!
4774
    }
2✔
4775

4776
    // Resend the pending subscriptions.
4777
    auto realm = Realm::get_shared_realm(interrupted_realm_config);
2✔
4778
    wait_for_upload(*realm);
2✔
4779
    wait_for_download(*realm);
2✔
4780
}
2✔
4781

4782
TEST_CASE("flx: fatal errors and session becoming inactive cancel pending waits", "[sync][flx][baas]") {
2✔
4783
    std::vector<ObjectSchema> schema{
2✔
4784
        {"TopLevel",
2✔
4785
         {
2✔
4786
             {"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
4787
             {"queryable_int_field", PropertyType::Int | PropertyType::Nullable},
2✔
4788
         }},
2✔
4789
    };
2✔
4790

4791
    FLXSyncTestHarness harness("flx_cancel_pending_waits", {schema, {"queryable_int_field"}});
2✔
4792
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4793

4794
    auto check_status = [](auto status) {
4✔
4795
        CHECK(!status.is_ok());
4!
4796
        std::string reason = status.get_status().reason();
4✔
4797
        // Subscription notification is cancelled either because the sync session is inactive, or because a fatal
4798
        // error is received from the server.
4799
        if (reason.find("Sync session became inactive") == std::string::npos &&
4✔
4800
            reason.find("Invalid schema change (UPLOAD): non-breaking schema change: adding \"Int\" column at field "
4✔
4801
                        "\"other_col\" in schema \"TopLevel\", schema changes from clients are restricted when "
2✔
4802
                        "developer mode is disabled") == std::string::npos) {
2✔
4803
            FAIL(reason);
×
4804
        }
×
4805
    };
4✔
4806

4807
    auto create_subscription = [](auto realm) -> realm::sync::SubscriptionSet {
4✔
4808
        auto mut_subs = realm->get_latest_subscription_set().make_mutable_copy();
4✔
4809
        auto table = realm->read_group().get_table("class_TopLevel");
4✔
4810
        mut_subs.insert_or_assign(Query(table));
4✔
4811
        return mut_subs.commit();
4✔
4812
    };
4✔
4813

4814
    auto [error_occured_promise, error_occurred] = util::make_promise_future<void>();
2✔
4815
    config.sync_config->error_handler = [promise = util::CopyablePromiseHolder(std::move(error_occured_promise))](
2✔
4816
                                            std::shared_ptr<SyncSession>, SyncError) mutable {
2✔
4817
        promise.get_promise().emplace_value();
2✔
4818
    };
2✔
4819

4820
    auto realm = Realm::get_shared_realm(config);
2✔
4821
    wait_for_download(*realm);
2✔
4822

4823
    auto subs = create_subscription(realm);
2✔
4824
    auto subs_future = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
4825

4826
    realm->sync_session()->pause();
2✔
4827
    auto state = subs_future.get_no_throw();
2✔
4828
    check_status(state);
2✔
4829

4830
    auto [download_complete_promise, download_complete] = util::make_promise_future<void>();
2✔
4831
    realm->sync_session()->wait_for_upload_completion([promise = std::move(download_complete_promise)](auto) mutable {
2✔
4832
        promise.emplace_value();
2✔
4833
    });
2✔
4834
    schema[0].persisted_properties.push_back({"other_col", PropertyType::Int | PropertyType::Nullable});
2✔
4835
    realm->update_schema(schema);
2✔
4836

4837
    subs = create_subscription(realm);
2✔
4838
    subs_future = subs.get_state_change_notification(sync::SubscriptionSet::State::Complete);
2✔
4839

4840
    harness.load_initial_data([&](SharedRealm realm) {
2✔
4841
        CppContext c(realm);
2✔
4842
        Object::create(c, realm, "TopLevel",
2✔
4843
                       std::any(AnyDict{{"_id", ObjectId::gen()},
2✔
4844
                                        {"queryable_int_field", static_cast<int64_t>(5)},
2✔
4845
                                        {"other_col", static_cast<int64_t>(42)}}));
2✔
4846
    });
2✔
4847

4848
    realm->sync_session()->resume();
2✔
4849
    download_complete.get();
2✔
4850
    error_occurred.get();
2✔
4851
    state = subs_future.get_no_throw();
2✔
4852
    check_status(state);
2✔
4853
}
2✔
4854

4855
TEST_CASE("flx: pause and resume bootstrapping at query version 0", "[sync][flx][baas]") {
2✔
4856
    FLXSyncTestHarness harness("flx_pause_resume_bootstrap");
2✔
4857
    SyncTestFile triggered_config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4858
    auto [interrupted_promise, interrupted] = util::make_promise_future<void>();
2✔
4859
    std::mutex download_message_mutex;
2✔
4860
    int download_message_integrated_count = 0;
2✔
4861
    triggered_config.sync_config->on_sync_client_event_hook =
2✔
4862
        [promise = util::CopyablePromiseHolder(std::move(interrupted_promise)), &download_message_integrated_count,
2✔
4863
         &download_message_mutex](std::weak_ptr<SyncSession> weak_sess, const SyncClientHookData& data) mutable {
26✔
4864
            auto sess = weak_sess.lock();
26✔
4865
            if (!sess || data.event != SyncClientHookEvent::DownloadMessageIntegrated) {
26✔
4866
                return SyncClientHookAction::NoAction;
22✔
4867
            }
22✔
4868

4869
            std::lock_guard<std::mutex> lk(download_message_mutex);
4✔
4870
            // Pause and resume the first session after the bootstrap message is integrated.
4871
            if (download_message_integrated_count == 0) {
4✔
4872
                sess->pause();
2✔
4873
                sess->resume();
2✔
4874
            }
2✔
4875
            // Complete the test when the second session integrates the empty download
4876
            // message it receives.
4877
            else {
2✔
4878
                promise.get_promise().emplace_value();
2✔
4879
            }
2✔
4880
            ++download_message_integrated_count;
4✔
4881
            return SyncClientHookAction::NoAction;
4✔
4882
        };
26✔
4883
    auto realm = Realm::get_shared_realm(triggered_config);
2✔
4884
    interrupted.get();
2✔
4885
    std::lock_guard<std::mutex> lk(download_message_mutex);
2✔
4886
    CHECK(download_message_integrated_count == 2);
2!
4887
    auto active_sub_set = realm->sync_session()->get_flx_subscription_store()->get_active();
2✔
4888
    REQUIRE(active_sub_set.version() == 0);
2!
4889
    REQUIRE(active_sub_set.state() == sync::SubscriptionSet::State::Complete);
2!
4890
}
2✔
4891

4892
TEST_CASE("flx: collections in mixed - merge lists", "[sync][flx][baas]") {
2✔
4893
    Schema schema{{"TopLevel",
2✔
4894
                   {{"_id", PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
4895
                    {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
4896
                    {"any", PropertyType::Mixed | PropertyType::Nullable}}}};
2✔
4897

4898
    FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field"}};
2✔
4899
    FLXSyncTestHarness harness("flx_collections_in_mixed", server_schema);
2✔
4900
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4901

4902
    auto set_list_and_insert_element = [](Obj& obj, ColKey col_any, Mixed value) {
8✔
4903
        obj.set_collection(col_any, CollectionType::List);
8✔
4904
        auto list = obj.get_list_ptr<Mixed>(col_any);
8✔
4905
        list->add(value);
8✔
4906
    };
8✔
4907

4908
    // Client 1 creates an object and sets property 'any' to an integer value.
4909
    auto foo_obj_id = ObjectId::gen();
2✔
4910
    harness.load_initial_data([&](SharedRealm realm) {
2✔
4911
        CppContext c(realm);
2✔
4912
        Object::create(c, realm, "TopLevel",
2✔
4913
                       std::any(AnyDict{{"_id", foo_obj_id}, {"queryable_str_field", "foo"s}, {"any", 42}}));
2✔
4914
    });
2✔
4915

4916
    // Client 2 opens the realm and downloads schema and object created by Client 1.
4917
    auto realm = Realm::get_shared_realm(config);
2✔
4918
    subscribe_to_all_and_bootstrap(*realm);
2✔
4919
    realm->sync_session()->pause();
2✔
4920

4921
    // Client 3 sets property 'any' to List and inserts two integers in the list.
4922
    harness.load_initial_data([&](SharedRealm realm) {
2✔
4923
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
4924
        auto obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
4925
        auto col_any = table->get_column_key("any");
2✔
4926
        set_list_and_insert_element(obj, col_any, 1);
2✔
4927
        set_list_and_insert_element(obj, col_any, 2);
2✔
4928
    });
2✔
4929

4930
    // While its session is paused, Client 2 sets property 'any' to List and inserts two integers in the list.
4931
    CppContext c(realm);
2✔
4932
    realm->begin_transaction();
2✔
4933
    auto table = realm->read_group().get_table("class_TopLevel");
2✔
4934
    auto obj = table->get_object_with_primary_key(Mixed{foo_obj_id});
2✔
4935
    auto col_any = table->get_column_key("any");
2✔
4936
    set_list_and_insert_element(obj, col_any, 3);
2✔
4937
    set_list_and_insert_element(obj, col_any, 4);
2✔
4938
    realm->commit_transaction();
2✔
4939

4940
    realm->sync_session()->resume();
2✔
4941
    wait_for_upload(*realm);
2✔
4942
    wait_for_download(*realm);
2✔
4943
    wait_for_advance(*realm);
2✔
4944

4945
    // Client 2 ends up with four integers in the list (in the correct order).
4946
    auto list = obj.get_list_ptr<Mixed>(col_any);
2✔
4947
    CHECK(list->size() == 4);
2!
4948
    CHECK(list->get(0) == 1);
2!
4949
    CHECK(list->get(1) == 2);
2!
4950
    CHECK(list->get(2) == 3);
2!
4951
    CHECK(list->get(3) == 4);
2!
4952
}
2✔
4953

4954
TEST_CASE("flx: nested collections in mixed", "[sync][flx][baas]") {
2✔
4955
    Schema schema{{"TopLevel",
2✔
4956
                   {{"_id", PropertyType::Int, Property::IsPrimary{true}},
2✔
4957
                    {"queryable_str_field", PropertyType::String | PropertyType::Nullable},
2✔
4958
                    {"any", PropertyType::Mixed | PropertyType::Nullable}}}};
2✔
4959

4960
    FLXSyncTestHarness::ServerSchema server_schema{schema, {"queryable_str_field"}};
2✔
4961
    FLXSyncTestHarness harness("flx_collections_in_mixed", server_schema);
2✔
4962
    SyncTestFile config(harness.app()->current_user(), harness.schema(), SyncConfig::FLXSyncEnabled{});
2✔
4963

4964
    // Client 1: {_id: 1, any: ["abc", [42]]}
4965
    harness.load_initial_data([&](SharedRealm realm) {
2✔
4966
        CppContext c(realm);
2✔
4967
        auto obj = Object::create(c, realm, "TopLevel",
2✔
4968
                                  std::any(AnyDict{{"_id", INT64_C(1)}, {"queryable_str_field", "foo"s}}));
2✔
4969
        auto table = realm->read_group().get_table("class_TopLevel");
2✔
4970
        auto col_any = table->get_column_key("any");
2✔
4971
        obj.get_obj().set_collection(col_any, CollectionType::List);
2✔
4972
        List list(obj, obj.get_object_schema().property_for_name("any"));
2✔
4973
        list.insert_any(0, "abc");
2✔
4974
        list.insert_collection(1, CollectionType::List);
2✔
4975
        auto list2 = list.get_list(1);
2✔
4976
        list2.insert_any(0, 42);
2✔
4977
    });
2✔
4978

4979
    // Client 2 opens the realm and downloads schema and object created by Client 1.
4980
    // {_id: 1, any: ["abc", [42]]}
4981
    auto realm = Realm::get_shared_realm(config);
2✔
4982
    subscribe_to_all_and_bootstrap(*realm);
2✔
4983
    realm->sync_session()->pause();
2✔
4984

4985
    // Client 3 adds a dictionary with an element to list 'any'
4986
    // {_id: 1, any: [{{"key": 6}}, "abc", [42]]}
4987
    harness.load_initial_data([&](SharedRealm realm) {
2✔
4988
        CppContext c(realm);
2✔
4989
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any(INT64_C(1)));
2✔
4990
        List list(obj, obj.get_object_schema().property_for_name("any"));
2✔
4991
        list.insert_collection(PathElement(0), CollectionType::Dictionary);
2✔
4992
        auto dict = list.get_dictionary(PathElement(0));
2✔
4993
        dict.insert_any("key", INT64_C(6));
2✔
4994
    });
2✔
4995

4996
    // While its session is paused, Client 2 makes a change to a nested list
4997
    // {_id: 1, any: ["abc", [42, "foo"]]}
4998
    CppContext c(realm);
2✔
4999
    realm->begin_transaction();
2✔
5000
    auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any(INT64_C(1)));
2✔
5001
    List list(obj, obj.get_object_schema().property_for_name("any"));
2✔
5002
    List list2 = list.get_list(PathElement(1));
2✔
5003
    list2.insert_any(list2.size(), "foo");
2✔
5004
    realm->commit_transaction();
2✔
5005

5006
    realm->sync_session()->resume();
2✔
5007
    wait_for_upload(*realm);
2✔
5008
    wait_for_download(*realm);
2✔
5009
    wait_for_advance(*realm);
2✔
5010

5011
    // Client 2 after the session is resumed
5012
    // {_id: 1, any: [{{"key": 6}}, "abc", [42, "foo"]]}
5013
    CHECK(list.size() == 3);
2!
5014
    auto nested_dict = list.get_dictionary(0);
2✔
5015
    CHECK(nested_dict.size() == 1);
2!
5016
    CHECK(nested_dict.get<Int>("key") == 6);
2!
5017

5018
    CHECK(list.get_any(1) == "abc");
2!
5019

5020
    auto nested_list = list.get_list(2);
2✔
5021
    CHECK(nested_list.size() == 2);
2!
5022
    CHECK(nested_list.get_any(0) == 42);
2!
5023
    CHECK(nested_list.get_any(1) == "foo");
2!
5024
}
2✔
5025

5026
} // namespace realm::app
5027

5028
#endif // REALM_ENABLE_AUTH_TESTS
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